Czyż nie są też „głębokie hierarchie kompozycji”?


19

Przepraszam, jeśli „Hierarchia składu” nie jest rzeczą, ale wyjaśnię, co mam na myśli w pytaniu.

Nie ma programisty OO, który nie spotkałby się z odmianą „Zachowaj płaskie hierarchie dziedziczenia” lub „Preferuj kompozycję nad dziedziczeniem” i tak dalej. Problematyczne są jednak również głębokie hierarchie kompozycji.

Załóżmy, że potrzebujemy zbioru raportów szczegółowo opisujących wyniki eksperymentu:

class Model {
    // ... interface

    Array<Result> m_results;
}

Każdy wynik ma określone właściwości. Obejmują one czas eksperymentu, a także niektóre metadane z każdego etapu eksperymentu:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

    Epoch m_epoch;
    Map<Stage, ExperimentModules> m_modules; 
}

Ok świetnie. Teraz każdy moduł eksperymentu ma ciąg opisujący wynik eksperymentu, a także zbiór odniesień do eksperymentalnych zestawów próbek:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

A potem każda próbka ma ... no cóż, dostajesz obraz.

Problem polega na tym, że jeśli modeluję obiekty z mojej domeny aplikacji, wydaje się to bardzo naturalne dopasowanie, ale pod koniec dnia jest Resultto po prostu głupi kontener danych! Nie wydaje się opłacalne tworzenie dla niego dużej grupy klas.

Zakładając, że struktury danych i klasy pokazane powyżej poprawnie modelują relacje w domenie aplikacji, czy istnieje lepszy sposób na modelowanie takiego „wyniku” bez uciekania się do głębokiej hierarchii kompozycji? Czy istnieje jakiś kontekst zewnętrzny, który pomógłby ustalić, czy taki projekt jest dobry, czy nie?


Nie rozumiem części kontekstu zewnętrznego.
Tulains Córdova

@ TulainsCórdova, o co tu pytam, „czy są sytuacje, w których głęboka kompozycja jest dobrym rozwiązaniem”?
AwesomeSauce,

Dodałem odpowiedź.
Tulains Córdova

5
Tak, dokładnie. Problemy, które ludzie próbują rozwiązać, zastępując dziedziczenie kompozycją, nie znikają magicznie tylko dlatego, że zmieniłeś jedną strategię agregacji cech na inną. Oba są rozwiązaniami do zarządzania złożonością, a złożoność ta wciąż istnieje i należy ją jakoś rozwiązać.
Mason Wheeler,

3
Nie widzę nic złego w modelach danych głębokich. Ale nie rozumiem też, jak można to modelować z dziedziczeniem, ponieważ jest to relacja 1: N na każdej warstwie.
usr

Odpowiedzi:


17

Na tym właśnie polega Prawo Demetera / zasada najmniejszej wiedzy . Użytkownik Modelniekoniecznie musi wiedzieć o ExperimentalModulesinterfejsie, aby wykonywać swoją pracę.

Ogólny problem polega na tym, że gdy klasa dziedziczy od innego typu lub ma pole publiczne z pewnym typem lub gdy metoda przyjmuje typ jako parametr lub zwraca typ, interfejsy wszystkich tych typów stają się częścią efektywnego interfejsu mojej klasy. Powstaje pytanie: od tych typów, na których jestem zależny: czy używa się ich w całej mojej klasie? A może są to tylko szczegóły implementacji, które przypadkowo ujawniłem? Ponieważ jeśli są to szczegóły implementacji, właśnie je ujawniłem i pozwoliłem użytkownikom mojej klasy na nie polegać - moi klienci są teraz ściśle powiązani z moimi szczegółami implementacji.

Więc jeśli chcę poprawić spójność, mogę ograniczyć to połączenie, pisząc więcej metod, które robią użyteczne rzeczy, zamiast wymagać od użytkowników sięgania do moich prywatnych struktur. Tutaj możemy zaoferować metody wyszukiwania lub filtrowania wyników. Dzięki temu moja klasa jest łatwiejsza do refaktoryzacji, ponieważ mogę teraz zmienić wewnętrzną reprezentację bez przerywania zmiany interfejsu.

Ale nie jest to bez kosztów - interfejs mojej wystawionej klasy staje się teraz rozdęty operacjami, które nie zawsze są potrzebne. Narusza to zasadę segregacji interfejsu: nie powinienem zmuszać użytkowników do polegania na większej liczbie operacji, niż faktycznie używają. I tu ważne jest projektowanie : Projektowanie polega na decydowaniu, która zasada jest ważniejsza w twoim kontekście, i na znalezieniu odpowiedniego kompromisu.

