niedziela, 30 czerwca 2013

Jak rodzi się złożoność kodu - księga trzecia

Kiedy rozpoczynaliśmy pracę nad obecnym projektem mieliśmy już trochę doświadczeń i przemyśleń odnośnie tego co czyni kod niezwykle nieczytelnym i trudnym w rozwoju. Wśród głównych przyczyn leżały ogromne klasy opakowujące kod proceduralny, długie metody czy masa zagnieżdżonych warunków - a wszystko przypieczętowane zmiennymi o słodko brzmiących nazwach temp czy zzz.

Na tym polu odnieśliśmy zauważalny sukces gdyż nasza największa klasa ma chyba 400 linii, żadna metoda nie przekracza 30 linii a 90% kodu nie ma więcej niż jednego poziomu zagnieżdżenia. Klęskę ponieśliśmy za to na innym froncie, którego istnienia kilka lat tamu nawet się nie domyślałem. Otóż kiedy tworzyliśmy nasze małe czytelne klasy zawsze musieliśmy je gdzieś umieścić. DAO szło zazwyczaj do paczki costam.dao kontrolery do paczki costam.controlers itd.

Istnieje specjalna metryka w sonarze, która bada tego typu spierdoliny : I chociaż nie mam jeszcze w pełni intuicyjnego wyczucia tej metryki to wskazane wartości pokazują, że może być lepiej.

A co da nam to, że paczki nie będą tak poprzeplatane? Aby to zrozumieć złamiemy (ale tylko pozornie) jedną z zasad dobrego kodu obiektowego

Jak OOP tworzy Klasę Blob/Good

Klasa Money będzie dobrym przykładem bo w dzisiejszych czasach każdy potrzebuje pieniędzy. Na początek zobaczmy co się stanie kiedy przedstawimy pieniądze jako zwykłą strukturę danych:

 

public class Money {

    private final BigDecimal amount;
    
    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public Currency getCurrency() {
        return currency;
    }
}

Niestety powyższa forma jest zaproszeniem do tworzenia klas typu Utils czy Helper. A pod tym linkiem ----> http://nemo.sonarsource.org/ możecie zobaczyć, że klasy utils to jedne z bardziej złożonych tworów.

No to spróbujmy, żeby było bardziej obiektowo :

 

public class Money {
    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    public Money add(Money moneyToAdd){
        checkTheSameCurrency(moneyToAdd);
        return new Money(moneyToAdd.amount.add(this.amount),currency);
    }
    
    public boolean isGreaterThan(Money moneyToCompare){
        checkTheSameCurrency(moneyToCompare);
        return this.amount.doubleValue()> moneyToCompare.amount.doubleValue();
    }

    private void checkTheSameCurrency(Money moneyToAdd) {
        if(moneyToAdd.currency!=this.currency){
            throw new RuntimeException("zle");
        }
    }
}

Wydaje się być ok. Jest enkapsulacja, nie zdradzamy wewnętrznej implementacji, jest "tell don't ask" i inne takie dobre praktyki.

A co gdy domena się rozwija i muszę obliczyć podatek? "Tell don't ask" ?

 

public class Money {

    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    //!!!!!!
    public Tax calculateTax(double percentage){
        return new Tax(amount.multiply(BigDecimal.valueOf(percentage)));
    }

No i pytanie co tak naprawdę symbolizuje klasa Money? Jeśli reprezentuje pieniądz to polityka podatkowa średnio tam pasuje. Oczywiście można powiedzieć - "no to dodajmy tam operacje mnożenia czy też wyciągania procentu". Tak a za chwilę dodamy operacji całki i obliczania pola. A co jeśli będę chciał przeliczać waluty - czy w ramach enkapsulacji dodać funkcjonalność kantoru do klasy Money albo Currency?

Jeśli mówić o smrodzie w kodzie to powyższe opcje dla mnie niezwykle śmierdzą. A co dyby tak wykorzystać modyfikator dostępu, którego nie ma?

 

public class Money {

    private final BigDecimal amount;

    private final Currency currency;

    public Money(BigDecimal bigDecimal, Currency currency) {
        this.amount = bigDecimal;
        this.currency = currency;
    }
    
    BigDecimal getAmount() {
        return amount;
    }

...
}
public class TaxCalculator {

    private static final double standardTaxPercentage=0.23;
    
