Przejd┼║ do tre┼Ťci

Wzorzec Strategii ­čÖĆ. Jak Czytelnie Pisa─ç Kod? ­čĄö

Pisanie kodu nie jest rzecz─ů trudn─ů. Pisanie kodu, kt├│ry b─Ödzie dzia┼éa─ç prawid┼éowo i b─Ödzie rozumiany przez innych jest trudniejsze. Natomiast kod, kt├│ry spe┼énia poprzednie w┼éa┼Ťciwo┼Ťci a dodatkowo jest utrzymywalny i ┼éatwo wymienialny jest ju┼╝ pewnego rodzaju sztuk─ů – sztuk─ů programowania.


Cze┼Ť─ç┬á­čÖé

Dzisiejszy artyku┼é b─Ödzie po┼Ťwi─Öcony pisaniu dobrego kodu. Zdefiniujemy czym jest dobry kod i jak go pisa─ç. W artykule, opr├│cz aspekt├│w technicznych porusz─Ö r├│wnie┼╝ kwestie mentalne – w ko┼äcu kod pisany jest przez ludzi. Nast─Öpnie inni go czytaj─ů, modyfikuj─ů lub usuwaj─ů i przepisuj─ů od nowa. I tak kr─Öci si─Ö nasz programistyczny ┼Ťwiat ­čÖé

W dzisiejszym artykule:


Czym jest dobry kod?

Aby pisa─ç dobry kod nale┼╝y zdefiniowa─ç czym on w og├│le jest. Moim zdaniem dobry kod to taki, kt├│ry po pierwsze jest ┼éatwy w zrozumieniu. Wiem, ┼╝e czasami rozwi─ůzujemy problemy, kt├│re nie s─ů trywialne i faktycznie w tych przypadkach ci─Ö┼╝ko o pisanie kodu ┼éatwego w zrozumieniu. Jednak w wi─Ökszo┼Ťci przypadk├│w nasz kod jest sekwencj─ů procedur, kt├│ra w spos├│b mniej lub bardziej sp├│jny spe┼énia wymagania biznesowe.

Drug─ů metryk─ů dobrego kodu jest mo┼╝liwo┼Ť─ç jego zmiany lub jego ca┼ékowita wymiana bez obawy popsucia innych, teoretycznie niezale┼╝nych obszar├│w. ┼üatwo to brzmi na papierze. W praktyce jest to zadanie bardzo trudne do osi─ůgni─Öcia. Dlaczego tak si─Ö dzieje? Wydaje mi si─Ö, ┼╝e cz─Ö┼Ťciowo jest to pochodna tego jak anga┼╝ujemy si─Ö w projekt. Z autopsji wiem, ┼╝e wyst─Öpuj─ů sytuacje gdy nam si─Ö po prostu nie chce my┼Ťle─ç i piszemy aby by┼éo. Czy mo┼╝emy temu zaradzi─ç? Chyba nie. Aczkolwiek z pewno┼Ťci─ů mo┼╝emy to za┼éagodzi─ç wykorzystuj─ůc wzorzec strategii.

Kolejnym problemem z metryk─ů zmiany lub ca┼ékowitej wymiany jest jej mierzalno┼Ť─ç. Ot├│z to czy dany kod jest ┼éatwy do zmiany (rozszerzenia) czy te┼╝ do ca┼ékowitej wymiany wyjdzie dopiero przy faktycznej potrzebie. Czasami na tak─ů potrzeb─Ö czekamy tydzie┼ä a czasami 6 miesi─Öcy. O ile w pierwszym przypadku zmian─ů mo┼╝e si─Ö zaj─ů─ç osoba, kt├│ra implementowa┼éa pocz─ůtkowe rozwi─ůzanie o tyle w drugim przypadku bardzo cz─Östo b─Ödzie to osoba nie zwi─ůzana z pocz─ůtkow─ů implementacj─ů.

