Łączenie metod a hermetyzacja

17

Istnieje klasyczny problem OOP łączenia łańcuchów metod z metodami „pojedynczego punktu dostępu”:

main.getA().getB().getC().transmogrify(x, y)

vs

main.getA().transmogrifyMyC(x, y)

Pierwszy wydaje się mieć tę zaletę, że każda klasa odpowiada tylko za mniejszy zestaw operacji i czyni wszystko o wiele bardziej modułowym - dodanie metody do C nie wymaga żadnego wysiłku w A, B lub C, aby ją ujawnić.

Minusem jest oczywiście słabsza enkapsulacja , którą rozwiązuje drugi kod. Teraz A ma kontrolę nad każdą metodą, która przez nią przechodzi, i może delegować ją do swoich pól, jeśli chce.

Zdaję sobie sprawę, że nie ma jednego rozwiązania i oczywiście zależy to od kontekstu, ale naprawdę chciałbym usłyszeć jakieś uwagi na temat innych ważnych różnic między tymi dwoma stylami i pod jakimi warunkami powinienem preferować którykolwiek z nich - ponieważ teraz, kiedy próbuję aby zaprojektować jakiś kod, wydaje mi się, że po prostu nie używam argumentów do decydowania w ten czy inny sposób.

Dąb
źródło

Odpowiedzi:

25

Myślę, że Prawo Demetera stanowi ważną wskazówkę w tym zakresie (z jego zaletami i wadami, które, jak zwykle, powinny być mierzone dla poszczególnych przypadków).

Zaletą przestrzegania Prawa Demetera jest to, że powstałe oprogramowanie jest łatwiejsze w utrzymaniu i adaptacji. Ponieważ obiekty są mniej zależne od wewnętrznej struktury innych obiektów, kontenery obiektów można zmieniać bez przerabiania ich obiektów wywołujących.

Wadą prawa Demetera jest to, że czasem wymaga napisania dużej liczby małych metod „otoki” w celu propagowania wywołań metod do komponentów. Ponadto interfejs klasy może stać się nieporęczny, ponieważ obsługuje metody klas zamkniętych, co powoduje, że klasa nie ma spójnego interfejsu. Ale może to być również znak złego projektu OO.

Péter Török
źródło
Zapomniałem o tym prawie, dziękuję za przypomnienie. Ale co pytam tu głównie o to, co zalety i wady są, a dokładniej w jaki sposób należy zdecydować użyć jednego stylu nad drugim.
Dąb
@Oak, dodałem cytaty opisujące zalety i wady.
Péter Török
10

Zasadniczo staram się ograniczyć metodę łączenia łańcuchów (na podstawie Prawa Demetera )

Jedyny wyjątek, jaki robię, to płynne interfejsy / wewnętrzne programowanie w stylu DSL.

Martin Fowler dokonuje tego samego rozróżnienia w językach specyficznych dla domeny, ale z powodu naruszenia separacji zapytań polecenia, która stwierdza:

że każda metoda powinna być albo komendą wykonującą akcję, albo zapytaniem zwracającym dane do programu wywołującego, ale nie jednym i drugim.

Fowler w swojej książce na stronie 70 mówi:

Separacja poleceń i zapytań jest niezwykle cenną zasadą w programowaniu i gorąco zachęcam zespoły do ​​korzystania z niej. Jedną z konsekwencji użycia Łańcucha metod w wewnętrznych DSL jest to, że zwykle łamie tę zasadę - każda metoda zmienia stan, ale zwraca obiekt, aby kontynuować łańcuch. Użyłem wielu decybeli dyskredytujących ludzi, którzy nie przestrzegają rozdzielania zapytań i zrobią to ponownie. Ale płynne interfejsy są zgodne z innym zestawem zasad, dlatego cieszę się, że mogę na to pozwolić.

KeesDijk
źródło
3

Myślę, że pytanie brzmi, czy używasz odpowiedniej abstrakcji.

W pierwszym przypadku mamy

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

Gdzie główny jest typu IHasGetA. Pytanie brzmi: czy ta abstrakcja jest odpowiednia. Odpowiedź nie jest trywialna. I w tym przypadku wygląda to trochę nie tak, ale i tak jest to teoretyczny przykład. Ale skonstruować inny przykład:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

Jest często lepszy niż

