Advertisement
Guest User

Untitled

a guest
Oct 19th, 2017
69
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 13.00 KB | None | 0 0
  1. Klasy lepiej trzymać w osobnych plikach, wyodrębniłem class Object do Object.cs.
  2. Nazwa Object jest trochę zbyt ogólna, warto zmienić ją na coś informującego. Dam na razie Option.
  3. picture zmienię na pictureFilePath, a number na id
  4.  
  5. Random rand = new Random();
  6. int move = rand.Next(mode);
  7. Option returned = new Option();
  8. for (int i = 0; i < mode; i++)
  9. {
  10.     if (move == objects[i].ID)
  11.         returned = new Option(objects[i]);
  12. }
  13.  
  14. Obiekty do tej tablicy:
  15.  
  16. Option[] objects =
  17. {
  18.     paper, rock, scissors, lizard, spock
  19. };
  20.  
  21. wrzuciłaś po kolei, zgodnie z ich numerami id (czy tam wcześniej number), więc nie ma potrzeby przeszukiwać tej całej tablicy w poszukiwaniu obiektu o danym id (number). Wystarczy:
  22.  
  23. Random rand = new Random();
  24. int move = rand.Next(mode);
  25. return objects[move];
  26.  
  27. Kolejne metody resultPaper, resultRock, etc. możemy spróbować znacznie uprościć, zmieniając klasę Option. zróbmy to w następujący sposób - miejmy dwie, a nie jedną zmienną przechowującą ścieżkę obrazka. Jedną nazwiemy winPicturePath, drugą losePicturePath (to file w sumie trochę zbędne). Dorzućmy je do konstruktora:
  28.  
  29. public Option(string name, string winPicturePath, string losePicturePath)
  30.  
  31. ID na moment się pozbywam, podejrzewam, że nie będziemy go potrzebowali.
  32.  
  33. Zróbmy jeszcze jeden improvement - niech klasa Option przechowuje już właściwy obrazek typu Image, a nie tylko ścieżkę do pliku. Image znajduje się w System.Drawing, więc trzeba dopisać usinga na początku pliku. Teraz mamy:
  34.  
  35. public Option(string name, string winPicturePath, string losePicturePath)
  36. {
  37.     this.name = name;
  38.     winPicture  = Image.FromFile(winPicturePath);
  39.     losePicture = Image.FromFile(losePicturePath);
  40. }
  41.  
  42. Konstruktor kopiujący to teraz trochę inna sprawa - na obiektach Image trzeba użyć metody Clone, żeby stworzyć ich kopię.
  43.  
  44. public Option(Option optionToCopy)
  45. {
  46.     this.name = optionToCopy.name;
  47.     this.winPicture  = (Image)optionToCopy.winPicture.Clone();
  48.     this.losePicture = (Image)optionToCopy.losePicture.Clone();
  49. }
  50.  
  51. W każdym razie nie będziemy go prawdopodobnie potrzebować.
  52.  
  53. Generalnie dążę do tego, żeby odciążyć klasę Program od wykonywania jakichkolwiek zadań. Lepiej przenieść wszystko do Form1, dlatego stworzenie obiektów klasy Option przenieśmy do Form1.
  54.  
  55. public static Option paper = new Option("paper", "paperwin.png", "paperlose.png");
  56. public static Option rock = new Option("rock", "rockwin.png", "rocklose.png");
  57. public static Option scissors = new Option("scissors", "sci
  58.  
  59. Wpisywanie tego jest trochę uciążliwe. Trzymamy się stałego szablonu, wystarczy dodać win albo lose, żeby dostać odpowiedni obrazek. Zróbmy metodę, która po prostu załaduje dla nas obiekt.
  60.  
  61. private Option LoadOption(string name)
  62. {
  63.    return new Option(name, name + "win.png", name + "lose.png");
  64. }
  65.  
  66. Nie jest to metoda statyczna, więc załadujmy te obiekty w konstruktorze. Również tych zmiennych wewnątrz klasy nie musimy oznaczać jako statyczne, bo Form1 nie jest statyczne i operujemy na konkretnej instancji tej klasy zatem:
  67.  
  68. public Option paper;
  69. public Option rock;
  70. public Option scissors;
  71. public Option lizard;
  72. public Option spock;
  73.  
  74. A w konstruktorze:
  75.  
  76. public Form1()
  77. {
  78.    InitializeComponent();
  79.  
  80.    paper    = LoadOption("paper");
  81.    rock     = LoadOption("rock");
  82.    scissors = LoadOption("scissors");
  83.    lizard   = LoadOption("lizard");
  84.    spock    = LoadOption("spock");
  85. }
  86.  
  87. Huh, dobra, ale naszym celem było uproszczenie metod z rodziny "resultX". To nie będzie takie proste z racji, że do zmiennych Form1.IserChoice i Form1.ComputerChoice przypisujemy ścieżki plików. Zróbmy to zupełnie inaczej. Stwórzmy klasę Decider, która będzie wiedziała, który wybór jest zwycięski i nas o tym poinformuje. Decider nie będzie musiał wiedzieć ani o nazwie pliku, ani o zmiennych Form1. W ten sposób rozdzielimy logikę do zupełnie odrębnej klasy.
  88.  
  89. (...)
  90.  
  91. Po niekrótkim namyśle doszedłem do wniosku, że możemy to zrobić na inny sposób. każdy obiekt klasy Option będzie wiedział, jaki inny obiekt tej klasy może pokonać. Zewnętrzny Decider nie jest zły, może nawet lepszy, bo mamy informację o wszystkich zwycięskich i przegranych parach zgromadzoną w jednym miejscu. Jesli będzie istniała potrzeba zmiany jakichś zasad gry, będziemy mogli zmodyfikować tylko tą klasę (patrz: Single Rensposibility Principle).
  92.  
  93. Tak więc, żeby zachować rzeczy w miarę prostymi, załóżmy pomysł pierwszy. Teraz każda opcją otrzymuję listę innych opcji, które może pokonać:
  94.  
  95. List<Option> optionsICanBeat; // w class Option
  96.  
  97. Dodatkowo zaimplementujmy dwie pomocne metody. Pierwsza z nich będzie pozwalała nam powiedzieć danej opcji, jakie inne opcje może pokonać:
  98.  
  99. public void TellMeWhoICanBeat(params Option[] options)
  100. {
  101.    foreach (var option in options)
  102.    {
  103.        optionsICanBeat.Add(option);
  104.    }
  105. }
  106.  
  107. (Mały tip: List to w istocie tablica, która sama się rozszerza. Ponieważ mamy maksymalnie 5 opcji, a każda z nich może pokonać 2 inne, zróbmy, żeby nasza lista miała na początku rozmiar 2. W konstruktorze dodamy kawałek:
  108.  
  109. optionsICanBeat = new List<Option>(2);
  110.  
  111. Oczywiście, tak czy siak musimy utworzyć obiekt listy, inaczej zostanie tam null).
  112.  
  113. Druga metoda będzie informowała, czy dana opcja jest w stanie pokonać inną:
  114.  
  115. public bool CanIBeat(Option option)
  116. {
  117.    return optionsICanBeat.Contains(option);
  118. }
  119.  
  120. Simple. Teraz trzeba ustalić, kto komu może skopać tyłek. Zrobimy to w konstruktorze Form1:
  121.  
  122. public Form1()
  123. {
  124.     (...)
  125.    
  126.    InitializeRules();
  127. }
  128.  
  129. private void InitializeRules()
  130. {
  131.    paper.TellMeWhoICanBeat(rock, spock);
  132.    rock.TellMeWhoICanBeat(scissors, lizard);
  133.    scissors.TellMeWhoICanBeat(paper, lizard);
  134.    lizard.TellMeWhoICanBeat(paper, spock);
  135.    spock.TellMeWhoICanBeat(scissors, spock);
  136. }
  137.  
  138. Fajnie, teraz określenie tego, kto jest zwycięzcą sprowadza się do czegoś pokroju rock.CanIBeat(paper).
  139.  
  140. Pozostaje problem - wraz z określonymi parami wiążą się określone komunikaty ("because spock vaporizes rock") i tak dalej. Będziemy musieli to uwzględnić przy "mówieniu" obiektowi, kogo może pokonać. Zrobimy to prosto - powiemy mu także, dlaczego!
  141.  
  142. Zacznijmy od zmiany sposobu przechowywania opcji, które możemy pokonywać. Stwórzmy następującą strukturę:
  143.  
  144. public class Option
  145. {
  146.    struct BeatableOption
  147.    {
  148.        public BeatableOption(Option option, string why)
  149.        {
  150.            this.Option = option;
  151.            this.Why = why;
  152.        }
  153.  
  154.        public Option Option;
  155.        public string Why;
  156.    }
  157.    
  158.     (...)
  159. }
  160.  
  161. Dalej, zmodyfikujmy nasz kontener:
  162.  
  163. List<BeatableOption> optionsICanBeat;
  164.  
  165. I zmienimy naszą metodę:
  166.  
  167. public void TellMeWhoICanBeat(Option option, string why)
  168. {
  169.    optionsICanBeat.Add(new BeatableOption(option, why));
  170. }
  171.  
  172. (Nie możemy teraz skorzystać z dobrodziejstwa paramsa, bo potrzebujemy drugiego argument - stringa why. Oczywiście, moglibyśmy zrobić params BeatableOption[] options, ale ustalmy, że BeatableOption jest prywatną strukturą - nikt z zewnątrz nie musi widzieć, w jaki sposób nasz obiekt wie, kogo może pokonać).
  173.  
  174. W związku z tym, zmieńmy naszą metodę InitializeRules:
  175.  
  176. private void InitializeRules()
  177. {
  178.    paper.TellMeWhoICanBeat(rock, "because paper covers rock");
  179.    paper.TellMeWhoICanBeat(spock, "because paper disproves spock");
  180.    rock.TellMeWhoICanBeat(scissors, "because rock crushes scissors");
  181.    rock.TellMeWhoICanBeat(lizard, "beacuse rock crushes lizard");
  182.    scissors.TellMeWhoICanBeat(paper, "because scissors cut paper");
  183.    scissors.TellMeWhoICanBeat(lizard, "because scissors decapitates lizard");
  184.    lizard.TellMeWhoICanBeat(paper, "because lizard eats paper");
  185.    lizard.TellMeWhoICanBeat(spock, "because lizard poisons spock");
  186.    spock.TellMeWhoICanBeat(rock, "because spock vaporizes rock");
  187.    spock.TellMeWhoICanBeat(scissors, "because spock smashes scissors");
  188. }
  189.  
  190. Zajebiście. Pójdźmy o krok dalej. Zamiast metody WhoCanIBeat wywoływanej na obiekcie stwórzmy metodę statyczną Option.WhoWins. Chcielibyśmy zwracać dwie wartości - kto wygrał i dlaczego. Możemy zwrócić tylko jedną, dlatego będziemy musieli to trochę oszukać:
  191.  
  192. public static Option WhoWins(Option option1, Option option2, ref string why)
  193. {
  194.    int numOfBeatableOptionsFor1 = option1.optionsICanBeat.Count;
  195.    int numOfBeatableOptionsFor2 = option2.optionsICanBeat.Count;
  196.  
  197.    int maxIterValue = Math.Max(numOfBeatableOptionsFor1,
  198.                                numOfBeatableOptionsFor2);
  199.  
  200.    for (int i = 0; i < maxIterValue; i++)
  201.    {
  202.        if (i < numOfBeatableOptionsFor1 && option1.optionsICanBeat[i].Option == option2)
  203.        {
  204.            // Option 1 beats option 2
  205.            why = option1.optionsICanBeat[i].Why;
  206.            return option1;
  207.        }
  208.  
  209.        if (i < numOfBeatableOptionsFor2 && option2.optionsICanBeat[i].Option == option1)
  210.        {
  211.            // Option 2 beats option 1
  212.            why = option2.optionsICanBeat[i].Why;
  213.            return option2;
  214.        }
  215.    }
  216.  
  217.    // No one wins
  218.    why = "";
  219.    return null;
  220. }
  221.  
  222. No dobra, to wygląda ciekawiej. W istocie ta metoda jest bardziej ogólna niż powinna być dla naszej małej aplikacji, ale zobaczmy. Na samym początku określamy, która lista posiada więcej elementów po to, aby móc je przeiterować w jednej pętli - samą pętle wykonujemy zatem tyle razy, ile jest potrzebne dla dłuższej listy. W pętli sprawdzamy, czy i-ta opcja w listach dla opcji 1 i 2 jest opcją, którą mogą one opokonać. Jeśli tak, to znaczy, że znaleźliśmy zwycięzcę, którego możemy zwrócić wraz z podanym powodem.
  223.  
  224. Istotne są tu dwie techniczne rzeczy. Pierwsze, każdy if posiada dodatkowy warunek sprawdzajacy, czy i jest mniejsze od długości danej listy, inaczej moglibyśmy wyjść poza zakres, zwłaszcza że pętla wykonuje się tyle razy, ile trwa najdłuższa lista, zapominając o krótszej. Druga sprawa - powód w zmiennej string (why) przekazujemy przez referencję. Dzięki temu możemy przed wywołaniem stworzyć zmienną:
  225.  
  226. string why;
  227.  
  228. a następnie przekazać ją do metody i uzyskać wynik. To prawie tak jakbyśmy zwracali dwie wartości!
  229.  
  230. A więc metoda WhoWins działa niemal jak metody resultX, więc możemy się ich pozbyć. Kluczowe jest tu jednak słowo "niemal". resultPaper nie tylko odpowiadało za określenie, kto wygrał, ale także ustawienie odpowiedniej grafiki, a w zasadzie podanie ścieżki do tej grafiki. Jedna zmiana pociągnęła wiele innych. Spróbujmy zatem wrócić do korzeni i zmieńmy nieco kod zajmujący się przyciskami:
  231.  
  232. private void PaperButton_Click(object sender, EventArgs e)
  233. {
  234.    ProcessMove(paper, 3);
  235. }
  236.  
  237. Wykonamy to analogicznie dla wszystkich przycisków. Metoda ProcessMove przyjmuje dwa argumenty - opcję wybraną przez użytkownika i liczbę dostępnych opcji (3 albo 5). Jej zadaniem jest wylosowanie ruchu komputera i przedstawienie wyniku na ekran.
  238.  
  239. Zaraz, za losowanie ruchu komputera odpowiadała metoda ComputerMove w klasie Program. Ponieważ wszystko odbywa się w klasie Form1 przenieśmy ją i tam. Użyjmy jej teraz, aby otrzymać rdzeń naszej aplikacji, który przetwarza ruch gracza:
  240.  
  241. private void ProcessMove(Option userOption, int numOfOptions)
  242. {
  243.    Option computerOption = ComputerMove(numOfOptions);
  244.  
  245.    if (userOption == computerOption)
  246.    {
  247.        Result1.Text = "DRAW";
  248.        Info1.Text = "";
  249.  
  250.        YouPicture1.Image = userOption.WinPicture;
  251.        ComputerPicture1.Image = computerOption.LosePicture;
  252.        return;
  253.    }
  254.  
  255.    string why = "";
  256.    Option winner = Option.WhoWins(userOption, computerOption, ref why);
  257.  
  258.    if (winner == userOption)
  259.    {
  260.        Result1.Text = "You won";
  261.        Info1.Text = why;
  262.  
  263.        YouPicture1.Image = userOption.WinPicture;
  264.        ComputerPicture1.Image = computerOption.LosePicture;
  265.    }
  266.    else
  267.    {
  268.        Result1.Text = "You lost";
  269.        Info1.Text = why;
  270.  
  271.        YouPicture1.Image = userOption.LosePicture;
  272.        ComputerPicture1.Image = computerOption.WinPicture;
  273.    }
  274. }
  275.  
  276. Myślę, że dobrnęliśmy do końca. Powyższą metodę można jeszcze zesplitowac na parę innych ale sama w sobie jest, myślę, całkiem czytelna. Co zatem dostaliśmy?
  277.  
  278. - nieco czytelniejszy kod - wywołania takie jak:
  279. paper = LoadOption("paper");
  280. paper.TellMeWhoICanBeat(rock, "because paper covers rock");
  281. Option.WhoWins(userOption, computerOption, ref why);
  282. wydają mi się bardziej czytelniejsze i zrozumiałe.
  283. - ważne - porozdzielaliśmy odpowiedzialności na poszczególne klasy/metody. Wcześniej metody w większości miały kilka zadań, przez co miałem miałem trudności z dotarciem, co zmienia co. Teraz wydaje mi się to jaśniejsze.
  284. - jeszcze ważniejsze - uniknęliśmy nadmiarowego i powtarzającego się kodu. Potężne switche zredukowaliśmy do jednej metody (WhoWins).
  285.  
  286. No, to tyle. Przepraszam, jeśli przekurwilem, powiedz mi na przyszłość, bo wiem, że jestem świrem, ale czasem zapominam, że inni nie. :P
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement