Zasada najmniejszej wiedzy

32

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?

okup
źródło
możliwy duplikat łączenia łańcuchowego z enkapsulacją
komnata
4
To prawdopodobnie bliższy duplikat.
Robert Harvey
1
Powiązane: Czy taki kod jest „wrakiem pociągu” (z naruszeniem Prawa Demeter)? (pełne ujawnienie: to moje własne pytanie)
CVn

Odpowiedzi:

21

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

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

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.

Doktor Brown
źródło
Takie podejście ma tę zaletę, że znacznie ułatwia testowanie jednostkowe, debugowanie i uzasadnianie kodu.
gntskn
+1. Chociaż istnieje bardzo dobry powód, aby nie wprowadzać zmian w kodzie, jest to, że interfejs klasy może zostać rozdęty metodami, których zadaniem jest wykonanie jednego lub kilku wywołań na kilka innych metod (i bez dodatkowej logiki). W niektórych typach środowiska programistycznego, powiedzmy C ++ COM (zwłaszcza WIC i DirectX), dodanie każdej pojedynczej metody do interfejsu COM wiąże się z wysokimi kosztami w porównaniu z innymi językami.
rwong
1
W projektowaniu interfejsu COM w C ++, rozkładanie dużych klas na małe klasy (co oznacza, że ​​masz większą szansę na rozmowę z wieloma obiektami) i minimalizowanie liczby metod interfejsu to dwa cele projektowe przynoszące realne korzyści (obniżenie kosztów) oparte na głębokim zrozumieniu mechaniki wewnętrznej (tabele wirtualne, możliwość ponownego użycia kodu, wiele innych rzeczy). Dlatego programiści C ++ COM zwykle muszą ignorować LoD.
rwong,
10
+1: Prawo Demetera
Binarny Worrier
Prawo demeter jest wskazówką do dokonywania wyborów architektonicznych, podczas gdy sugerujesz zmianę kosmetyczną, więc wydaje się, że przestrzegasz tego „prawa”. To byłoby nieporozumienie, ponieważ zmiana składniowa nie oznacza nagle, że wszystko jest w porządku. O ile mi wiadomo, prawo demeter w gruncie rzeczy oznaczało: jeśli chcesz robić OOP, przestań pisać kod produkcyjny z funkcjami gettera wszędzie.
user2180613
39

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 frameczegoś (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.

frame = encoder->WaitEncoderFrame()

Korzystając z tego kodu, bierzesz odpowiedzialność za jakoś uzyskanie Frame. Nie wziąłeś jeszcze żadnej odpowiedzialności za rozmowę z Frame.

frame->DoOrGetSomething(); 

Teraz musisz wiedzieć, jak rozmawiać Frame, ale zastąp to:

new FrameHandler(frame)->DoOrGetSomething();

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 frameci. Jeśli frameprzyszedł 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 podobna FrameHandlers. Zamiast tego teraz jesteś tym, który rozmawia Framei 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.

candied_orange
źródło
2
„Mówi ci, że lepiej rozmawiać tylko ze swoimi„ przyjaciółmi ”, a nie z„ przyjaciółmi znajomych ”” ++
RubberDuck
1
Drugi akapit, czy to gdzieś został skradziony, czy wymyśliłeś? To pozwoliło mi całkowicie zrozumieć tę koncepcję . Sprawiło, że stało się jasne, kim są „przyjaciele przyjaciół” i jakie są wady uwikłania zbyt wielu klas (stawów). Cytat +.
die maus
2
@diemaus Jeśli ukradłem ten akapit tarczy skądś, jego źródło wyciekło mi z głowy. Wtedy mój mózg myślał, że to sprytne. Pamiętam tylko, że dodałem go po wielu opiniach, więc miło jest zobaczyć, jak został zatwierdzony. Cieszę się, że pomogło.
candied_orange
19

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.

[Jakaś książka] mówi, że [coś] jest nie tak.

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.

Robert Harvey
źródło
2

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:

Ujawniaj interfacetypy, a nie classtypy, za pomocą metod i właściwości.

W przypadku Original Poster (OP), encoder->WaitEncoderFrame()zwróciłby IEncoderFramezamiast a Frame, i określiłby, jakie operacje są dozwolone.


ROZWIĄZANIE 1

W najprostszym przypadku, Framea Encoderobie klasy są pod twoją kontrolą, IEncoderFramejest podzbiorem metod, które Frame już publicznie ujawnia, a Encoderklasy tak naprawdę nie obchodzi, co zrobisz z tym obiektem. Wówczas implementacja jest trywialna ( kod w c # ):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

ROZWIĄZANIE 2

W pośrednim przypadku, gdy Framedefinicja nie jest pod twoją kontrolą lub niewłaściwe byłoby dodanie IEncoderFramemetod Frame, dobrym rozwiązaniem jest adapter . Tak omawia odpowiedź CandiedOrange , as new FrameHandler( frame ). WAŻNE: Jeśli to zrobisz, będzie bardziej elastyczny, jeśli udostępnisz go jako interfejs , a nie jako klasę . Encodermusi 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 :

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

KOSZT: Przydział i GC nowego obiektu, EncoderFrameWrapper, za każdym razem, gdy encoder.TheFramejest 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 Encoderi Frame. 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ą:

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

To stało się brzydkie. Jest mniej skomplikowana implementacja, gdy opakowanie musi dotknąć szczegółów swojego twórcy / właściciela (kodera):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

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 Encoderpoczuł, że ujawnianie Frame framejest 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,IEncoderFrameFrameEncoder.

Należy również pamiętać, że jeśli nie masz uprawnień do dodawania IEncoderFramedo Frame, lub wymagające metody nie pasują dobrze do ogólnej Frameklasy 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 metod Encoderrealizacji 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 IEncoderFramejest po prostu podzbiór Framefunkcjonalności. W drugim IEncoderFrameznajduje się adapter. W trzecim IEncoderFramejest podział na Encoderfunkcjonalność. Nie ma znaczenia, czy Twoje potrzeby zmienią się między tymi trzema sytuacjami: interfejs API pozostaje taki sam.

ToolmakerSteve
źródło
Łączenie klasy z interfejsem zwróconym przez jednego z jej współpracowników, a nie konkretną klasą, podczas gdy ulepszenie jest nadal źródłem sprzężenia. Jeśli interfejs musi się zmienić, oznacza to, że twoja klasa będzie musiała się zmienić. Ważną kwestią do usunięcia jest to, że sprzężenie nie jest z natury złe, o ile upewnisz się, że unikniesz niepotrzebnego łączenia z niestabilnymi obiektami lub strukturami wewnętrznymi . Właśnie dlatego Prawo Demetera należy przyjąć z dużą szczyptą soli; instruuje cię, abyś zawsze unikał czegoś, co może, ale nie musi stanowić problemu, w zależności od okoliczności.
Periata Breatta
@PeriataBreatta - z pewnością nie mogę i nie zgadzam się z tym. Ale chciałbym podkreślić jedną rzecz: AN interfejs , z definicji, reprezentuje to, co musi być znany na granicy między tymi dwoma klasami. Jeśli „musi się zmienić”, ma to fundamentalne znaczenie - żadne alternatywne podejście nie zdołałoby w magiczny sposób uniknąć wymaganego kodowania. Porównaj to z wykonaniem którejkolwiek z trzech opisanych przeze mnie sytuacji, ale bez używania interfejsu - zamiast tego zwróć konkretną klasę. W 1, Frame, w 2, EncoderFrameWrapper, w 3, Encoder. Zamkniesz się w tym podejściu. Interfejs może się do nich dostosować.
ToolmakerSteve,
@PeriataBreatta ... co pokazuje korzyść z jawnego zdefiniowania go jako interfejsu. Mam nadzieję, że w końcu udoskonalę IDE, aby uczynić to tak wygodnym, że interfejsy będą używane o wiele intensywniej. Większość dostępów wielopoziomowych odbywa się za pośrednictwem interfejsu, dlatego zarządzanie zmianami jest znacznie łatwiejsze. (Jeśli w niektórych przypadkach jest to „przesada”, analiza kodu w połączeniu z adnotacjami o tym, gdzie jesteśmy gotowi podjąć ryzyko braku interfejsu w zamian za niewielki wzrost wydajności, może „skompilować” - zastępując go jedna z tych 3 konkretnych klas z 3 „rozwiązań”.)
ToolmakerSteve
@PeriataBreatta - a kiedy mówię „interfejs może się do nich dostosować”, nie mówię „ahhhh, to wspaniale, że ta jedna funkcja języka może obejmować te różne przypadki”. Mówię, że definiowanie interfejsów minimalizuje zmiany, których możesz potrzebować. W najlepszym przypadku projekt może zmienić się od najprostszego przypadku (rozwiązanie 1) do najtrudniejszego przypadku (rozwiązanie 3) bez zmiany interfejsu - tylko potrzeby wewnętrzne producenta stały się bardziej złożone. I nawet gdy potrzebne są zmiany, są one mniej rozpowszechnione, IMHO.
ToolmakerSteve,