Rozumiem motyw zasady najmniejszej wiedzy , ale znajdę pewne wady, jeśli spróbuję zastosować ją w moim projekcie.
Jeden z przykładów tej zasady (właściwie jak jej nie używać), które znalazłem w książce Wzory pierwszego projektu, określają, że błędne jest wywoływanie metody na obiektach, które zostały zwrócone z wywołania innych metod, pod względem tej zasady .
Wydaje się jednak, że czasami bardzo potrzebne jest korzystanie z takich możliwości.
Na przykład: Mam kilka klas: klasę przechwytywania wideo, klasę kodera, klasę streamerów i wszystkie one używają podstawowych innych klas, VideoFrame, a ponieważ wchodzą ze sobą w interakcje, mogą na przykład zrobić coś takiego:
streamer
kod klasowy
...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....
Jak widać, zasada ta nie ma tu zastosowania. Czy można tu zastosować tę zasadę, czy też nie zawsze można zastosować tę zasadę w takim projekcie?
źródło
Odpowiedzi:
Zasada, o której mówisz (lepiej znana jako Prawo Demetera ) dla funkcji, można zastosować, dodając kolejną metodę pomocniczą do klasy streamera, taką jak
Teraz każda funkcja „rozmawia tylko z przyjaciółmi”, a nie z „przyjaciółmi znajomych”.
IMHO jest szorstką wytyczną, która może pomóc w stworzeniu metod, które ściśle odpowiadają zasadzie pojedynczej odpowiedzialności. W prostym przypadku, takim jak powyższy, jest to prawdopodobnie bardzo opiniotwórcze, czy naprawdę jest to kłopotliwe i czy wynikowy kod jest naprawdę „czystszy”, czy też po prostu formalnie rozszerzy kod bez zauważalnego zysku.
źródło
Zasada najmniejszej wiedzy lub Prawo Demetera jest ostrzeżeniem przed zaplątaniem się w twoją klasę szczegółami innych klas, które przemierzają warstwę po warstwie. Mówi ci, że lepiej rozmawiać tylko z „przyjaciółmi”, a nie z „przyjaciółmi znajomych”.
Wyobraź sobie, że zostałeś poproszony o przyspawanie tarczy do posągu rycerza w lśniącej zbroi płytowej. Ostrożnie ustawiasz tarczę na lewym ramieniu, aby wyglądała naturalnie. Zauważasz, że są trzy małe miejsca na przedramieniu, łokciu i ramieniu, gdzie tarcza dotyka zbroi. Spawaj wszystkie trzy miejsca, ponieważ chcesz mieć pewność, że połączenie jest silne. Teraz wyobraź sobie, że twój szef jest szalony, ponieważ nie może poruszyć łokciem swojej zbroi. Zakładałeś, że zbroja nigdy się nie poruszy, a zatem stworzyłeś nieruchome połączenie między przedramieniem a ramieniem. Tarcza powinna łączyć się tylko z przyjacielem, przedramieniem. Nie do przyjaciół przedramion. Nawet jeśli musisz dodać kawałek metalu, aby ich dotknąć.
Metafory są fajne, ale co właściwie rozumiemy przez przyjaciela? Każda rzecz, którą obiekt potrafi stworzyć lub znaleźć, jest przyjacielem. Alternatywnie, obiekt może po prostu poprosić o przekazanie innych obiektów , z których zna tylko interfejs. Nie liczą się oni jako przyjaciele, ponieważ nie jest narzucone oczekiwanie, jak je zdobyć. Jeśli obiekt nie wie, skąd się wziął, ponieważ coś przeszło / wstrzyknęło, to nie jest przyjacielem przyjaciela, to nawet nie jest przyjacielem. Jest to coś, co obiekt umie tylko używać. To dobra rzecz.
Próbując zastosować takie zasady, ważne jest, aby zrozumieć, że nigdy nie zabraniają ci osiągnięcia czegoś. Są ostrzeżeniem, że możesz zaniedbywać więcej pracy, aby osiągnąć lepszy projekt, który osiąga to samo.
Nikt nie chce pracować bez powodu, dlatego ważne jest, aby zrozumieć, co z tego wynika. W takim przypadku kod jest elastyczny. Możesz wprowadzać zmiany i mieć mniej innych klas, na które mogą się martwić. To brzmi dobrze, ale nie pomaga ci zdecydować, co robić, chyba że potraktujesz to jako doktrynę religijną.
Zamiast ślepo przestrzegać tej zasady, weź prostą wersję tego problemu. Napisz rozwiązanie, które nie będzie zgodne z zasadą i takie, które spełnia. Teraz, gdy masz dwa rozwiązania, możesz porównać ich wrażliwość na zmiany, próbując wprowadzić je w oba.
Jeśli NIE MOŻESZ rozwiązać problemu zgodnie z tą zasadą, prawdopodobnie brakuje Ci innej umiejętności.
Jednym z rozwiązań Twojego konkretnego problemu jest wstrzyknięcie
frame
czegoś (klasy lub metody), która wie, jak rozmawiać z ramkami, abyś nie musiał rozpowszechniać wszystkich szczegółów rozmowy w ramce w swojej klasie, które teraz wiedzą tylko, jak i kiedy dostać ramkę.To faktycznie jest zgodne z inną zasadą: Oddziel użycie od konstrukcji.
Korzystając z tego kodu, bierzesz odpowiedzialność za jakoś uzyskanie
Frame
. Nie wziąłeś jeszcze żadnej odpowiedzialności za rozmowę zFrame
.Teraz musisz wiedzieć, jak rozmawiać
Frame
, ale zastąp to:A teraz musisz tylko wiedzieć, jak rozmawiać z przyjacielem, FrameHandler.
Jest wiele sposobów na osiągnięcie tego i być może nie jest to najlepszy, ale pokazuje, że przestrzeganie zasady nie czyni problemu nierozwiązywalnym. To po prostu wymaga od ciebie więcej pracy.
Każda dobra reguła ma wyjątek. Najlepsze przykłady, jakie znam, to wewnętrzne języki specyficzne dla domeny . A DSL s łańcuch Sposóbzdaje się nadużywać Prawa Demeter przez cały czas, ponieważ ciągle wywołujesz metody zwracające różne typy i używasz ich bezpośrednio. Dlaczego to jest w porządku? Ponieważ w DSL wszystko, co jest zwracane, to starannie zaprojektowany przyjaciel, z którym masz rozmawiać bezpośrednio. Z założenia masz prawo oczekiwać, że łańcuch metod DSL się nie zmieni. Nie masz tego prawa, jeśli po prostu losowo zagłębisz się w bazę kodu, łącząc razem wszystko, co znajdziesz. Najlepsze DSL to bardzo cienkie reprezentacje lub interfejsy z innymi obiektami, do których prawdopodobnie nie powinieneś zagłębiać się. Wspominam o tym tylko dlatego, że odkryłem, że lepiej rozumiałem prawo demeter, kiedy dowiedziałem się, dlaczego DSL są dobrym projektem. Niektórzy posunęli się nawet do stwierdzenia, że DSL nawet nie naruszają prawdziwego prawa demeter.
Innym rozwiązaniem jest pozwolenie, aby coś innego wstrzyknęło
frame
ci. Jeśliframe
przyszedł od setera lub najlepiej konstruktora, nie bierzesz żadnej odpowiedzialności za konstruowanie lub nabywanie ramki. Oznacza to, że twoja rola tutaj będzie o wiele bardziej podobnaFrameHandlers
. Zamiast tego teraz jesteś tym, który rozmawiaFrame
i robi coś innego, aby dowiedzieć się, jak uzyskać.Frame
W pewien sposób jest to to samo rozwiązanie z prostą zmianą perspektywy.Zasady SOLID są tymi największymi, których staram się przestrzegać. Dwa z nich są przestrzegane tutaj: zasady pojedynczej odpowiedzialności i zasady inwersji zależności. Naprawdę trudno jest uszanować tych dwóch i nadal naruszać Prawo Demetera.
Mentalność, która narusza Demeter, jest jak jedzenie w restauracji bufetowej, w której po prostu łapiesz, co chcesz. Przy odrobinie pracy z góry możesz zapewnić sobie menu i serwer, który przyniesie ci wszystko, co chcesz. Usiądź wygodnie, zrelaksuj się i napiwek.
źródło
Czy projektowanie funkcjonalne jest lepsze niż projektowanie obiektowe? To zależy.
Czy MVVM jest lepszy niż MVC? To zależy.
Amos i Andy czy Martin i Lewis? To zależy.
Od czego to zależy? Wybory, których dokonujesz, zależą od tego, jak dobrze każda technika lub technologia spełnia funkcjonalne i niefunkcjonalne wymagania twojego oprogramowania, jednocześnie odpowiednio spełniając twoje cele projektowe, wydajnościowe i konserwacyjne.
Kiedy czytasz to w książce lub blogu, oceń roszczenie na podstawie jego zasadności; to znaczy zapytaj dlaczego. Nie ma żadnej dobrej lub złej techniki w tworzeniu oprogramowania, jest tylko „jak dobrze ta technika spełnia moje cele? Czy jest skuteczna czy nieskuteczna? Czy rozwiązuje jeden problem, ale tworzy nowy? Czy może być dobrze zrozumiany przez cały zespół programistów, czy jest to zbyt niejasne? ”
W tym konkretnym przypadku - akt wywołania metody na obiekcie zwrócony przez inną metodę - ponieważ istnieje rzeczywisty wzorzec projektowy, który kodyfikuje tę praktykę (fabryka), trudno sobie wyobrazić, jak można stwierdzić, że jest kategorycznie źle.
Powodem tego jest „zasada najmniejszej wiedzy”, ponieważ „niskie sprzężenie” jest pożądaną jakością systemu. Obiekty, które nie są ściśle ze sobą związane, działają bardziej niezależnie, a zatem są łatwiejsze do utrzymania i modyfikacji indywidualnie. Ale jak pokazuje twój przykład, są chwile, w których lepsze sprzężenie jest bardziej pożądane, aby obiekty mogły bardziej efektywnie koordynować swoje wysiłki.
źródło
Odpowiedź doktora Browna pokazuje klasyczną implementację podręcznika Law of Demeter - i irytację / chaos związany z dodawaniem dziesiątek metod w ten sposób, prawdopodobnie dlatego programiści, w tym ja, często nie przejmują się tym, nawet jeśli powinni.
Istnieje alternatywny sposób na rozdzielenie hierarchii obiektów:
W przypadku Original Poster (OP),
encoder->WaitEncoderFrame()
zwróciłbyIEncoderFrame
zamiast aFrame
, i określiłby, jakie operacje są dozwolone.ROZWIĄZANIE 1
W najprostszym przypadku,
Frame
aEncoder
obie klasy są pod twoją kontrolą,IEncoderFrame
jest podzbiorem metod, które Frame już publicznie ujawnia, aEncoder
klasy tak naprawdę nie obchodzi, co zrobisz z tym obiektem. Wówczas implementacja jest trywialna ( kod w c # ):ROZWIĄZANIE 2
W pośrednim przypadku, gdy
Frame
definicja nie jest pod twoją kontrolą lub niewłaściwe byłoby dodanieIEncoderFrame
metodFrame
, dobrym rozwiązaniem jest adapter . Tak omawia odpowiedź CandiedOrange , asnew FrameHandler( frame )
. WAŻNE: Jeśli to zrobisz, będzie bardziej elastyczny, jeśli udostępnisz go jako interfejs , a nie jako klasę .Encoder
musi wiedziećclass FrameHandler
, ale klienci muszą tylko wiedziećinterface IFrameHandler
. Lub, jak to nazwałem,interface IEncoderFrame
- aby wskazać, że jest to konkretnie ramka widziana z POV enkodera :KOSZT: Przydział i GC nowego obiektu, EncoderFrameWrapper, za każdym razem, gdy
encoder.TheFrame
jest wywoływane. (Możesz buforować to opakowanie, ale to dodaje więcej kodu. I jest łatwe do niezawodnego kodowania tylko wtedy, gdy pola ramki kodera nie można zastąpić nową ramką.)ROZWIĄZANIE 3
W trudniejszym przypadku nowe opakowanie musiałoby wiedzieć o obu
Encoder
iFrame
. Sam obiekt naruszyłby LoD - manipuluje relacją między Enkoderem a Ramą, co powinno być obowiązkiem Enkodera - i prawdopodobnie utrudniłby prawidłowe działanie. Oto, co może się zdarzyć, jeśli zaczniesz tą drogą:To stało się brzydkie. Jest mniej skomplikowana implementacja, gdy opakowanie musi dotknąć szczegółów swojego twórcy / właściciela (kodera):
Oczywiście, gdybym wiedział, że tu skończę, mógłbym tego nie zrobić. Można po prostu napisać metody LoD i zrobić to. Nie ma potrzeby definiowania interfejsu. Z drugiej strony podoba mi się to, że interfejs łączy ze sobą powiązane metody. Lubię, jak to jest robić „operacje podobne do ramki”, co przypomina ramkę.
UWAGI KOŃCOWE
Zastanów się: jeśli implementator
Encoder
poczuł, że ujawnianieFrame frame
jest odpowiednie dla ich ogólnej architektury lub było „o wiele łatwiejsze niż implementacja LoD”, byłoby znacznie bezpieczniej, gdyby zamiast tego zrobili pierwszy fragment, który pokazuję - ujawniają ograniczony podzbiór Ramka jako interfejs. Z mojego doświadczenia wynika, że często jest to całkowicie wykonalne rozwiązanie. Po prostu dodaj metody do interfejsu w razie potrzeby. (Mówię o scenariuszu, w którym „wiemy”, że Frame ma już potrzebne metody, lub ich dodanie byłoby łatwe i nie budzi kontrowersji. Funkcją „implementacji” dla każdej metody jest dodanie jednej linii do definicji interfejsu). I wiedz, że nawet w najgorszym przyszłym scenariuszu możliwe jest utrzymanie tego interfejsu API - tutaj,IEncoderFrame
Frame
Encoder
.Należy również pamiętać, że jeśli nie masz uprawnień do dodawania
IEncoderFrame
doFrame
, lub wymagające metody nie pasują dobrze do ogólnejFrame
klasy i rozwiązanie nr 2 nie Ci odpowiadać, być może ze względu na dodatkowy obiektowego tworzenia i-zniszczenia, rozwiązanie nr 3 można postrzegać jako po prostu sposób na zorganizowanie metodEncoder
realizacji LoD. Nie wystarczy przejść przez dziesiątki metod. Zawiń je w interfejs i użyj „jawnej implementacji interfejsu” (jeśli jesteś w c #), aby można było uzyskać do nich dostęp tylko wtedy, gdy obiekt jest oglądany przez ten interfejs.Kolejną kwestią, na którą chcę podkreślić, jest to, że decyzja o ujawnieniu funkcjonalności jako interfejsu poradziła sobie ze wszystkimi 3 sytuacjami opisanymi powyżej. W pierwszym
IEncoderFrame
jest po prostu podzbiórFrame
funkcjonalności. W drugimIEncoderFrame
znajduje się adapter. W trzecimIEncoderFrame
jest podział naEncoder
funkcjonalność. Nie ma znaczenia, czy Twoje potrzeby zmienią się między tymi trzema sytuacjami: interfejs API pozostaje taki sam.źródło