Luźne sprzężenie w projektowaniu obiektowym

16

Próbuję nauczyć się GRASP i znalazłem to wyjaśnione ( tutaj na stronie 3 ) o niskim sprzężeniu i byłem bardzo zaskoczony, gdy to znalazłem:

Rozważ metodę addTrackdla Albumklasy, dwie możliwe metody to:

addTrack( Track t )

i

addTrack( int no, String title, double duration )

Która metoda zmniejsza sprzężenie? Drugi tak, ponieważ klasa korzystająca z klasy Album nie musi znać klasy Track. Zasadniczo parametry metod powinny wykorzystywać typy podstawowe (int, char ...) i klasy z pakietów java. *.

Mam skłonność do tego. Uważam, że addTrack(Track t)jest lepszy niż addTrack(int no, String title, double duration)z różnych powodów:

  1. Zawsze lepiej jest, aby metoda miała jak najmniejszą liczbę parametrów (zgodnie z Clean Code wujka Boba brak lub jeden najlepiej, 2 w niektórych przypadkach i 3 w szczególnych przypadkach; więcej niż 3 wymaga refaktoryzacji - są to oczywiście zalecenia, a nie holly reguły) .

  2. Jeśli addTrackjest to metoda interfejsu, a wymagania muszą Trackzawierać więcej informacji (powiedzmy rok lub gatunek), należy zmienić interfejs, aby metoda obsługiwała inny parametr.

  3. Kapsułkowanie jest zepsute; jeśli addTrackjest w interfejsie, to nie powinien znać elementów wewnętrznych Track.

  4. W rzeczywistości jest bardziej sprzężony w drugi sposób, z wieloma parametrami. Załóżmy, że noparametr należy zmienić z intna, longponieważ istnieje więcej niż MAX_INTścieżki (lub z dowolnego powodu); wtedy zarówno Trackmetoda, jak i metoda muszą zostać zmienione, a jeśli metoda byłaby addTrack(Track track)tylko Trackzmieniona.

Wszystkie 4 argumenty są faktycznie ze sobą powiązane, a niektóre z nich są konsekwencjami innych.

Które podejście jest lepsze?

m3th0dman
źródło
2
Czy to dokument złożony przez profesora lub trenera? Na podstawie adresu URL linku, który podałeś, wygląda na to, że była to klasa, choć w dokumencie nie widzę uznania, kto go utworzył. Jeśli była to część zajęć, sugeruję zadać te pytania osobie, która dostarczyła dokument. Nawiasem mówiąc, zgadzam się z twoim rozumowaniem - wydaje mi się oczywiste, że klasa Albumu z natury chciałaby wiedzieć o klasie Track.
Derek
Szczerze mówiąc, za każdym razem, gdy czytam o „najlepszych praktykach”, biorę je z odrobiną soli!
AraK
@Derek Znalazłem dokument, wyszukując w Google „przykład wzorców chwytania”; Nie wiem, kto to napisał, ale ponieważ pochodzi z uniwersytetu, uważam, że jest wiarygodny. Szukam przykładu na podstawie podanych informacji i zignorowania źródła.
m3th0dman
4
@ m3th0dman „ale ponieważ pochodzi z uniwersytetu, uważam, że jest wiarygodny”. Dla mnie, ponieważ pochodzi z uniwersytetu, uważam to za niewiarygodne. Nie ufam komuś, kto nie pracował nad wieloletnimi projektami, mówiąc o najlepszych praktykach w tworzeniu oprogramowania.
AraK
1
@AraK Rzetelny nie oznacza niekwestionowanego; i to właśnie robię tutaj, kwestionując to.
m3th0dman

Odpowiedzi:

15

Cóż, twoje pierwsze trzy punkty dotyczą właściwie innych zasad niż łączenie. Zawsze musisz znaleźć równowagę między często sprzecznymi zasadami projektowania.

Twój czwarty punkt jest o sprzężenie, a ja zdecydowanie zgadzam się z tobą. Sprzężenie dotyczy przepływu danych między modułami. Typ kontenera, w którym przepływają dane, jest w dużej mierze nieistotny. Przekazanie czasu trwania podwójnie zamiast pola a Tracknie eliminuje potrzeby jego zaliczania. Moduły nadal muszą współdzielić tę samą ilość danych i nadal mieć taką samą ilość sprzężenia.