Jak widzisz pisanie dobrego kodu jest sztuk─ů zrozumienia. Kod kt├│ry piszemy b─Ödzie czytany i zarz─ůdzany przez innych. Najwi─Ökszym wyzwaniem programisty jest aby napisa─ç kod, kt├│ry w jasny spos├│b przeka┼╝e intencje i b─Ödzie u┼╝yty we w┼éa┼Ťciwym kontek┼Ťcie. Istniej─ů zasady i konwencje, kt├│re programi┼Ťci powinni zna─ç i stosowa─ç. Pomagaj─ů one dw├│m zupe┼énie r├│┼╝nym osobom (programistom) na wzajemne zrozumienie bez wzgl─Ödu na narodowo┼Ť─ç czy przekonania. Wydaje mi si─Ö, ┼╝e must-have ka┼╝dego programisty to przeczytanie lektur wujka Boba o czystym kodzie. Wi─Ökszo┼Ť─ç programist├│w na ca┼éym ┼Ťwiecie przeczyta┼éa te lektury (przynajmniej Czysty Kod) dzi─Öki czemu w naszym programistycznym ┼Ťwiecie mamy wsp├│lne poj─Öcie czym jest czysty kod i jak go tworzy─ç.


Wzorzec Strategii a Dobry Kod

Strategia czyli jeden z wzorc├│w programowania obiektowego, kt├│rego raczej nie musz─Ö i nie chc─Ö szczeg├│┼éowo przedstawia─ç. Je┼╝eli kto┼Ť chce sobie przypomnie─ç jak ten wzorzec dzia┼éa to odsy┼éam m.in. do tego linka. W du┼╝ym skr├│cie jest to wzorzec behawioralny, kt├│ry pozwala na zdefiniowanie rodziny (zbior├│w) algorytm├│w, umieszczenie ka┼╝dego z nich w osobnej klasie i uczynienie ich obiektami wymiennymi.

Co strategia ma do dobrego kodu? Moim zdaniem kluczowe jest zdanie rodzina algorytm├│w. Mo┼╝e to by─ç algorytm do obliczania ceny produktu w zale┼╝no┼Ťci kt├│ry klient dokonuje zakupu lub obliczanie ceny pizzy w zale┼╝no┼Ťci od tego jakie sk┼éadniki zosta┼éy u┼╝yte i jaki rabat b─Ödzie udzielony. Rodzin─ů algorytm├│w r├│wnie dobrze mo┼╝e by─ç wyb├│r implementacji do obliczania drogi, jak─ů mamy do pokonania w zale┼╝no┼Ťci czy jedziemy autem, rowerem czy pokonujemy tras─Ö pieszo (przyk┼éad: google maps). Przyk┼éady u┼╝ycia mo┼╝na mno┼╝y─ç.

Rozk┼éadaj─ůc definicj─Ö rodzina algorytm├│w na ┼éopatki to oka┼╝e si─Ö, ┼╝e algorytm jest to wykonanie logiki pewnej decyzji biznesowej. A co si─Ö dzieje z decyzjami biznesowymi? Zmieniaj─ů si─Ö ­čÖé . Nie zawsze – dobrym przyk┼éadem mog─ů by─ç jakie┼Ť regulacje prawne, kt├│re od X lat s─ů niezmienne i szansa na to ┼╝e ulegn─ů zmianie jest nik┼éa. Jednak znaczna wi─Ökszo┼Ť─ç decyzji biznesowych ulega zmianom. S─ů doprecyzowywane, usuwane, dodawane, zamieniane na inne.

Dostrzegasz korelacj─Ö pomi─Ödzy wzorcem strategii a decyzjami biznesowymi? Ja tak. Wykorzystuj─ůc strategie nasze oprogramowanie jest bardziej elastyczne. Je┼╝eli mia┼ébym przytoczy─ç zasady SOLID┬áto kod, kt├│ry wytworzymy z automatu przyjmuje jedn─ů z zasad [1] a drug─ů [2] w momencie gdy pomy┼Ťlimy i nie upychamy wszystkiego do jednego worka:

  • Open-closed principle [1]
  • Single-responsibility principle [2]

Dzi─Öki temu nasz kod b─Ödzie otwarty na rozszerzenie. B─Ödziemy mogli dowolnie dodawa─ç nowe funkcjonalno┼Ťci przez dodanie nowego algorytmu i w jednym miejscu w kodzie definiowa─ç nowe rozszerzenie (wyb├│r strategii). Wyb├│r strategii mo┼╝e by─ç instrukcj─ů switch-case,┬áprostym warunkiem if lub w bardziej hardcorowych przypadkach konfiguracj─ů wyekstraktowan─ů do zewn─Ötrznego pliku. Jednak moim zdaniem ostatnia opcja nie sprawdza si─Ö w przypadku rozwi─ůza┼ä biznesowych, gdzie wiemy dok┼éadnie kto jest naszym klientem (nie jest to oprogramowanie kierowane do wszystkich).


