Zasady SOLID #2 – Open-Closed Principle

padlock
Stworzenie działającej aplikacji nie oznacza zakończenia prac nad projektem. Czasem wpadamy na nowe pomysły jak ulepszyć aplikację czy też dodać do niej dodatkowe funkcjonalności. Jednak zdarza się, iż po dodaniu czegoś nowego, przestaje nam działać coś innego. Dzieje się tak, gdyż dodając nowe funkcjonalności jesteśmy zmuszeni modyfikować dotychczas napisany kod. W tym artykule przedstawię zasadę, która pomoże rozwiązać ten problem.

Wprowadzenie

Druga z zasad SOLID to Zasada Otwarte-Zamknięte (ang. Open-Closed Principle). Zgodnie z jej treścią, oprogramowanie powinno być otwarte na rozszerzenia, lecz zamknięte na modyfikacje. Stosując zasadę OCP, oprogramowanie powinno być pisane w taki sposób, aby możliwe było dodawanie nowych funkcjonalności bez modyfikacji istniejącego już kodu. Jeżeli dana metoda jest wykorzystywana w kilku miejscach w aplikacji, to jej modyfikacja mogłaby spowodować, że któryś z modułów przestanie działać. Dużo lepszym rozwiązaniem byłoby dodanie np. nowej metody, klasy lub interfejsu.

Objaśnienie zasady

Przykładem urządzenia otwartego na rozszerzenia i zamkniętego na modyfikacje może być robot kuchenny. Mogę dowolnie rozszerzać jego możliwości dzięki wymiennym akcesoriom. Potrzebujemy ubić pianę z białek? Instalujemy końcówkę z trzepaczką. Musimy zmielić mięso? Nic trudnego! Montujemy moduł z maszynką do mięsa. Możliwości jest wiele, a wszystko to możemy dokonać bez konieczności ingerencji w wewnętrzne podzespoły robota.
Natomiast przykładem urządzenia, którego pewien aspekt jest niezgodny z Zasadą Otwarte-Zamknięte jest… smartfon. Współczesne smartfony to niesamowite urządzenia o możliwościach niedostępnych dla klasycznych telefonów komórkowych, z których korzystaliśmy w przeszłości. Niestety jest pewien element, który był rozwiązany lepiej w telefonach poprzedniej generacji. Jest nim bateria.
Gdy klasycznym komórkom psuła się bateria, po prostu otwieraliśmy klapkę z tyłu, wyciągaliśmy baterię i wkładaliśmy nową. Współczesne smartfony mają zwartą konstrukcję, dostęp do baterii jest możliwy przy wykorzystaniu specjalistycznych narzędzi, a samą wymianę dokonuje najczęściej autoryzowany serwis producenta urządzenia. Wymiana takiej baterii wymaga ingerencji w wewnętrzną strukturę smartfona. Przekładając to na świat programistyczny, taka wymiana jest niezgodna z Zasadą Otwarte-Zamknięte.

Przykład

Tym razem naszym zadaniem będzie napisać oprogramowanie do pralki. Pralka ma mieć 3 programy: do ubrań bawełnianych, wełnianych oraz syntetycznych. Uruchamianie pralki ma być realizowane poprzez wywołanie jednej metody. Musimy więc zaimplementować pewną metodę, która będzie uruchamiana w różny sposób w zależności od wybranego programu.

Kod niezgodny z OCP

Nasze oprogramowanie mogłoby wyglądać tak:
public class WashingMachine {

    public void wash(Program program) {
        switch (program) {
            case COTTON:
                System.out.println("Wash with cotton clothes program");
                break;
            case WOOL:
                System.out.println("Wash with wool clothes program");
                break;
            case SYNTHETIC:
                System.out.println("Wash with synthetic clothes program");
                break;
        }
    }
}

public enum Program {

