SOLIDny tydzień – O jak Open-Closed
Dzisiaj bierzemy na warsztat drugą literę z ze zbioru SOLID czyli 'O’ jak Open-Closed. Regułę prostą ale bardzo ważną zwłaszcza w większych projektach.
W skrócie
Open-closed czyli 'otwarty-zamknięty’ jest regułą głoszącą, że klasa jest otwarta na rozbudowę ale zamknięta na modyfikację. Tak brzmi oficjalna definicja, którą sobie zaraz omówimy.
Druga litera zbioru SOLID, tak jak wspomniałem na wstępie jest jedną z prostszych, jeśli nie najprostszą z reguł. Dzięki temu jej stosowanie nie stanowi problemu nawet dla osób mało doświadczonych. Jednak nie zawsze są one świadome korzyści z tego płynących. A dopiero świadome korzystanie z zasad daje wymierne efekty i widocznie poprawia jakość.
Czyli, że co?
Mimo, że definicja Open-Closed brzmi lekko zagmatwanie to jest ona jak najbardziej logiczna. Najprościej rzecz ujmując chodzi o to, że rozbudowa klasy o obsługę nowych elementów powinna być możliwa bez konieczności zmieniania czegokolwiek w samej klasie. Niewykonalne? A co jeśli Ci powiem, że prawdopodobnie spotkałeś się z przedmiotami „implementującymi” tą zasadę w rzeczywistości?
Idealnym przykładem jest tutaj komputer stacjonarny. Możesz mu zmienić kartę graficzną albo dołożyć więcej pamięci RAM lub zamienić dysk z HDD na SSD bez konieczności grzebania w płycie głównej w celu dostosowania jej do innego modelu. Po prostu wymieniasz co trzeba korzystając z dostępnych i zgodnych interfejsów i fizycznie wszystko działa.
Dokładnie tak samo powinno być z Twoimi klasami – wkładasz inny element zgodny z interfejsem i wszystko działa. Tylko tyle i aż tyle.
Częstym błędem jest zamykanie się na rozbudowę bez modyfikacji poprzez odwoływanie się w klasie bezpośrednio do innej konkretnej klasy. Przykładowo możemy mieć prostą klasę do obsługi płatności, która wysyła zapytanie do PayPala:
class Payment { private PayPalService _payPalService; public Payment(PayPalService payPalService) { _payPalService = payPalService; } public void Pay(decimal amount) { // ... something ... _payPalService.Request(amount); } }
Teraz chcąc zmienić dostawcę płatności np. PayU musisz zmodyfikować klasę Payment tak żeby korzystała z innego serwisu, który będzie robił co prawda to samo ale będzie innego typu.
Sytuacja jeszcze bardziej się skomplikuje kiedy dasz użytkownikowi wybór przez co chce płacić. Bo co wtedy? Będziesz przyjmował kolejny serwis i dodawał kolejną metodę robiącą to samo tylko inaczej?
class Payment { private PayPalService _payPalService; private PayUService _payUService; public Payment(PayPalService payPalService, PayUService payUService) { _payPalService = payPalService; _payUService= payUService; } public void PayByPayPal(decimal amount) { // ... something ... _payPalService.Request(amount); } public void PayByPayU(decimal amount) { // ... something ... _payUService.Request(amount); } }
Zaczyna się robić dziwnie.
A może by tak interfejsik?
A wystarczyło żeby klasy Payment nie obchodziło jaki dokładnie dostawca jest użyty, byle zawierał pasujący interfejs. Dokładnie tak samo jak w komputerze – płyty głównej nie obchodzi czy włożyłeś kartę NVidii czy AMD, ważne żeby posiadała interfejs PCI-Express.
Dla powyższego przykładu można problem rozwiązać łącząc klasę Payment jedynie z interfejsem IPaymentProvider.
Albo przez konstruktor:
interface IPaymentProvider { void Request(double amount, ...); } class Payment { private IPaymentProvider _paymentProvider; public Payment(IPaymentProvider paymentProvider) { _paymentProvider = paymentProvider; } public void Pay(decimal amount) { // ... something ... _paymentProvider.Request(amount); } }
Albo przez parametr metody:
interface IPaymentProvider { void Request(double amount, ...); } class Payment { public void Pay(decimal amount, IPaymentProvider paymentProvider) { // ... something ... paymentProvider.Request(amount); } }
Zależnie od tego jak tworzysz i używasz obiekt klasy Payment.
Teraz, mając interfejs, klasa Payment może działać z PayPalem, który ten interfejs będzie implementował. Ale jeżeli zajdzie potrzeba zmiany dostawcy to po prostu go przekażesz, nie dotykając nawet jednego znaku w kodzie klasy Payment.
Oczywiście zamiast interfejsu możesz użyć w razie potrzeby klasy abstrakcyjnej albo po prostu klasy bazowej jednak w 95% przypadków interfejs jest wystarczający i najbardziej uniwersalny, zwłaszcza w języku C#.
O poprzedniej regule możesz przeczytać we wczorajszym wpisie.
A jeżeli nie chcesz przegapić żadnej z części i dostać w niedzielę maila podsumowującego z ich pełną listą to zapisz się do mojego newslettera:
private IPaymentProvider _paymentProvider;
public Payment(PayPalService payPalService)
{
_payPalService = payPalService;
}
tutaj parametrem nie powinien być interfejs?
Faktycznie! Dzięki, już poprawiam :) Tak to jest jak się kopiuje kod.
To jeszcze tylko w:
public void Pay(decimal amount, PayPalService payPalService)
{
// … something …
paymentProvider.Request(amount);
}
Też powinien być interfejs :)
Ostatni raz pisałem posta przed spaniem i sprawdzałem zaraz po obudzeniu się…
Dzięki! :D