Tylko interfejs! Nie, bo abstrakcja! Dokąd prowadzi dyskryminacja.
Zasada odwracania zależności głosi, że moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Obie grupy modułów powinny zależeć od abstrakcji. Innymi słowy abstrakcje nie powinny zależeć od szczegółowych rozwiązań, to one (rozwiązania) powinny zależeć od abstrakcji. Użyte w treści reguły pojęcie abstrakcji należy interpretować jako klasę abstrakcyjną lub interfejs. Jak widać pozostaje tutaj swoboda wyboru jednego z tych dwóch bytów jako elementu wyjściowego. W każdym bądź razie żadne z nich nie jest tu faworyzowane.
Skoro więc jest wolność wyboru, to czy nie można przyjąć – dla uproszczenia – że używamy tylko jednego z nich? Nic bardziej mylnego. Skoro wystarczyłby tylko jeden byt – istniałby tylko on. A jednak są dwa byty: interfejs i klasa abstrakcyjna. Kiedy więc używać każdego z nich?
Wszystko zależy od kontekstu. Załóżmy (korzystając z dość popularnego przykładu), że mamy zaprogramować zarządzanie pracownikami. Pierwszym spostrzeżeniem jest to, że pracownik to osoba. Charakteryzuje go imię, nazwisko, adres zamieszkania, wiek, płeć. Czy zatem wyprowadzić pracownika od klasy Osoba? A może Osoba powinna być interfejsem? Tylko czy rzeczywiście w ogóle należy rozpatrywać taki byt jak Osoba? Przecież z punktu widzenia tworzonego oprogramowania istotne jest zarządzanie pracownikami i dodatkowy byt – Osoba w ogóle nie jest potrzebny. Wszystkie cechy, jakie miałaby taka klasa może przejąć na siebie klasa Pracownik. Z punktu widzenia realizowanego zadania będzie to wystarczające.
To uproszczenie jednak nie zmienia faktu, że obecnie (dla odmiany) trzeba zadecydować czym tak naprawdę powinien być pracownik – klasą abstrakcyjną czy interfejsem? Osobiście uważam, że klasą abstrakcyjną, albowiem jakikolwiek typ specjalizowanego pracownika nie powstałby potem, każdy będzie musiał zawierać wymienione wyżej cechy. Zatem nie ma sensu tworzyć tych cech jako szkieletu (tak byłoby w interfejsie) i urzeczywistniać ich w klasie, która będzie taki interfejs implementować. Dlaczego? Powody są dwa. Po pierwsze interfejs ten będzie wykorzystywany tylko przez jedną klasę, więc de facto stanie się bytem sztucznym i zbędnym (przez niemożność ponownego zastosowania). A drugi powód, to fakt, że każda specjalizacja pracownika i tak musi zawierać te same właściwości, dlatego, aby nie dublować implementacji powinny zostać one zawarte w klasie abstrakcyjnej. Dzięki temu każdy „dziedzic” tej klasy będzie już korzystał z dobrodziejstwa inwentarza (czyli tego co zaimplementowana w klasie bazowej).
class Employee { public string FirstName { get; set; } public string LastName { get; set; } public Sex Sex { get; set; } public Address Address { get; set; } public decimal Salary { get; set; } }
Czy właśnie przekreśliłem potrzebę używania interfejsu? Nie, on też jest potrzebny, ale w innym celu. W powyższym kontekście nie miał sensu ponieważ wprowadzał zbyteczną nadmiarowość, ale…
Załóżmy, że nasz pracownik posiada także taką (jakże istotną) właściwość, jak wynagrodzenie. Z pewnością nie każdy może to wynagrodzenie ustalać i źle by się stało, aby nieuprawnieni mogli o nim decydować. Niemniej klasa definiująca pracownika powinna umożliwiać oba działania, tak odczytu wynagrodzenia, jak i jego ustalania. Jak w takim razie zrealizować wyjątek? Tu z pomocą przychodzi interfejs. Wystarczy zdefiniować interfejs, który dopuszczać będzie wyłącznie operację odczytu wynagrodzenia i tego interfejsu używać w kontaktach z obiektami, które nie mają prawa modyfikować wynagrodzenia (ot np. obiekt generowania listy płac).
interface ISalaryPreview { public decimal Salary { get; } } class Employee : ISalaryPreview { public string FirstName { get; set; } public string LastName { get; set; } public Sex Sex { get; set; } public Address Address { get; set; } public decimal Salary { get; set; } }
W tym wypadku uzasadnieniem powstania interfejsu jest jego rola ograniczająca dostęp do obiektu. Choć jest on wykorzystywany wyłącznie w ramach obiektu klasy Pracownik i nigdzie indziej (czyli coś co było argumentem przeciw stosowaniu interfejsu jako elementu wyjściowego dla pracowników), to fakt zawężania dostępu do reprezentowanego obiektu, jest wystarczającym dla jego powstania. Inaczej mówiąc – w tym wypadku rezygnacja z interfejsu przekreśla możliwość udostępniania płacy pracowników wyłącznie do odczytu, w przypadku pierwszym (interfejs jako byt wyjściowy dla klasy Pracownik), eliminacja interfejsu pozwalała zrealizować funkcjonalność zarządzania pracownikami bez umniejszania funkcjonalności.
Zasadność powstania interfejsu „tylko do odczytu”, potwierdza także zasada segregacji interfejsów (ISP – Interface Segregation Principle – jedna z reguł SOLID), która głosi, że interfejsy należy wyodrębniać pod kątem korzystających z niego klientów. W tym wypadku klient, którym jest mechanizm generowania listy płac, bez istnienia takiego interfejsu mógłby nadużywać swojej roli, co było niewskazane z punktu widzenia reguł biznesowych.
Jak widać z powyższych rozważań nie można przyjąć jedynie słusznej drogi i np. postawić wyłącznie na interfejsy lub klasy abstrakcyjne. W każdym wypadku należy rozważać zgodność przyjętego rozwiązania ze wszystkimi zasadami SOLID. Także w programowaniu dyskryminacja jest niewskazana.