    COTTON,
    WOOL,
    SYNTHETIC
} 
Pierwsze co przychodzi do głowy to instrukcja switch (lub instrukcja warunkowa), która uruchamia odpowiedni program w zależności od wartości przesłanej do metody jako argument (w naszym przypadku jest to typ wyliczeniowy Program).
Na pierwszy rzut oka wszystko zostało zaimplementowane poprawnie. Jednak problemy zaczynają się, gdy musimy rozszerzyć działanie naszego oprogramowania. Dostaliśmy zadanie dodania dodatkowych trzech programów: do ubrań czarnych, białych i kolorowych. Po dodaniu tych funkcjonalności nasz kod wygląda następująco:
public class WashingMachine {

    public void wash(Program program) {
        switch (program) {
            case COTTON:
                System.out.println("Wash with cotton clothes program");
                break;
            case WOOL:
                System.out.println("Wash with wool clothes program");
                break;
            case SYNTHETIC:
                System.out.println("Wash with synthetic clothes program");
                break;
            case WHITE:
                System.out.println("Wash with white clothes program");
                break;
            case BLACK:
                System.out.println("Wash with black clothes program");
                break;
            case COLOR:
                System.out.println("Wash with color clothes program");
                break;
        }
    }
}

public enum Program {

    COTTON,
    WOOL,
    SYNTHETIC,
    WHITE,
    BLACK,
    COLOR
} 
Dodając nowe programy, naruszyliśmy Zasadę Otwarte-Zamknięte, gdyż rozszerzenie programu wiązało się z modyfikacją metody wash(). Poza tym, byliśmy zmuszeni dodać kolejne 3 stałe do typu wyliczeniowego Program. Rozszerzając zatem nasz kod, naruszyliśmy zasadę OCP w dwóch miejscach. Nie wspominając o tym, że im więcej programów dodajemy, tym metoda wash() staje się mniej czytelna.

Kod zgodny z OCP

W celu rozwiązania naszego problemu, musimy wykorzystać interfejs oraz zastosować mechanizm polimorfizmu. Spójrzmy na kod poniżej:
public class WashingMachine {

    private Program program;

    public WashingMachine(Program program) {
        this.program = program;
    }

    public void wash() {
        program.wash();
    }
}

public interface Program {

    void wash();
}

public class Cotton implements Program {

    @Override
    public void wash() {
        System.out.println("Wash with cotton clothes program");
    }
}

public class Wool implements Program {

    @Override
    public void wash() {
        System.out.println("Wash with wool clothes program");
    }
}

public class Synthetic implements Program {

    @Override
    public void wash() {
        System.out.println("Wash with synthetic clothes program");
    }
} 
W tym rozwiązaniu zastępujemy typ wyliczeniowy interfejsem Program z zadeklarowaną metodą wash(). Natomiast poszczególne stałe typu Program z poprzedniego przykładu zostały zastąpione odpowiednimi klasami. Każda z nich nadpisuje metodę wash() na swój sposób. Natomiast w klasie WashingMachine wywołujemy metodę wash() na odpowiedniej implementacji interfejsu.
Dzięki takiemu podejściu nie musimy się martwić o to, że będziemy zmuszeni do modyfikacji istniejącego kodu podczas dodawania nowych pogramów. Wystarczy dodać nowe klasy implementujące interfejs Program i nadpisać metodę wash(), jak na diagramie poniżej:
UML diagram
Zastosowanie Zasady Otwarte-Zamknięte spowodowało także, iż nasze oprogramowanie stało się nie tylko prostsze w rozbudowie, ale też czytelniejsze.

Podsumowanie

Rzadko spotyka się takie oprogramowanie, które raz napisane, pozostaje takie aż do końca. Dobre aplikacje, aby cieszyły się uznaniem użytkowników, muszą być stale utrzymywane, refkatoryzowane, należy dodawać do nich nowe funkcjonalności itp. Im częściej jesteśmy zmuszeni modyfikować nasz kod, tym bardziej wzrasta ryzyko, że coś popsujemy. Stosując Zasadę Otwarte-Zamknięte, zadanie mamy znacznie ułatwione, gdyż nie musimy modyfikować naszego kodu, a jedynie dodawać coraz to nowsze rozszerzenia.

Znajdź mnie również na:

arrow