    public Tax calculateTax(Money money){
        return new  Tax(money.getAmount().multiply(BigDecimal.valueOf(standardTaxPercentage)));

    }
}
Dla wielu osób wynik tego niezwykle wciągającego eksperymentu myślowego może być czymś oczywistym ale ja na swojej drodze napotykam wiele osób, które potrzebują takiej wiedzy (w tym mnie). Ładujemy Money i TaxKalkulator do jednej paczki i mamy enkapsulację pieniędzy w ramach tej paczki. Powinno nam to pomoc w uniknięciu problemu klasy BLOBa ale co jeśli owa paczka niezwykle nam się rozrośnie? Tutaj trudno mi określić granicę kiedy "jest już za dużo".

Bonus - skąd się bierze fundamentalizm obiektowy

Znam pewnych ludzi, którzy kłócili by się, że powyższy przykład można rozwiązać jakimś wizytatorem czy innymi cudami. Problem polega na tym, że czasem mogą mieć rację a czasem nie ale sam fakt uznawania zasady rodem z wikipedii typu "tell don't ask" czy "DRY" za święte i nienaruszalne doprowadza mnie do szalonego płaczu. A w ogólności dobija mnie traktowanie OOP jako panaceum na wszystko - czyli zarzuty typu "ten kod jest zły bo nie obiektowy".

Jeśli ktoś ma niech zerknie sobie do rozdziału nr 6 "Czystego kodu" pod tytułem "struktury danych". Są tam dwa przykłady obliczania pól figur - jeden obiektowy a drugi proceduralny. Nie chce mi się kopiować tutaj źródeł ale wniosek postawiony przez autora jest jasny: wersja bardziej proceduralna z ifami jest lepsza jeśli dojdą nam nowe funkcje (jedno miejsce do zmiany vs wszystkie klasy figur do zmiany) - ale wersja z polimorfizmem jest lepsza jeśli spodziewamy się nowych kształtów - wniosek: obiektowy nie znaczy najlepszy.

Bo nawet czy jest sens toczyć spory odnośnie tego czy kod z ogromną klasą ale idealna enkapsulacją jest bardziej obiektowy niż kod z małymi klasami, które jednak nie starają się robić wszystkiego jednocześnie udostępniając gdzieniegdzie szczegóły implementacyjne?

A może jednak przychodzi czas aby nie ograniczać się do racjonalizacji swoich wyborów prostymi zasadami z wikipedii ale zgłębić dokładniej jakie te wybory niosą konsekwencje? Są różne modele rozwoju zdolności : Shu Ha Ri , SU HA RY , SŁU CHAJ STARY - opisujące jak to od prostych reguł należy przechodzić do zrozumienia zasad poprzez magiczną transcendencję. Na samym początku jest bagno, totalnie zjebany kod i tutaj jest miejsce na stosowanie zasad "Tell don't ask" czy "DRY" bez myślenia bo cokolwiek się nie zrobi to będzie lepiej. Ale jeśli zostanie się na tym etapie to jesteśmy na prostej ścieżce do Obiektowego Fundamentalizmu

O a teraz złamię jeszcze jedną zasadę - święte DRY

 

class PracownikUczelni{
private String tytuł;
}

class Film{
private String tytuł;
}

No i DRY jest złamane. Można oczywiście zrobić też tak:
 

class abstract PosiadaczTytułu{
private String tytuł;
} 

class PracownikUczelni extends PosiadaczTytułu{}

class Film extends PosiadaczTytułu{}

No i DRY jest spełnione a przy okazji mamy zjebaną hierarchię klas. NA początek zasady z wikipedii są dobre ale później trzeba zacząć używać mózgu - nie ma wyjścia. A jeśli ktoś potrzebuje potwierdzenia od jakiejś gwiazdy światowego kalibru to Dan North w jednej ze swoich prezentacji napisał "DRY is an enemy of decoupled" (A jeśli dla kogoś Dan North nie jest gwiazdą światowego kalibru albo zastanawia się "kto to k***a jest?" to niech delektuje się moimi wnioskami)

1 komentarz:

  1. z tym drajem i kaplingiem to się niestety trzeba zgodzić... jakiś czas temu widziałem dość ciekawy artykuł z, jak mi się wtedy zdawało, paroma fajnymi pomysłami. http://java.dzone.com/articles/useful-abuse DRY jest tutaj doprowadzony aż do absurdu (w paru przykładach), którego autor zupełnie nie dostrzega.

    OdpowiedzUsuń