Nie bierze również pod uwagę całego sprzężenia w systemie jako agregatu. Wprowadzając Trackklasę, co prawda dodaje kolejną zależność między dwoma poszczególnymi modułami, może znacznie zmniejszyć sprzężenie systemu , co jest tutaj ważnym środkiem.

Rozważmy na przykład przycisk „Dodaj do listy odtwarzania” i Playlistobiekt. Wprowadzenie Trackobiektu można uznać za zwiększenie sprzężenia, jeśli weźmie się pod uwagę tylko te dwa obiekty. Teraz masz trzy współzależne klasy zamiast dwóch. To jednak nie jest cały twój system. Musisz także zaimportować ścieżkę, odtworzyć ścieżkę, wyświetlić ścieżkę itp. Dodanie jeszcze jednej klasy do tego miksu jest znikome.

Teraz rozważ potrzebę dodania obsługi odtwarzania utworów przez sieć zamiast tylko lokalnie. Musisz tylko utworzyć NetworkTrackobiekt zgodny z tym samym interfejsem. Bez Trackobiektu musiałbyś tworzyć funkcje wszędzie takie jak:

addNetworkTrack(int no, string title, double duration, URL location)

To skutecznie podwaja twoje sprzężenie, wymagając nawet modułów, które nie dbają o rzeczy specyficzne dla sieci, aby mimo to nadal je śledzić, aby móc je przekazać.

Twój test efektu tętnienia jest dobry do ustalenia prawdziwej ilości sprzężenia. Naszym celem jest ograniczenie miejsc, na które wpływa zmiana.

Karl Bielefeldt
źródło
1
+ Sprzężenie z prymitywami nadal sprzęga się bez względu na to, jak jest krojone.
JustinC
+1 za wzmiankę o dodaniu opcji adresu URL / efektu tętnienia.
user949300,
4
+1 Ciekawym przeczytaniem na ten temat byłaby również dyskusja na temat zasady inwersji zależności w DIP na wolności, w której użycie typów pierwotnych jest faktycznie postrzegane jako „zapach” pierwotnej obsesji z poprawką Object Value . Dla mnie to brzmi jak lepiej byłoby przekazać obiekt Track, który cały zespół prymitywnych typów ... A jeśli chcesz uniknąć zależności od / łączenia się z konkretnymi klasami, użyj interfejsów.
Marjan Venema
Odpowiedź zaakceptowana z powodu miłego wyjaśnienia różnicy między sprzężeniem całego systemu a sprzężeniem modułów.
m3th0dman
10

Moja rekomendacja to:

Posługiwać się

addTrack( ITrack t )

ale upewnij się, że ITrackjest to interfejs, a nie konkretna klasa.

Album nie zna wewnętrznych elementów ITrackimplementujących. Jest to powiązane jedynie z umową określoną przez ITrack.

Myślę, że jest to rozwiązanie, które generuje najmniejszą ilość sprzężenia.

Tulains Córdova
źródło
1
Uważam, że Track to po prostu prosty obiekt służący do przesyłania danych typu bean / data, w którym są tylko pola i obiekty pobierające / ustawiające; czy w tym przypadku wymagany jest interfejs?
m3th0dman
6
Wymagany? Prawdopodobnie nie. Sugestywne, tak. Konkretne znaczenie toru może i będzie ewoluować, ale to, czego wymaga klasa konsumpcyjna, prawdopodobnie nie będzie.
JustinC
2
@ m3th0dman Zawsze polegaj na abstrakcjach, a nie na konkrecjach. Dotyczy to niezależnie od Tracktego, czy jesteś głupi czy mądry. Trackjest konkrecją. ITrackinterfejs jest abstrakcją. W ten sposób będziesz w stanie mieć różne rodzaje Torów w przyszłości, o ile będą one zgodne ITrack.
Tulains Córdova
4
Zgadzam się z tym pomysłem, ale gubię przedrostek „ja”. Z Clean Code autorstwa Roberta Martina, strona 24: „Poprzednia ja, tak powszechna w dzisiejszych starszych wersjach, jest w najlepszym razie rozproszeniem i zbyt dużą ilością informacji. Nie chcę, aby użytkownicy wiedzieli, że wręczam im berło."
Benjamin Brumfield
1
@BenjaminBrumfield Masz rację. Prefiks też mi się nie podoba, chociaż zostawiam odpowiedź w celu zachowania przejrzystości.
Tulains Córdova
4