Projektując bibliotekę, która musi zachowywać kompatybilność ze źródłami w wielu wersjach, byłbym bardziej skłonny do dokładniejszego przestrzegania zasady najmniejszej wiedzy. Jeśli moja biblioteka korzysta z innej biblioteki wewnętrznie, nigdy nie ujawniłbym użytkownikom niczego zdefiniowanego przez tę bibliotekę. Gdybym musiał udostępnić interfejs z biblioteki wewnętrznej, stworzyłbym prosty obiekt opakowujący. Wzory projektowe, takie jak wzór mostu lub wzór elewacji, mogą w tym pomóc.

Ale jeśli moje interfejsy są używane tylko wewnętrznie lub jeśli interfejsy, które ujawnię, są bardzo podstawowe (część standardowej biblioteki lub biblioteki tak podstawowej, że nigdy nie ma sensu jej zmieniać), ukrywanie jej może być bezużyteczne te interfejsy. Jak zwykle nie ma jednoznacznych odpowiedzi na wszystkie pytania.


Wspomniał pan o mojej głównej trosce; pomysł, że musiałem sięgnąć daleko w proces wdrażania, aby zrobić coś użytecznego na wyższych poziomach abstrakcji. Bardzo przydatna dyskusja, dzięki.
AwesomeSauce,

7

Czyż nie są też „głębokie hierarchie kompozycji”?

Oczywiście. Reguła powinna w rzeczywistości brzmieć „utrzymuj wszystkie hierarchie tak płaskie, jak to możliwe”.

Ale głębokie hierarchie kompozycji są mniej kruche niż hierarchie głębokiego dziedziczenia i są bardziej elastyczne do zmiany. Intuicyjnie nie powinno to dziwić; hierarchie rodzinne są takie same. Twój związek z krewnymi jest ustalony w genetyce; twoje relacje z przyjaciółmi i małżonkiem nie są.

Twój przykład nie wydaje mi się „głęboką hierarchią”. Forma dziedziczy z prostokąta, który dziedziczy z zestawu punktów, który dziedziczy ze współrzędnych x i y, a ja jeszcze nie mam pełnej siły pary.


4

Parafrazując Einsteina:

wszystkie hierarchie powinny być możliwie jak najbardziej płaskie, ale nie bardziej płaskie

Oznacza to, że jeśli modelujesz taki problem, nie powinieneś mieć wątpliwości co do modelowania go takim, jakim jest.

Jeśli aplikacja była tylko gigantycznym arkuszem kalkulacyjnym, nie musisz modelować hierarchii kompozycji. W przeciwnym razie zrób to. Nie sądzę, że rzecz jest nieskończenie głęboka. Po prostu trzymaj pobierających, którzy zwracają kolekcje, leniwie, jeśli zapełnianie takich kolekcji jest drogie, jak proszenie ich o repozytorium, które wyciąga je z bazy danych. Mam na myśli leniwe, nie wypełniaj ich, dopóki nie będą potrzebne.


1

Tak, możesz go modelować jak tabele relacyjne

Result {
    Id
    ModelId
    Epoch
}

itp., co często może prowadzić do łatwiejszego programowania i prostego mapowania do bazy danych. ale kosztem utraty twardego kodowania waszych związków


0

Naprawdę nie wiem, jak to rozwiązać w Javie, ponieważ Java jest zbudowana wokół idei, że wszystko jest przedmiotem. Nie można pomijać klas pośrednich, zastępując je słownikiem, ponieważ typy w obiektach blob danych są heterogeniczne (takie jak znacznik czasu + lista lub ciąg + lista ...). Alternatywa zastąpienia klasy kontenera jej polem (np. Przekazania zmiennej „m_epoch” przez „m_modules”) jest po prostu zła, ponieważ tworzy niejawny obiekt (nie można mieć go bez siebie) bez żadnego konkretnego zasiłek.

W moim języku predylekcyjnym (Python) moją podstawową zasadą byłoby, aby nie tworzyć obiektu, jeśli nie należy do niego żadna szczególna logika (oprócz podstawowego pobierania i ustawiania). Ale biorąc pod uwagę twoje ograniczenia, myślę, że twoja hierarchia kompozycji jest najlepszym możliwym projektem.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.