Jak zastosowa─ç to w praktyce?

To jest chyba najtrudniejsza cz─Ö┼Ť─ç tego artyku┼éu. Nie mog─Ö da─ç Ci konkretnych wytycznych kiedy zastosowa─ç wzorzec strategii bo nie znam domeny w jakiej pracujesz. Nie wiem co jest elementem zmiennym i z czego biznes czerpie najwi─Öksz─ů warto┼Ť─ç. Co jest jeszcze trudniejsze – cz─Östo bywa tak, ┼╝e chocia┼╝ wiesz w jakiej domenie si─Ö obracasz to nie jeste┼Ť w niej ekspertem i nie masz poj─Öcia czy wymaganie, kt├│re w┼éa┼Ťnie implementujesz mo┼╝e by─ç zmieniane. I co masz zrobi─ç?

Teraz napisz─Ö kilka zda┼ä, z kt├│rymi cz─Ö┼Ť─ç mo┼╝e si─Ö nie zgodzi─ç (by─ç mo┼╝e sam za jaki┼Ť czas stwierdz─Ö, ┼╝e to jest bez sensu). Zak┼éadaj─ůc, ┼╝e nie mo┼╝esz wyci─ůgn─ů─ç wi─Öcej informacji ze strony biznesu (po prostu nie mo┼╝esz dowiedzie─ç si─Ö wi─Öcej o domenie) musisz dzia┼éa─ç na czuja i wsz─Ödzie tam gdzie czujesz, ┼╝e kontekst mo┼╝e by─ç zmienialny stostuj wzorzec strategii.

Aby lepiej to zobrazowa─ç podam Ci przyk┼éad. Za┼é├│┼╝my ┼╝e pracujesz dla sklepu, kt├│ry zajmuje si─Ö produkcj─ů i dystrybucj─ů biurek. Do tej pory sklep dzia┼éa┼é lokalnie i ca┼éa sprzeda┼╝ opiera┼éa si─Ö o kontakt telefoniczny. Teraz przechodz─ů w sprzeda┼╝ online. Twoim zadaniem jest implementacja logiki prostego systemu p┼éatno┼Ťci. Ma to by─ç dostarczone szybko (ka┼╝da stracona godzina to $). W celu uproszczenia za┼é├│┼╝my, ┼╝e ca┼éy system jest ju┼╝ zrobiony a Ty musisz zaimplementowa─ç ten jeden ma┼éy feature.

Musisz zaimplementowa─ç p┼éatno┼Ť─ç, kt├│ra mo┼╝e by─ç realizowana za pomoc─ů p┼éatno┼Ťci bankowej lub blika. Kiedy u┼╝ytkownicy dokonuj─ů p┼éatno┼Ťci przy pomocy blika, dodatkowo zyskuj─ů punkty lojalno┼Ťciowe (taka promocja). Kod do tego mo┼╝e wygl─ůda─ç nast─Öpuj─ůco:

public OrderResult makePurchase() {
  OrderResult result = purchaseDesk();

  // implementacja p┼éatno┼Ťci
  makePayment(result);

  return result;
}

private void makePayment(OrderResult result) {
  if (result.isWeb()) {
    result.setPaymentMethod(PaymentType.BANK_ACC);
    notificationSystem.sendEmailNotification();    
  } else if (result.isBlik()) {
    result.setPaymentMethod(PaymentType.BLIK);
    result.addLoyalityPoint();
    notificationSystem.sendMobileNotification();
  }  

  throw new UnsupportedOperationException("Payments other than Web and Blik are not supported");
}

Nie wchod┼║my w mantry tego czy ten kod ma sens czy nie. Chodzi o fakt, ┼╝e teraz dochodzi kolejne rozszerzenie. Sklep dostaje feedback, ┼╝e wyb├│r p┼éatno┼Ťci jest zbyt sk─ůpy i klienci chc─ů aby dodano szybkie przelewy oraz mo┼╝liwo┼Ť─ç przelewu offline (co to za klienci?? ­čśÇ ). Sterowany czasem mo┼╝esz i┼Ť─ç w kierunku dodawania kolejnych if’├│w do metody makePayment(…).

