soLid…ne programowanie!

Po dłuższej chwili wracam z kolejnym wpisem dotyczącym SOLID. Tym razem dobierzemy się do zasady „L” – Liskov substitution.

Po co to wszystko??

Zasada „L” z SOLID odnosi się do jednego z wyróżników programowania obiektowego, jakim jest dziedziczenie. Zasada ta w bardzo ogólnym brzmieniu stwierdza „Każda klasa nadrzędna może być zastąpiona klasą po niej dziedziczącą (podklasą). Podstawienie nie powinno powodować, żadnych efektów ubocznych w działaniu programu.”
Brzmi dość zawile, jednak pisząc programy, na pewno mniej lub bardziej świadomie, natknęliście się na użycie tej zasady.

Przykład: tło do soLid

Najlepiej będzie zobrazować zasadę “L” w przykładzie. Załóżmy, że piszemy program obsługujący różne konta bankowe użytkownika. Stworzyliśmy klasę Account, która posiada podstawowe dane użytkownika.

public class Account {
   private UUID id ;

   private double balance;

   private List<User> owners;

// konstruktor
//gettery

    public double resolveAccountTransaction(Transaction transaction) {
       return this.balance - transaction.getAmount() + defaultSavingRule(transaction);
}

    private double defaultSavingRule(Transaction transaction) {
       return 0.1;
    }

}

Klasa posiada metodę odnosząca się do każdego konta resolveAccountTransaction, która zmienia wartość balansu po przeprowadzonej transakcji, oraz implementuje mechanizm oszczędzania (w tym przypadku oddaje 0.1 od każdej transakcji), przy użyciu prywatnej metody defaultSavingRule.

Załóżmy, że użytkownik może wybrać pomiędzy dwoma różnymi rodzajami konta, Basic i Premium (poniżej implementacja):

public class BasicAccount extends Account {

//konstruktory

   @Override
   public double resolveAccountTransaction(Transaction transaction) {
       return super.resolveAccountTransaction(transaction) + basicSavingsRule(transaction);
   }

   private double basicSavingsRule(Transaction transaction) {
       double saving = 0;
       if (transaction.getAmount() > 1000) {
           saving = 1;
       }
       return saving;
   }
}

oraz

public class PremiumAccount extends Account {

  //konstruktory

   @Override
   public double resolveAccountTransaction(Transaction transaction) {
       return super.resolveAccountTransaction(transaction) + premiumSavingsRule(transaction);
   }

   private double premiumSavingsRule(Transaction transaction) {
       double saving = 0;
       if (transaction.getAmount() > 1000) {
           saving = 5;
       }
       return saving;
   }

Jak widać obydwa Basic i Premium dziedziczą po klasie Account i nadpisują, przy użyciu adnotacji @OVerride implementacje metody resolveAccountTransaction. Konto typu Basic, przy kwocie transakcji większej niż  1000 oddaje 1. Konto Premium przy takiej samej wielkości transakcji oddaje 5 jednostek płatniczych.

Dodatkowe klasy pomocnicze

W systemie istnieje serwis FinancialOperations pozwalający na zmianę stanu systemu, po wykonaniu transakcji.

public class FinancialOperations {

   private Transaction transaction;

   //konstruktor

   public Account executeFinancialOperation(Account account) {
       return new Account(account.getId(), account.resolveAccountTransaction(transaction), account.getOwners());
   }
}

Posiadamy tez bardzo proste klasy Transaction oraz User. Niosą one odpowiednio nazwe użytkownika oraz wielkość transferu.

Meritum soLid

Napiszmy podstawowe komendy uruchamiające program i sprawdźmy działanie zasady „L” z grupy SOLID w praktyce:

public class Main {

   public static void main(String[] args) {
       User user = new User("Jan", "Testowy");
       Account account = new BasicAccount(20000, List.of(user));
       System.out.println(account.getBalance());
       account.getOwners().forEach(usr -> System.out.println(usr.getFirstName()));

       Transaction transaction = new Transaction(1100);
       FinancialOperations financialOperations = new FinancialOperations(account, transaction);

       account = financialOperations.executeFinancialOperation(account);
       System.out.println(account.getBalance());

       account = new PremiumAccount(account.getId(), account.getBalance(), account.getOwners());
       account = financialOperations.executeFinancialOperation(account);
       System.out.println(account.getBalance());
   }
}

W pierwszym bloku tworzymy obiekty niezbędne do działania systemu, User, Account (typu Basic). W kolejnym bloku Tworzymy transakcję i uruchamiamy serwis obsługi zmiany stanu konta. Konto zostało zainicjalizowane z 2000 jednostek płatniczych. 

Po wykonaniu operacji balans wynosi 18901.1. Z konta wypłynęło 1100 jp. Obsługa oszczędzania dodała za standardowe oszczędności od transakcji 0.1 jp. oraz z racji konta Basic (kwota większa niż 1000 jp.) dodano 1 jp.

Historia lubi sie zmieniać. Nasz klient zdecydowal, że jego konto zostanie przemianowane na konto Premium. I teraz :

Zauważ, że tworzenie konta wyglądało tak

 Account account = new BasicAccount(20000, List.of(user)); 

Subklasa jest typem klasy matki (zasada programowania obiektowego). Wobec tego BasicAccount jak i PremiumAccount są typu Account, po którym dziedziczą.

Wykonując podstawienie Basic -> Premium:

account =  new PremiumAccount(account.getId(), account.getBalance(), account.getOwners())

Konto jest nadal typem Account, natomiast przypisana jest teraz do niego inna podklasa, dziedzicząca po Account. Ponownie wykonanie transakcji przelewu na 1100 jp uruchamai tę samą sygnaturę metody resolveAccountTransaction lecz z oddmienną (zależną od typu konta) implementacją (oddaje 5 jp po transakcji).

 account = financialOperations.executeFinancialOperation(account);

Podsumowanie oraz „upside down”

Z punktu widzenia użytkownika, obsługa się nie zmieniła. Ponadto podstawienie innej subklasy tego samego typu i wywołanie takiej samej sygnatury metody nie powoduje negatywnych efektów pracy systemu.

I odwrotnie, jeżeli podstawienie powoduje niechciane zmiany w systemie, bądź wymaga modyfikacji kodu, jasnym jest, że złamaliśmy zasadę podstawienia Liskov (widać też dość bezpośrednią zależność z poprzednią omawianą zasadą SOLID, Open/Closed)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *