Languages

  • Polski

» Liskov Substitution Principle (LSP) & Interface Segregation Principle (ISP)

W ostatnim artykule chciałbym również poruszyć dwie zasady.

Wstęp teoretyczny

LSP

Konwertując tę zasadę na bardziej programistyczną płaszczyznę brzmi ona następująco: „Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application” – Obiekty klasy nadrzędnej powinny być możliwe do podmiany przez obiekty jej klas podrzędnych nie psując tym samym aplikacji. Zatem, gdy posiadamy obiekty klasy nadrzędnej zaimplementowane gdzieś w 1000 miejscach w kodzie, powinna być możliwa ich podmiana obiektami klas podrzędnych, które dziedziczą po tej klasie nadrzędnej. Nie może to jednak psuć aplikacji. Czyli nie może być tak, że metoda klasy nadrzędnej nigdy nie wyrzucała wyjątku, a metoda klasy podrzędnej nagle to robi i trzeba modyfikować kod aplikacji w 1000 miejscach, bo nagle jest możliwość, że powstanie nieobsłużony wyjątek (jak widzicie jest to powiązane również z OCP – te zasady wzajemnie się przenikają).

ISP

„Clients should not be forced to depend upon interfaces that they do not use”. – Klienci nie powinni być zmuszani do polegania na interfejsach, których nie używają. Gdy posiadamy interfejs, który nakazuje każdej klasie Pracownik implementację metody liczącej wysokość zarobku pracownika oraz wyświetlanie numeru identyfikacyjnego, i w międzyczasie zatrudnimy na stanowisko jakiegoś wolontariusza, który pracownikiem niejako jest, to błędem byłoby nakazywanie takiej klasie implementację metody liczącej wysokość zarobku (wolontariusz pracuje za darmo). No ale interfejs sztywno robi swoje i zmusza klasę Wolontariusz do liczenia zarobku pracownika…

Trochę bardziej praktycznie…

Skupmy się w tym artykule bardziej na klasie Mage tego samego przykładu gry MMORPG. A dokładniej na zaklęciach. Mamy:

  • klasę Mage, która odpowiada za zarządzanie zaklęciami,
  • klasy „zaklęciowe”: Fireball, Slow i DragonShout,
  • interfejs Spells.
public class Mage {
    public void castSpell(Spell spell) {
        spell.cast();
    }
}

public class Fireball {
    public void cast() {
        System.out.println("FIREBALL!");
    }
}

public class Slow {
    public void cast() {
        System.out.println("SLOW!");
    }
}

public class DragonShout implements Spell {

    @Override
    public void cast() {
        System.out.println("FUS RO DAH!");
    }
}

public interface Spell {
    void cast();
}
Show/Hide

Powiedzmy, że chcemy dodać nowe zaklęcie – leczenie. Tylko że interesuje nas, żeby takie zaklęcie móc używać nie tylko na sojuszników, ale i również na siebie. Pamiętacie przykład z różnymi formami rzucania zaklęć: na cel, na dotyk i na siebie? Spróbujmy zatem dodać możliwość rzucania zaklęć na własną postać. Dodajemy nową metodę do interfejsu Spells a także do każdej z metod odpowiednią implementację. Na koniec podpinamy nowoutworzoną funkcjonalność do klasy Mage:

public interface Spell {
    void cast();
    void useOnYourself();
}

public class Fireball implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: FIREBALL!");
    }

    @Override
    public void useOnYourself() {
        System.out.println("Use on yourself: FIREBALL!");
    }
}

public class Slow implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: SLOW!");
    }

    @Override
    public void useOnYourself() {
        System.out.println("Use on yourself: SLOW!");
    }
}

public class DragonShout implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: FUS RO DAH!");
    }

    @Override
    public void useOnYourself() {
        System.out.println("Use on yourself: FUS RO DAH!");
    }
}

public class Mage {

    public void castSpell(Spell spell) {
        spell.cast();
    }

    public void useSpellOnYourself(Spell spell) {
        spell.useOnYourself();
    }
}
Show/Hide

Dorzucamy jeszcze nową klasę Heal:

public class Heal implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: HEAL!");
    }

    @Override
    public void useOnYourself() {
        System.out.println("Use on yourself: HEAL!");
    }
}
Show/Hide

Wszystko dobrze tylko…dlaczego dajemy użytkownikowi możliwość podpalenia samego siebie, spowolnienia, czy odepchnięcia smoczym krzykiem? Nie no, jeśli chcemy mieć takie utrudnienie to czemu nie, ale załóżmy, że wolimy, żeby użytkownik zaklęć nie mógł nimi zrobić sobie krzywdy. 🙂 W takim razie powstaje problem: Co zrobić z metodami useOnYourself() w klasach zaklęć wyraźnie ofensywnych? Możemy:

  • nie zrobić nic (nie polecam),
  • wypisać informację, że nie ma takiej metody (to taki ćwierćśrodek),
  • wyrzucić wyjątek (a to półśrodek, ale na razie się nada).

Rzucajmy zatem wyjątkami!

public class Fireball implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: FIREBALL!");
    }

    @Override
    public void useOnYourself() throws NoSuchMethodException {
        throw new NoSuchMethodException();
    }
}

public class Slow implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: SLOW!");
    }

    @Override
    public void useOnYourself() throws NoSuchMethodException {
        throw new NoSuchMethodException();
    }
}

public class DragonShout implements Spell {

    @Override
    public void cast() {
        System.out.println("Cast: FUS RO DAH!");
    }

    @Override
    public void useOnYourself() throws NoSuchMethodException {
        throw new NoSuchMethodException();
    }
}
Show/Hide

I choć nie użyjemy już na swojej postaci zaklęcia ofensywnego, to niestety takie rozwiązanie zmusi nas do obsługiwania tych wyjątków w klasie nadrzędnej Mage albo w tym przypadku w Main.

public class Main {
    public static void main(String[] args) {
        Mage mage = new Mage();

        mage.castSpell(new Fireball());
        mage.castSpell(new Slow());
        mage.castSpell(new DragonShout());
        mage.castSpell(new Heal());

        try {
            mage.useSpellOnYourself(new Heal());
        } catch (NoSuchMethodException e) {
            System.out.println("No such method implemented!");
        }

        //mage.useSpellOnYourself(new DragonShout());
    }
}
Show/Hide

Nie byłoby to komfortowe, gdybyśmy musieli zmieniać teraz 5000 linijek w kodzie, jeżeli nasza aplikacja byłaby trochę bardziej rozbudowana, nie? Spróbujmy w takim razie trochę bardziej finezyjnego rozwiązania.

Naprawiamy kod…

Najpierw rozdzielmy nasz interfejs na dwa i zobaczmy co się stanie…

public interface CastSpells {
    void cast();
}

public interface UseOnYourselfSpells {
    void useOnYourself();
}
Show/Hide

A teraz zaimplementujmy odpowiednie interfejsy do klas „zaklęciowych” tak jak tego potrzebujemy:

public class Fireball implements CastSpells {

    @Override
    public void cast() {
        System.out.println("Cast: FIREBALL!");
    }
}

public class Slow implements CastSpells {

    @Override
    public void cast() {
        System.out.println("Cast: SLOW!");
    }
}

public class DragonShout implements CastSpells {

    @Override
    public void cast() {
        System.out.println("Cast: FUS RO DAH!");
    }
}

public class Heal implements CastSpells, UseOnYourselfSpells {

    @Override
    public void cast() {
        System.out.println("Cast: HEAL!");
    }

    @Override
    public void useOnYourself() {
        System.out.println("Use on yourself: HEAL!");
    }
}
Show/Hide

W ten sposób ani nie spowodujemy rozjechania się aplikacji użyciem wyjątku w klasie podrzędnej w stosunku do nadrzędnej klasy interfejsowej Spell, bo podzieliliśmy ją na dwa interfejsy, ani też nie zmuszamy naszego klienta (Mage) do polegania na interfejsach których dane klasy „zaklęciowe” nie używają. LSP oraz ISP są zachowane.

Linki do repozytorium na GitHubie:

Dzięki wielkie za Twoją uwagę! To już koniec serii artykułów o zasadach ukrytych pod akronimem SOLID. Dobrego dnia!

You become responsible, forever, for what you have tamed.

-- Little Prince --