private void makePayment(OrderResult result) { 
  if (result.isWeb()) { 
  ....

  else if (result.isFastPayment()) {
    result.setPaymentMethod(PaymentType.FAST_PAYMENT);
    metricService.measureUsage();
  } else if (result.isOffline()) {
    result.setPaymentMethod(PaymentType.OFFLINE);
    result.extractLoyalityPoint();
    metricService.addDumbness();
    notificationSystem.sendSmsNotification();
    notificationSystem.informAboutLegacyPaymentMethod();
  }

  throw new UnsupportedOperationException("Payments other than Web and Blik are not supported"); 
}

B─Öd─Ö z Tob─ů szczery – nie mam z tym problemu do czasu kiedy logika metody makePayment(…) zaczyna si─Ö rozrasta─ç, tak jak w naszym wyimaginowanym przypadku. Kolejny problem to kiedy r├│┼╝ne osoby modyfikowa┼éy ten kod i teraz ┼╝adna inna (nawet te, kt├│re ten kod pisa┼éy) nie chce tego fragmentu rusza─ç bo nie do ko┼äca wie jak to dzia┼éa i boi si─Ö zepsu─ç. By┼éem w tym miejscu w kodzie produkcyjnym. Nadal si─Ö z tym spotykam i my┼Ťl─Ö, ┼╝e ka┼╝dy kto pisze software zna to uczucie. Wiesz, ┼╝e przyda┼éoby si─Ö zrefaktoryzowa─ç ten fragment kodu ale nie mo┼╝esz bo nie ma czasu i zasob├│w a ┼╝eby zrozumie─ç ca┼éo┼Ť─ç trzeba b─Ödzie wej┼Ť─ç g┼é─Öboko w logik─Ö.

Gdyby┼Ťmy zamiast tego od razu wyczuli mo┼╝liwo┼Ť─ç zmian, zaoszcz─Ödziliby┼Ťmy p├│┼║niejszych b├│l├│w. Oczywi┼Ťcie zdaj─Ö sobie spraw─Ö, ┼╝e niesie to inne problemy, typu przekazywanie zale┼╝no┼Ťci do konkretnych strategii (co┼Ť czego nie uwzgl─Ödni┼éem w przyk┼éadzie poni┼╝ej). Mo┼╝na to za┼éatwi─ç w najprostszy spos├│b, czyli wstrzykuj─ůc zale┼╝no┼Ť─ç przez konstruktor. Jednak najwa┼╝niejsze pytanie kt├│re si─Ö pojawia i na kt├│re nie umiem Ci odpowiedzie─ç – czy warto? Wed┼éug mnie w wielu przypadkach tak ale jednak nie zawsze.

public OrderResult makePurchase() {
  OrderResult result = purchaseDesk();

  PaymentStrategy strategy = choosePaymentStrategy(orderResult);
  strategy.makePayment(orderResult);

  return orderResult;
}

private PaymentStrategy choosePaymentStrategy(OrderResult result) {
  if (result.isWeb()) {
    return new WebPaymentStrategy();
  } else if (result.isBlik()) {
    return new BlikPaymentStrategy();
  }  else if (result.isFastPayment()) {
    return new FastPaymentStrategy();
  } else if ( result.isOffline()) {
    return new OfflinePaymentStrategy();
  }   
  throw new UnsupportedOperationException("");
}

// przykładowa implementacja Strategii
class WebPaymentStrategy implements PaymentStrategy {
  @Override
  public void makePayment(OrderResult orderResult) {
    result.setPaymentMethod(PaymentType.BANK_ACC); 
    notificationSystem.sendEmailNotification(); 
  }
}

Mo┼╝e si─Ö zdarzy─ç tak, ┼╝e mia┼ée┼Ť z┼ée przeczucie i kod, kt├│ry uj─ů┼ée┼Ť w strategi─Ö nie ma ┼╝adnych rozszerze┼ä. Sko┼äczy┼ée┼Ť z interfejsem i jego dwoma implementacjami kt├│re si─Ö nie zmieniaj─ů. Co┼Ť co m├│g┼éby┼Ť uj─ů─ç w jednej metodzie z dwoma if’ami i 15 liniami kodu jest hierarchi─ů klas. Pytanie – czy to jest a┼╝ tak z┼ée? W najgorszym przypadku masz 2 dodatkowe klasy, ka┼╝da zawieraj─ůca po 5 linii logiki kodu. Nie jestem zwolennikiem takiego podej┼Ťcia ale uwa┼╝am, ┼╝e b─Ödzie to lepsze ni┼╝ metoda z zagnie┼╝d┼╝aj─ůcymi si─Ö if’ami, kt├│re z czasem mog─ů doj┼Ť─ç. Czy jest z┼éoty ┼Ťrodek? I tak i nie. Zale┼╝y z kim pracujesz. Ja osobi┼Ťcie jestem zdania, ┼╝e powinni┼Ťmy zaczyna─ç jak najpro┼Ťciej – w tym przypadku od metody. A kiedy widzisz, ┼╝e to si─Ö rozrasta robisz refaktor. Nie akceptujesz wym├│wek typu: to ma by─ç na ju┼╝!


Podsumowanie

Mam nadziej─Ö, ┼╝e ten artyku┼é u┼Ťwiadomi nas troch─Ö w dzia┼éaniu. Nie piszmy kodu bez my┼Ťlenia! Patrzmy, co mo┼╝e si─Ö zdarzy─ç dalej. By─ç mo┼╝e kod, kt├│ry w┼éa┼Ťnie klepiesz za 3 miesi─ůce b─Ödzie musia┼é by─ç wyrzucony do kosza a zamiast niego o wiele szybciej powstanie nowa implementacja? Tylko pytanie – czy Tw├│j kod jest gotowy na tak szybk─ů wymian─Ö? Czy kod, kt├│ry wcze┼Ťniej naklepa┼ée┼Ť nie zawiera zawi┼éo┼Ťci i nieoczekiwanych wynik├│w?

Oczywiste jest, ┼╝e im d┼éu┼╝ej jeste┼Ť na danym projekcie, tym wi─Öksz─ů wiedz─Ö masz na temat tego co tworzysz. Mo┼╝e faktycznie b─Ödzie tak, ┼╝e uznasz i┼╝ kod, kt├│ry pisa┼ée┼Ť 2 miesi─ůce temu trzeba przepisa─ç z nowo poznan─ů wiedz─ů. Jak du┼╝y b─Ödzie koszt przepisania? Czy b─Ödzie to tylko kwestia napisania nowej logiki i jej odpowiednie wstrzykni─Öcie? A mo┼╝e modyfikowanie 10 miejsc z doz─ů niepewno┼Ťci czy system b─Ödzie dzia┼éa─ç dalej tak samo? Sam odpowiedz na to pytanie i pami─Ötaj, ┼╝e kod kt├│ry dzi┼Ť tworzysz jutro mo┼╝e by─ç zarz─ůdzany przez kogo┼Ť innego.

Na koniec pozwolę sobie utworzyć własny cytat:

Kod jest jak bumerang, kiedy┼Ť do Ciebie wr├│ci.

Bartosz D─ůbek

 

Źródła:


Za tydzień

Mamy pierwszy tydzie┼ä nowego miesi─ůca a to oznacza podsumowanie pa┼║dziernika oraz plany na listopad.

5 2 votes
Article Rating
Subscribe
Powiadom o
guest
5 komentarzy
najnowszy
najstarszy oceniany
Inline Feedbacks
View all comments
Łukasz
Łukasz
3 lat temu

Dlaczego stosujesz „else if” je┼╝eli masz w warunku poprzednim „return”?

Krzysiek
Krzysiek
3 lat temu

jaki poleci┼éby┼Ť spos├│b, ┼╝eby nie u┼╝ywa─ç IF’ow w miejscu choosePaymentStrategy ?

Patryk
Patryk
3 lat temu

Generalnie konstrukcje „if..else if .. else` s─ů ma┼éo rozszerzalne, nowa strategia b─Ödzie powodowa─ç modyfikacje tego kodu. S─ů ju┼╝ tam 4 strategie prawdopodobnie przy 3 ju┼╝ bym si─Ö zastanowi┼é czy nie zrobi─ç tego bardziej elastyczniej. Wszystko zale┼╝y od wymaga┼ä i jak cz─Östo dochodzi┼éyby by nowe strategie.

Przykładowo można by stworzyć ComposePaymentStrategy(list: List<PaymentStrategy>) gdzie PaymentStrategy mogło by samo decydować czy się wybrać na podstawie OrderResult-a

5
0
Would love your thoughts, please comment.x