main.superTransmogrify(v, w, x, y, z);

Ponieważ w drugim przykładzie zarówno thisi mainzależy od rodzaju v, w, x, yi z. Ponadto kod nie wygląda tak naprawdę lepiej, jeśli każda deklaracja metody zawiera pół tuzina argumentów.

Lokalizator usług faktycznie wymaga pierwszego podejścia. Nie chcesz uzyskać dostępu do instancji, którą tworzy za pośrednictwem lokalizatora usług.

Tak więc „sięgnięcie” przez obiekt może stworzyć wiele zależności, tym bardziej, jeśli jest ono oparte na właściwościach rzeczywistych klas.
Jednak tworzenie abstrakcji, polegające na zapewnieniu obiektu, jest czymś zupełnie innym.

Na przykład możesz mieć:

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

Gdzie mainjest przykład Main. Jeśli znajomość klasy mainzmniejszy zależność do IHasGetAzamiast Main, przekonasz się, że sprzężenie faktycznie jest dość niskie. Kod wywołujący nawet nie wie, że w rzeczywistości wywołuje ostatnią metodę na oryginalnym obiekcie, co faktycznie ilustruje stopień oddzielenia.
Sięgasz ścieżką zwięzłych i ortogonalnych abstrakcji, a nie w głąb implementacji.

back2dos
źródło
Bardzo interesujący punkt o dużym wzroście liczby parametrów.
Dąb
2

Prawo Demeter , jak @ Péter Török zaznacza, sugeruje „Compact” formę.

Ponadto, im więcej metod zostanie wyraźnie wspomnianych w kodzie, tym więcej klas zależy od klasy, co zwiększa problemy z konserwacją. W twoim przykładzie zwarta forma zależy od dwóch klas, podczas gdy dłuższa forma zależy od czterech klas. Dłuższa forma nie tylko narusza Prawo Demetera; spowoduje to również zmianę kodu za każdym razem, gdy zmienisz jedną z czterech metod, do których się odwołujesz (w przeciwieństwie do dwóch w zwartej formie).

CesarGon
źródło
Z drugiej strony ślepe przestrzeganie tego prawa oznacza, że ​​liczba metod Aeksploduje, a wiele metod i tak Amoże chcieć oddelegować. Mimo to zgadzam się z zależnościami - to drastycznie zmniejsza ilość zależności wymaganych od kodu klienta.
Dąb
1
@Oak: Robienie czegokolwiek na ślepo nigdy nie jest dobre. Trzeba patrzeć na zalety i wady i podejmować decyzje na podstawie dowodów. Dotyczy to również prawa Demetera.
CesarGon,
2

Sam zmagałem się z tym problemem. Wadą „sięgania” głęboko w różne obiekty jest to, że podczas refaktoryzacji będziesz musiał zmienić strasznie dużo kodu, ponieważ istnieje tak wiele zależności. Ponadto kod staje się nieco rozdęty i trudniejszy do odczytania.

Z drugiej strony posiadanie klas, które po prostu „przekazują” metody, oznacza również narzut związany z deklarowaniem wielu metod w więcej niż jednym miejscu.

Rozwiązaniem, które łagodzi to i jest odpowiednie w niektórych przypadkach, jest posiadanie klasy fabrycznej, która buduje swego rodzaju obiekt fasadowy poprzez kopiowanie danych / obiektów z odpowiednich klas. W ten sposób możesz zakodować obiekt fasadowy, a po refaktoryzacji wystarczy zmienić logikę fabryki.

Homde
źródło
1

Często stwierdzam, że logika programu jest łatwiejsza do opanowania za pomocą metod łańcuchowych. Dla mnie customer.getLastInvoice().itemCount()lepiej pasuje do mojego mózgu customer.countLastInvoiceItems().

Niezależnie od tego, czy jest to warte konserwacji, musisz mieć dodatkowe sprzęgło. (Lubię też małe funkcje w małych klasach, więc mam tendencję do tworzenia łańcuchów. Nie mówię, że to prawda - po prostu to robię.)

JT Grimes
źródło
powinien to być IMO customer.NrLastInvoices lub customer.LastInvoice.NrItems. Ten łańcuch nie jest zbyt długi, więc prawdopodobnie nie warto spłaszczyć, jeśli ilość kombinacji jest nieco duża
Homde