Scenariusz
Aplikacja internetowa definiuje interfejs zaplecza użytkownika IUserBackend
za pomocą metod
- getUser (uid)
- createUser (uid)
- deleteUser (identyfikator użytkownika)
- setPassword (identyfikator użytkownika, hasło)
- ...
Różne interfejsy użytkownika (np. LDAP, SQL, ...) implementują ten interfejs, ale nie każdy backend może zrobić wszystko. Na przykład konkretny serwer LDAP nie pozwala tej aplikacji internetowej na usuwanie użytkowników. Zatem LdapUserBackend
klasa, która implementuje IUserBackend
, nie będzie implementować deleteUser(uid)
.
Konkretna klasa musi zakomunikować aplikacji internetowej, co aplikacja internetowa może robić z użytkownikami backendu.
Znane rozwiązanie
Widziałem rozwiązanie, w którym metoda IUserInterface
ma implementedActions
metodę, która zwraca liczbę całkowitą, która jest wynikiem bitowych OR operacji bitowych AND z żądanymi akcjami:
function implementedActions(requestedActions) {
return (bool)(
ACTION_GET_USER
| ACTION_CREATE_USER
| ACTION_DELTE_USER
| ACTION_SET_PASSWORD
) & requestedActions)
}
Gdzie
- ACTION_GET_USER = 1
- ACTION_CREATE_USER = 2
- ACTION_DELETE_USER = 4
- ACTION_SET_PASSWORD = 8
- .... = 16
- .... = 32
itp.
Tak więc aplikacja internetowa ustawia maskę bitową z tym, czego potrzebuje i implementedActions()
odpowiada za pomocą logicznej wartości logicznej, czy ją obsługuje.
Opinia
Te operacje bitowe dla mnie wyglądają jak relikty z epoki C, niekoniecznie łatwe do zrozumienia pod względem czystego kodu.
Pytanie
Jaki jest nowoczesny (lepszy?) Wzorzec dla klasy do komunikowania podzbioru metod interfejsów, które implementuje? Czy też „metoda operacji bitowej” z góry jest nadal najlepszą praktyką?
( W przypadku, gdy ma to znaczenie: PHP, chociaż szukam ogólnego rozwiązania dla języków OO )
źródło
IUserBackend
powinna w ogóle zawierać tejdeleteUser
metody. 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łIUserBackend
i nie będzie miał problemów z niezaimplementowanymi metodami.Odpowiedzi:
Mówiąc ogólnie, można tu zastosować dwa podejścia: test i rzut lub kompozycja poprzez polimorfizm.
Testuj i rzucaj
Takie podejście już opisałeś. W jakiś sposób wskazujesz użytkownikowi klasy, czy pewne inne metody są zaimplementowane, czy nie. Można to zrobić za pomocą jednej metody i wyliczenia bitowego (jak opisano) lub za pomocą szeregu
supportsDelete()
metod itp.Następnie, jeśli
supportsDelete()
zwracafalse
, wywołaniedeleteUser()
może spowodowaćNotImplementedExeption
wyrzucenie lub metoda po prostu nic nie robi.Jest to popularne rozwiązanie wśród niektórych, ponieważ jest proste. Jednak wielu - łącznie ze mną - argumentuje, że jest to naruszenie zasady substytucji Liskowa (L w SOLID) i dlatego nie jest dobrym rozwiązaniem.
Kompozycja poprzez polimorfizm
Podejście tutaj polega na tym, aby uznać
IUserBackend
instrument za zbyt tępy. Jeśli klasy nie zawsze mogą zaimplementować wszystkie metody w tym interfejsie, podziel interfejs na bardziej skoncentrowane części. Być może masz:IGeneralUser IDeletableUser IRenamableUser ...
Innymi słowy, wszystkie metody, które mogą zaimplementować wszystkie twoje backendy, wchodząIGeneralUser
i tworzysz osobny interfejs dla każdej akcji, którą tylko niektóre mogą wykonać.W ten sposób
LdapUserBackend
nie implementuje sięIDeletableUser
i testujesz to za pomocą testu takiego jak (używając składni C #):(Nie jestem pewien mechanizmu w PHP do określania, czy instancja implementuje interfejs i jak następnie rzutujesz na ten interfejs, ale jestem pewien, że istnieje odpowiednik w tym języku)
Zaletą tej metody jest to, że dobrze wykorzystuje polimorfizm, aby umożliwić Twojemu kodowi zgodność z zasadami SOLID, i jest moim zdaniem o wiele bardziej elegancki.
Minusem jest to, że zbyt łatwo może stać się nieporęczny. Jeśli, na przykład, będziesz musiał wdrożyć dziesiątki interfejsów, ponieważ każdy konkretny backend ma nieco inne możliwości, to nie jest to dobre rozwiązanie. Radzę więc, abyś wykorzystał swój osąd, czy takie podejście jest dla ciebie praktyczne przy tej okazji i zastosuj go, jeśli tak jest.
źródło
if (backend instanceof IDelatableUser) {...}
Divide(float,float)
metodzie. Wartość wejściowa jest zmienna, a wyjątek obejmuje niewielki podzbiór możliwych wykonań. Ale jeśli rzucasz w oparciu o typ implementacji, to niemożność wykonania jest faktem. Wyjątek obejmuje wszystkie możliwe dane wejściowe , a nie tylko ich podzbiór. To tak, jakby umieścić znak „mokrej podłogi” na każdej mokrej podłodze w świecie, w którym każda podłoga jest zawsze mokra.NotImplementedException
. Ten wyjątek jest przeznaczony do tymczasowych wyłączeń, tj. Kodu, który nie został jeszcze opracowany, ale zostanie opracowany. To nie to samo, co definitywne stwierdzenie, że dana klasa nigdy nie zrobi nic z daną metodą, nawet po zakończeniu rozwoju.Obecna sytuacja
Obecna konfiguracja narusza zasadę segregacji interfejsu (I w SOLID).
Odniesienie
Innymi słowy, jeśli jest to twój interfejs:
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:
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
Zasadniczo chcesz:
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:
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 (ElectricDuck
upewnij się, że nie jest toIUserBackend
klasa, 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ć
ElectricDuck
samą kolej na środku tejSwim()
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 .
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:
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
IDeleteUserService
interfejs, będzie mogła usunąć użytkownika = Bez naruszenia zasady substytucji Liskowa .Jeśli ktoś spróbuje przekazać obiekt, który się nie implementuje
IDeleteUserService
, program odmówi kompilacji. Dlatego lubimy mieć bezpieczeństwo typu.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:
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.
źródło
TryDeleteUser
aby to odzwierciedlić); lub masz metodę celowo zgłaszającą wyjątek, jeśli jest to możliwa, ale problematyczna sytuacja. Korzystanie z metodyCanDoThing()
iDoThing()
działa, ale wymagałoby to od zewnętrznych rozmówców użycia dwóch połączeń (i ukarania ich za niepowodzenie), co jest mniej intuicyjne i nie tak eleganckie.Jeśli chcesz używać typów wyższego poziomu, możesz wybrać typ zestawu w wybranym języku. Mam nadzieję, że zapewnia on cukier składniowy do wykonywania określonych skrzyżowań i określania podzbiorów.
Tak właśnie robi Java z EnumSet (bez cukru składniowego, ale hej, to Java)
źródło
W świecie .NET możesz dekorować metody i klasy niestandardowymi atrybutami. Może to nie dotyczyć twojej sprawy.
Wydaje mi się jednak, że problem, który masz, może dotyczyć wyższego poziomu projektu.
Jeśli jest to funkcja interfejsu użytkownika, taka jak strona edycji użytkownika lub komponent, to w jaki sposób maskowane są różne możliwości? W tym przypadku „test i rzut” będzie dość nieefektywnym podejściem do tego celu. Zakłada się, że przed załadowaniem każdej strony uruchamia się próbne wywołanie każdej funkcji w celu ustalenia, czy widżet lub element powinien być ukryty, czy przedstawiony inaczej. Alternatywnie, masz stronę internetową, która w zasadzie zmusza użytkownika do odkrycia tego, co jest dostępne przez ręczne „przetestuj i wyrzuć”, jakąkolwiek drogę kodowania wybierzesz, ponieważ użytkownik nie odkryje, że coś jest niedostępne, dopóki nie pojawi się wyskakujące ostrzeżenie.
W przypadku interfejsu użytkownika możesz chcieć przyjrzeć się zarządzaniu funkcjami i powiązać z tym wybór dostępnych implementacji, zamiast wybierać implementacje, które określają, którymi funkcjami można zarządzać. Możesz przyjrzeć się ramom do tworzenia zależności funkcji i jawnie zdefiniować możliwości jako byty w modelu domeny. Można to nawet powiązać z autoryzacją. Zasadniczo, podejmowanie decyzji, czy zdolność jest dostępna, czy nie, w oparciu o poziom autoryzacji, można rozszerzyć na decyzję, czy zdolność jest rzeczywiście zaimplementowana, a następnie „funkcje” interfejsu użytkownika na wysokim poziomie mogą mieć wyraźne odwzorowania na zestawy zdolności.
Jeśli jest to interfejs API sieci Web, ogólny wybór projektu może być skomplikowany przez konieczność obsługi wielu publicznych wersji interfejsu API „Zarządzaj użytkownikiem” lub zasobu REST „Użytkownik” w miarę rozszerzania się możliwości.
Podsumowując, w świecie .NET możesz wykorzystać różne sposoby Refleksji / Atrybutu, aby z góry określić, które klasy zaimplementować, ale w każdym razie wydaje się, że prawdziwe problemy będą związane z tym, co robisz z tymi informacjami.
źródło