Argumentowałbym, że druga przykładowa metoda najprawdopodobniej zwiększa sprzężenie, ponieważ najprawdopodobniej tworzy instancję obiektu Track i przechowuje go w bieżącym obiekcie Album. (Jak zasugerowałem w moim komentarzu powyżej, zakładam, że nieodłączną cechą klasy Albumu jest koncepcja klasy Track gdzieś w niej).

Pierwsza przykładowa metoda zakłada, że ​​instancja ścieżki jest tworzona poza klasą albumu, więc przynajmniej możemy założyć, że instancja klasy ścieżki nie jest sprzężona z klasą albumu.

Jeśli najlepsze praktyki sugerują, że nigdy nie mamy odwołania do jednej klasy do drugiej klasy, całe programowanie obiektowe zostanie wyrzucone przez okno.

Derek
źródło
Nie rozumiem, w jaki sposób niejawne odwołanie do innej klasy sprawia, że ​​jest ono bardziej sprzężone niż posiadanie wyraźnego odwołania. Tak czy inaczej, obie klasy są połączone. Wydaje mi się, że lepiej jest, aby sprzężenie było jawne, ale nie sądzę, aby było to bardziej „sprzężone” w jakikolwiek sposób.
TMN
1
@TMN, dodatkowe sprzężenie polega na tym, że sugeruję, iż drugi przykład prawdopodobnie skończyłby wewnętrznie tworzeniem nowego obiektu Track. Instancja obiektu jest sprzężona z metodą, która w przeciwnym razie powinna po prostu dodać obiekt Track do jakiejś listy w obiekcie Album (łamanie zasady pojedynczej odpowiedzialności). Gdyby kiedykolwiek trzeba było zmienić sposób tworzenia Śledzenia, należałoby również zmienić metodę addTrack (). Tak nie jest w przypadku pierwszego przykładu.
Derek
3

Sprzęganie jest tylko jednym z wielu aspektów, które można uzyskać w kodzie. Zmniejszając sprzężenie, niekoniecznie poprawiasz swój program. Zasadniczo jest to najlepsza praktyka, ale w tym konkretnym przypadku, dlaczego nie Tracknależy tego wiedzieć?

Korzystając Trackz przekazanej klasy Album, ułatwiasz czytanie kodu, ale co ważniejsze, jak wspomniałeś, zmieniasz statyczną listę parametrów w obiekt dynamiczny. To ostatecznie sprawia, że ​​interfejs jest znacznie bardziej dynamiczny.

Wspominasz, że enkapsulacja jest zepsuta, ale tak nie jest. Albummusi znać elementy wewnętrzne Track, a jeśli nie używałeś obiektu, Albummusiałby znać każdą przekazywaną mu informację, zanim będzie mógł z niej korzystać. Program wywołujący musi również znać elementy wewnętrzne Track, ponieważ musi skonstruować Trackobiekt, ale program wywołujący musi znać te same informacje, jeśli zostały przekazane bezpośrednio do metody. Innymi słowy, jeśli zaletą enkapsulacji nie jest znajomość zawartości obiektu, nie można jej użyć w tym przypadku, ponieważ Albummusi ona korzystać z Trackinformacji tak samo.

Nie chcesz używać, Trackjeśli Trackzawiera wewnętrzną logikę, do której nie chcesz, aby osoba dzwoniąca miała dostęp. Innymi słowy, gdyby Albumbyła to klasa, z której miałby korzystać programista korzystający z biblioteki, nie chciałbyś, aby używał jej Track, gdybyś użył jej do powiedzenia: wywołaj metodę, aby zachować ją w bazie danych. Prawdziwy problem polega na tym, że interfejs jest zaplątany w model.

Aby rozwiązać problem, należy rozdzielić Trackkomponenty interfejsu i komponenty logiczne, tworząc dwie osobne klasy. Dla dzwoniącego Trackstaje się lekką klasą, która ma przechowywać informacje i oferować drobne optymalizacje (dane obliczeniowe i / lub wartości domyślne). Wewnątrz użyłbyś Albumklasy o nazwie TrackDAOdo wykonywania ciężkich operacji podnoszenia związanych z zapisywaniem informacji z Trackbazy danych.

Oczywiście to tylko przykład. Jestem pewien, że w ogóle nie jest to twój przypadek, więc nie krępuj się i użyj Trackpoczucia winy. Pamiętaj tylko, aby pamiętać o swoim rozmówcy podczas tworzenia klas i tworzyć interfejsy w razie potrzeby.

