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ę
addTrack
dlaAlbum
klasy, 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:
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) .
Jeśli
addTrack
jest to metoda interfejsu, a wymagania musząTrack
zawierać więcej informacji (powiedzmy rok lub gatunek), należy zmienić interfejs, aby metoda obsługiwała inny parametr.Kapsułkowanie jest zepsute; jeśli
addTrack
jest w interfejsie, to nie powinien znać elementów wewnętrznychTrack
.W rzeczywistości jest bardziej sprzężony w drugi sposób, z wieloma parametrami. Załóżmy, że
no
parametr należy zmienić zint
na,long
ponieważ istnieje więcej niżMAX_INT
ścieżki (lub z dowolnego powodu); wtedy zarównoTrack
metoda, jak i metoda muszą zostać zmienione, a jeśli metoda byłabyaddTrack(Track track)
tylkoTrack
zmieniona.
Wszystkie 4 argumenty są faktycznie ze sobą powiązane, a niektóre z nich są konsekwencjami innych.
Które podejście jest lepsze?
Odpowiedzi:
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
Track
nie 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
Track
klasę, 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
Playlist
obiekt. WprowadzenieTrack
obiektu 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ć
NetworkTrack
obiekt zgodny z tym samym interfejsem. BezTrack
obiektu musiałbyś tworzyć funkcje wszędzie takie jak: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.
źródło
Moja rekomendacja to:
Posługiwać się
ale upewnij się, że
ITrack
jest to interfejs, a nie konkretna klasa.Album nie zna wewnętrznych elementów
ITrack
implementujących. Jest to powiązane jedynie z umową określoną przezITrack
.Myślę, że jest to rozwiązanie, które generuje najmniejszą ilość sprzężenia.
źródło
Track
tego, czy jesteś głupi czy mądry.Track
jest konkrecją.ITrack
interfejs 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 zgodneITrack
.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.
źródło
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
Track
należy tego wiedzieć?Korzystając
Track
z przekazanej klasyAlbum
, 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.
Album
musi znać elementy wewnętrzneTrack
, a jeśli nie używałeś obiektu,Album
musiał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ętrzneTrack
, ponieważ musi skonstruowaćTrack
obiekt, 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żAlbum
musi ona korzystać zTrack
informacji tak samo.Nie chcesz używać,
Track
jeśliTrack
zawiera wewnętrzną logikę, do której nie chcesz, aby osoba dzwoniąca miała dostęp. Innymi słowy, gdybyAlbum
była to klasa, z której miałby korzystać programista korzystający z biblioteki, nie chciałbyś, aby używał jejTrack
, 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ć
Track
komponenty interfejsu i komponenty logiczne, tworząc dwie osobne klasy. Dla dzwoniącegoTrack
staje 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śAlbum
klasy o nazwieTrackDAO
do wykonywania ciężkich operacji podnoszenia związanych z zapisywaniem informacji zTrack
bazy 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
Track
poczucia winy. Pamiętaj tylko, aby pamiętać o swoim rozmówcy podczas tworzenia klas i tworzyć interfejsy w razie potrzeby.źródło
Obydwa są prawidłowe
jest lepszy (jak już argumentowałeś)
jest mniej sprzężony, ponieważ używany kod
addTrack
nie musi wiedzieć, że istniejeTrack
klasa. 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.
źródło
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.
źródło