Obecna sytuacja
Obecna konfiguracja narusza zasadę segregacji interfejsu (I w SOLID).
Odniesienie
Według Wikipedii zasada segregacji interfejsów (ISP) stanowi, że żaden klient nie powinien być zmuszany do polegania na metodach, których nie używa . Zasadę segregacji interfejsów sformułował Robert Martin w połowie lat 90.
Innymi słowy, jeśli jest to twój interfejs:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Następnie każda klasa, która implementuje ten interfejs, musi korzystać z każdej wymienionej metody interfejsu. Bez wyjątku.
Wyobraź sobie, że istnieje ogólna metoda:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Jeśli miałbyś to zrobić tak, aby tylko niektóre klasy implementujące były w stanie usunąć użytkownika, wtedy ta metoda czasami wysadzi ci się w twarz (lub nic nie zrobisz). To nie jest dobry projekt.
Twoje proponowane rozwiązanie
Widziałem rozwiązanie, w którym IUserInterface ma zaimplementowaną metodęAction, która zwraca liczbę całkowitą, która jest wynikiem bitowych OR operacji bitowo AND z żądanymi akcjami.
Zasadniczo chcesz:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Ignoruję, jak dokładnie ustalamy, czy dana klasa jest w stanie usunąć użytkownika. Niezależnie od tego, czy jest to wartość logiczna, trochę flaga ... nie ma znaczenia. Wszystko sprowadza się do odpowiedzi binarnej: czy może usunąć użytkownika, tak czy nie?
To rozwiązałoby problem, prawda? Technicznie tak. Ale teraz naruszasz zasadę substytucji Liskowa (L w wersji SOLID).
Pomijając dość skomplikowane wyjaśnienie Wikipedii, znalazłem dobry przykład na StackOverflow . Zwróć uwagę na „zły” przykład:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Zakładam, że widzisz tutaj podobieństwo. Jest to metoda, która ma obsługiwać obiekt abstrakcyjny ( IDuck, IUserBackend), ale z powodu kompromisowego projektu klasy musi najpierw obsłużyć określone implementacje ( ElectricDuckupewnij się, że nie jest to IUserBackendklasa, która nie może usuwać użytkowników).
Jest to sprzeczne z celem opracowania abstrakcyjnego podejścia.
Uwaga: przykład tutaj jest łatwiejszy do naprawienia niż Twoja sprawa. Dla przykładu, wystarczy mieć ElectricDucksamą kolej na środku tej Swim()metody. Obie kaczki nadal potrafią pływać, więc wynik funkcjonalny jest taki sam.
Możesz zrobić coś podobnego. Nie robić . Nie możesz udawać, że usuwasz użytkownika, ale w rzeczywistości masz pustą treść metody. Chociaż działa to z technicznego punktu widzenia, uniemożliwia ustalenie, czy klasa implementująca rzeczywiście zrobi coś, gdy zostanie o to poproszona. Jest to wylęgarnia nieusuwalnego kodu.
Moje proponowane rozwiązanie
Powiedziałeś jednak, że jest możliwe (i poprawne), że klasa implementująca obsługuje tylko niektóre z tych metod.
Na przykład, powiedzmy, że dla każdej możliwej kombinacji tych metod istnieje klasa, która ją zaimplementuje. Obejmuje wszystkie nasze bazy.
Rozwiązaniem jest tutaj podział interfejsu .
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
Zauważ, że widziałeś to na początku mojej odpowiedzi. Nazwa zasady segregacji interfejsów już pokazuje, że zasada ta została zaprojektowana, aby segregować interfejsy w wystarczającym stopniu.
Pozwala to na łączenie i łączenie interfejsów według własnego uznania:
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
Każda klasa może zdecydować, co chce zrobić, bez łamania umowy o interfejsie.
Oznacza to również, że nie musimy sprawdzać, czy dana klasa jest w stanie usunąć użytkownika. Każda klasa, która implementuje IDeleteUserServiceinterfejs, będzie mogła usunąć użytkownika = Bez naruszenia zasady substytucji Liskowa .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Jeśli ktoś spróbuje przekazać obiekt, który się nie implementuje IDeleteUserService, program odmówi kompilacji. Dlatego lubimy mieć bezpieczeństwo typu.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Notatka
Doszedłem do skrajności tego przykładu, dzieląc interfejs na możliwie najmniejsze części. Jeśli jednak Twoja sytuacja jest inna, możesz uciec od większych kawałków.
Na przykład jeśli każda usługa, która może utworzyć użytkownika, jest zawsze w stanie usunąć użytkownika (i odwrotnie), możesz zachować te metody jako część jednego interfejsu:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Robienie tego nie ma żadnej korzyści technicznej zamiast rozdzielania na mniejsze fragmenty; ale sprawi, że rozwój będzie nieco łatwiejszy, ponieważ wymaga mniej kotłów.
IUserBackendpowinna w ogóle zawierać tejdeleteUsermetody. To powinno być częściąIUserDeleteBackend(lub jakkolwiek chcesz to nazwać). Kod, który musi usunąć użytkowników, będzie miał argumentyIUserDeleteBackend, kod, który nie potrzebuje tej funkcji, będzie używałIUserBackendi nie będzie miał problemów z niezaimplementowanymi metodami.