Neil
źródło
3

Obydwa są prawidłowe

addTrack( Track t ) 

jest lepszy (jak już argumentowałeś)

addTrack( int no, String title, double duration ) 

jest mniej sprzężony, ponieważ używany kod addTracknie musi wiedzieć, że istnieje Trackklasa. Nazwę ścieżki można zmienić na przykład bez konieczności aktualizacji kodu wywołującego.

Podczas gdy mówisz o bardziej czytelnym / łatwym do utrzymania kodzie, artykuł mówi o sprzęganiu . Mniej sprzężony kod niekoniecznie jest łatwiejszy do wdrożenia i zrozumienia.

k3b
źródło
Zobacz argument 4; Nie rozumiem, jak ten drugi jest mniej sprzężony.
m3th0dman
3

Niski poziom sprzężenia nie oznacza braku sprzężenia. Coś gdzieś musi wiedzieć o obiektach gdzie indziej w bazie kodu, a im bardziej zmniejszasz zależność od „niestandardowych” obiektów, tym więcej powodów podajesz dla zmiany kodu. To, co autor, którego cytujesz, promuje za pomocą drugiej funkcji, jest mniej sprzężone, ale także mniej zorientowane obiektowo, co jest sprzeczne z całą ideą GRASP jako metodologii projektowania obiektowego . Chodzi o to, jak zaprojektować system jako zbiór obiektów i ich interakcji; unikanie ich jest jak nauczanie prowadzenia samochodu, mówiąc, że zamiast tego należy jeździć na rowerze.

Zamiast tego właściwą drogą jest zmniejszenie zależności od konkretnych obiektów, co jest teorią „luźnego sprzężenia”. Im mniej określonych rodzajów betonu musi znać dana metoda, tym lepiej. Tylko przez to stwierdzenie pierwsza opcja jest w rzeczywistości mniej sprzężona, ponieważ druga metoda przyjmująca prostsze typy musi wiedzieć o wszystkich tych prostszych typach. Pewnie, że są one wbudowane, a kod w metodzie może wymagać opieki, ale podpis metody i wywołujący metodę zdecydowanie nie . Zmiana jednego z tych parametrów odnoszących się do koncepcyjnej ścieżki dźwiękowej będzie wymagała więcej zmian, gdy są one oddzielne, w porównaniu z ich zawartością w obiekcie Track (który jest punktem obiektów; enkapsulacja).

Idąc o krok dalej, gdyby oczekiwano, że Track zostanie zastąpiony czymś, co lepiej wykonuje tę samą pracę, być może interfejs określający wymaganą funkcjonalność byłby w porządku, ITrack. Mogłoby to pozwolić na różne implementacje, takie jak „AnalogTrack”, „CdTrack” i „Mp3Track”, które zapewniły dodatkowe informacje bardziej specyficzne dla tych formatów, a jednocześnie zapewniłyby podstawową ekspozycję danych ITrack, która koncepcyjnie reprezentuje „ścieżkę”; skończony fragment dźwięku. Track może podobnie być abstrakcyjną klasą podstawową, ale wymaga to zawsze użycia implementacji nieodłącznie związanej z Track; zaimplementuj go jako BetterTrack, a teraz musisz zmienić oczekiwane parametry.

Zatem złota zasada; programy i ich składniki kodu zawsze będą miały powody do zmiany. Nie możesz napisać programu, który nigdy nie będzie wymagał edycji kodu, który już napisałeś, aby dodać coś nowego lub zmodyfikować jego zachowanie. Twoim celem, w dowolnej metodologii (GRASP, SOLID, innym akronimie lub modzie, o której możesz pomyśleć), jest po prostu zidentyfikowanie rzeczy, które będą musiały się zmieniać w czasie, i zaprojektowanie systemu tak, aby zmiany te były jak najłatwiejsze do wprowadzenia (przetłumaczone; dotykanie jak najmniejszej liczby wierszy kodu i wpływanie na jak najmniejszą liczbę innych obszarów systemu poza zakres zamierzonej zmiany, jak to możliwe). W tym przypadku najbardziej prawdopodobne jest to, że ślad zyska więcej członków danych, o które addTrack () może lub nie musi dbać, a nie ten tor zostanie zastąpiony przez BetterTrack.

KeithS
źródło