Załóżmy na przykład, że masz aplikację o powszechnie udostępnianej klasie o nazwie User
. Ta klasa ujawnia wszystkie informacje o użytkowniku, jego identyfikatorze, nazwie, poziomach dostępu do każdego modułu, strefie czasowej itp.
Dane użytkownika są oczywiście szeroko przywoływane w całym systemie, ale z jakiegokolwiek powodu system jest skonfigurowany tak, aby zamiast przekazywać ten obiekt użytkownika do zależnych od niego klas, po prostu przekazujemy z niego poszczególne właściwości.
Klasa, która wymaga identyfikatora użytkownika, po prostu będzie wymagała GUID userId
jako parametru, czasami możemy również potrzebować nazwy użytkownika, aby była przekazywana jako osobny parametr. W niektórych przypadkach jest to przekazywane do poszczególnych metod, więc wartości nie są w ogóle utrzymywane na poziomie klasy.
Za każdym razem, gdy potrzebuję dostępu do innej informacji z klasy User, muszę wprowadzić zmiany, dodając parametry, a gdy dodanie nowego przeciążenia nie jest właściwe, muszę zmienić każde odwołanie do metody lub konstruktora klasy.
Użytkownik jest tylko jednym przykładem. Jest to powszechnie praktykowane w naszym kodzie.
Czy mam rację, sądząc, że jest to naruszenie zasady otwartej / zamkniętej? Nie chodzi tylko o zmianę istniejących klas, ale przede wszystkim o ich utworzenie, aby w przyszłości najprawdopodobniej konieczne będą szeroko zakrojone zmiany?
Gdybyśmy tylko przeszli przez User
obiekt, mógłbym dokonać niewielkiej zmiany w klasie, z którą pracuję. Jeśli muszę dodać parametr, być może będę musiał wprowadzić dziesiątki zmian w odniesieniach do klasy.
Czy ta praktyka narusza jakieś inne zasady? Może inwersja zależności? Chociaż nie odwołujemy się do abstrakcji, istnieje tylko jeden rodzaj użytkownika, więc nie ma potrzeby posiadania interfejsu użytkownika.
Czy naruszane są inne zasady inne niż SOLID, takie jak podstawowe zasady programowania obronnego?
Czy mój konstruktor powinien wyglądać tak:
MyConstructor(GUID userid, String username)
Albo to:
MyConstructor(User theUser)
Opublikował:
Sugerowano, że odpowiedź na pytanie znajduje się w „Pass ID czy Object?”. To nie odpowiada na pytanie, w jaki sposób decyzja o przejściu w którąkolwiek stronę wpływa na próbę przestrzegania zasad SOLID, która stanowi sedno tego pytania.
I
inSOLID
?MyConstructor
w zasadzie mówi teraz: „PotrzebujęGuid
astring
”. Dlaczego więc nie mieć interfejsu zapewniającegoGuid
ai astring
, niechUser
implementuje ten interfejs i pozwalaMyConstructor
zależeć od instancji implementującej ten interfejs? A jeśli potrzebaMyConstructor
zmiany, zmień interfejs. - Pomogło mi bardzo pomyśleć o interfejsach, które „należałyby” do konsumenta, a nie do dostawcy . Pomyśl więc „jako konsument potrzebuję czegoś, co robi to i to” zamiast „jako dostawca mogę to zrobić i tamto”.Odpowiedzi:
Nie ma absolutnie nic złego w przekazywaniu całego
User
obiektu jako parametru. W rzeczywistości może to pomóc w wyjaśnieniu kodu i uczynić programistom bardziej oczywistym, co robi metoda, jeśli podpis metody wymagaUser
.Przekazywanie prostych typów danych jest przyjemne, dopóki nie znaczą one czegoś innego niż są. Rozważ ten przykład:
I przykładowe użycie:
Czy potrafisz dostrzec wadę? Kompilator nie może. Przekazywany „identyfikator użytkownika” jest tylko liczbą całkowitą. Nazywamy zmienną,
user
ale inicjujemy jej wartość zblogPostRepository
obiektu, który prawdopodobnie zwracaBlogPost
obiekty, a nieUser
obiekty - ale kod się kompiluje, co kończy się błędnym błędem czasu wykonywania.Rozważmy teraz ten zmieniony przykład:
Być może
Bar
metoda używa tylko „identyfikatora użytkownika”, ale podpis metody wymagaUser
obiektu. Wróćmy teraz do tego samego przykładowego użycia, co poprzednio, ale zmień go, aby przekazać „użytkownika” w:Teraz mamy błąd kompilatora.
blogPostRepository.Find
Metoda zwracaBlogPost
obiekt, który my nazywamy sprytnie „user”. Następnie przekazujemy tego „użytkownika” doBar
metody i natychmiast otrzymujemy błąd kompilatora, ponieważ nie możemy przekazaćBlogPost
metody do metody, która akceptujeUser
.System typów języka jest wykorzystywany do szybszego pisania poprawnego kodu i identyfikowania defektów w czasie kompilacji, a nie w czasie wykonywania.
Naprawdę, konieczność przefiltrowania dużej ilości kodu, ponieważ zmiany informacji o użytkowniku są jedynie objawem innych problemów. Przekazując cały
User
obiekt, zyskujesz powyższe korzyści, oprócz korzyści wynikających z braku konieczności refaktoryzacji wszystkich sygnatur metod, które akceptują informacje użytkownika, gdy coś sięUser
zmienia w klasie.źródło
Nie, to nie jest naruszenie tej zasady. Zasada ta dotyczy niezmieniania
User
w sposób wpływający na inne części kodu, które z niej korzystają. Twoje zmianyUser
mogą być takim naruszeniem, ale nie jest to powiązane.Nie. To, co opisujesz - tylko wstrzykiwanie wymaganych części obiektu użytkownika do każdej metody - jest odwrotne: to zwykła inwersja zależności.
Nie. To podejście jest całkowicie poprawnym sposobem kodowania. Nie narusza takich zasad.
Ale odwrócenie zależności jest tylko zasadą; to nie jest niezłomne prawo. A czysta DI może zwiększyć złożoność systemu. Jeśli okaże się, że tylko wstrzyknięcie potrzebnych wartości użytkownika do metod, zamiast przekazywania całego obiektu użytkownika do metody lub konstruktora, stwarza problemy, nie rób tego w ten sposób. Chodzi o uzyskanie równowagi między zasadami a pragmatyzmem.
Aby rozwiązać swój komentarz:
Częścią problemu jest to, że wyraźnie nie podoba ci się to podejście, zgodnie z komentarzem „niepotrzebnie [przejść] ...”. I to dość sprawiedliwe; nie ma tutaj właściwej odpowiedzi. Jeśli uznasz to za uciążliwe, nie rób tego w ten sposób.
Jednak w odniesieniu do zasady otwartej / zamkniętej, jeśli będziesz ściśle przestrzegał tej zasady, wówczas „... zmień wszystkie odniesienia do wszystkich pięciu istniejących metod ...” oznacza, że metody te zostały zmodyfikowane, kiedy powinny być zamknięty do modyfikacji. W rzeczywistości jednak zasada otwarta / zamknięta ma sens w przypadku publicznych interfejsów API, ale nie ma większego sensu w przypadku elementów wewnętrznych aplikacji.
Ale potem wędrujesz na terytorium YAGNI i nadal będzie ono prostopadłe do zasady. Jeśli masz metodę,
Foo
która przyjmuje nazwę użytkownika, a następnie chceszFoo
również wybrać datę urodzenia, zgodnie z zasadą, dodajesz nową metodę;Foo
pozostaje bez zmian. To znowu dobra praktyka dla publicznych interfejsów API, ale to nonsens dla wewnętrznego kodu.Jak wspomniano wcześniej, chodzi o równowagę i zdrowy rozsądek w każdej sytuacji. Jeśli parametry te często się zmieniają, to tak, użyj
User
bezpośrednio. Pozwoli ci to zaoszczędzić od opisywanych zmian na dużą skalę. Ale jeśli często się nie zmieniają, dobrym pomysłem jest przekazanie tylko tego, co jest potrzebne.źródło
User
instancję, a następnie zapytasz ten obiekt, aby uzyskać parametr, wówczas tylko częściowo odwracasz zależności; wciąż jest trochę pytań. Rzeczywista inwersja zależności to 100% „powiedz, nie pytaj”. Ale ma złożoną cenę.Tak, zmiana istniejącej funkcji stanowi naruszenie zasady otwartej / zamkniętej. Zmieniasz coś, co powinno zostać zamknięte z powodu zmiany wymagań. Lepszym projektem (aby nie zmieniać, gdy zmieniają się wymagania) byłoby przekazanie Użytkownikowi rzeczy, które powinny działać na użytkownikach.
Może to jednak naruszać zasadę segregacji interfejsów, ponieważ możesz przekazywać o wiele więcej informacji, niż funkcja musi wykonać.
Tak jak w przypadku większości rzeczy - to zależy .
Używając tylko nazwy użytkownika, pozwólmy, aby funkcja była bardziej elastyczna, pracując z nazwami użytkowników bez względu na to, skąd pochodzą i bez potrzeby tworzenia w pełni funkcjonalnego obiektu użytkownika. Zapewnia odporność na zmiany, jeśli uważasz, że zmieni się źródło danych.
Korzystanie z całego użytkownika czyni go bardziej zrozumiałym w zakresie użytkowania i zawiera mocniejszą umowę z dzwoniącymi. Zapewnia odporność na zmiany, jeśli uważasz, że potrzebna będzie większa liczba użytkowników.
źródło
User.find()
. W zasadzie nie powinno nawet byćUser.find
. Znalezienie użytkownika nigdy nie powinno być obowiązkiemUser
.User
z funkcją. Może to ma sens. Ale może funkcja powinna dbać tylko o nazwę użytkownika - a przekazywanie takich informacji, jak data dołączenia użytkownika lub adres jest nieprawidłowe.User
klasa nie powinna mieć. Może istnieć cośUserRepository
podobnego, który zajmuje się takimi rzeczami.Ten projekt jest zgodny ze wzorcem obiektu parametru . Rozwiązuje problemy wynikające z posiadania wielu parametrów w sygnaturze metody.
Nie. Zastosowanie tego wzorca włącza zasadę otwarcia / zamknięcia (OCP). Na przykład klasy pochodne
User
można podać jako parametr, który wywołuje inne zachowanie w klasie konsumującej.To może się zdarzyć. Pozwól mi wyjaśnić na podstawie zasad SOLID.
Zasada pojedynczej odpowiedzialności (SRP) może zostać naruszona, jeśli ma projekt, jak wyjaśniono:
Problem dotyczy wszystkich informacji . Jeśli
User
klasa ma wiele właściwości, staje się ogromnym obiektem transferu danych, który transportuje niepowiązane informacje z perspektywy klas konsumujących. Przykład: z punktu widzenia klasy konsumującejUserAuthentication
właściwośćUser.Id
iUser.Name
są istotne, ale nie istotneUser.Timezone
.Zasada Segregacja Interfejs (ISP) jest również naruszona z podobne rozumowanie, ale dodaje inną perspektywę. Przykład: załóżmy, że klasa konsumująca
UserManagement
wymagaUser.Name
podzielenia właściwościUser.LastName
i należy w tym celu zmodyfikowaćUser.FirstName
klasęUserAuthentication
.Na szczęście ISP daje również możliwość wyjścia z problemu: zwykle takie obiekty parametrów lub obiekty transportu danych zaczynają się od małych i rosną z czasem. Jeśli stanie się to niewygodne, rozważ następujące podejście: Wprowadź interfejsy dostosowane do potrzeb klas konsumujących. Przykład: Wprowadź interfejsy i pozwól, aby
User
klasa z niego wywodziła:Każdy interfejs powinien ujawniać podzbiór powiązanych właściwości
User
klasy potrzebnych do tego, aby klasa konsumująca wypełniła swoje działanie. Poszukaj klastrów właściwości. Spróbuj ponownie użyć interfejsów. W przypadku klasy konsumującejUserAuthentication
użyjIUserAuthenticationInfo
zamiastUser
. Następnie, jeśli to możliwe, podzielUser
klasę na wiele konkretnych klas, używając interfejsów jako „wzornika”.źródło
Kiedy skonfrontowałem się z tym problemem w moim własnym kodzie, doszedłem do wniosku, że podstawowe klasy / obiekty modelu są odpowiedzią.
Typowym przykładem może być wzorzec repozytorium. Często przy wyszukiwaniu bazy danych za pośrednictwem repozytoriów wiele metod w repozytorium wymaga wielu takich samych parametrów.
Moje podstawowe zasady dotyczące repozytoriów to:
Jeżeli więcej niż jedna metoda przyjmuje te same 2 lub więcej parametrów, parametry należy zgrupować razem jako obiekt modelowy.
Jeżeli metoda wymaga więcej niż 2 parametrów, parametry należy zgrupować razem jako obiekt modelu.
Modele mogą dziedziczyć po wspólnej podstawie, ale tylko wtedy, gdy naprawdę ma to sens (zwykle lepiej jest zrefaktoryzować później niż na początku dziedziczenia).
Problemy z użyciem modeli z innych warstw / obszarów nie ujawniają się, dopóki projekt nie stanie się trochę skomplikowany. Dopiero wtedy okaże się, że mniej kodu powoduje więcej pracy lub więcej komplikacji.
I tak, zupełnie dobrze jest mieć 2 różne modele o identycznych właściwościach, które służą różnym warstwom / celom (tj. ViewModels vs POCO).
źródło
Sprawdźmy tylko poszczególne aspekty SOLID:
Jedną z rzeczy, które mylą instynkty projektowe, jest to, że klasa jest zasadniczo dla obiektów globalnych i zasadniczo tylko do odczytu. W takiej sytuacji naruszanie abstrakcji nie zaszkodzi bardzo: Samo odczytanie niezmodyfikowanych danych tworzy dość słabe połączenie; dopiero gdy staje się ogromną kupą, ból staje się zauważalny.
Aby przywrócić instynkty projektowe, wystarczy założyć, że obiekt nie jest zbyt globalny. Jakiego kontekstu potrzebowałaby funkcja, gdyby
User
obiekt mógł zostać zmutowany w dowolnym momencie? Jakie elementy obiektu prawdopodobnie zostałyby zmutowane razem? Można je rozdzielićUser
, zarówno jako podobiektu odniesienia, jak i interfejsu, który wyświetla tylko „wycinek” powiązanych pól, nie jest tak ważne.Kolejna zasada: spójrz na funkcje, które używają części
User
i zobacz, które pola (atrybuty) zwykle idą w parze. To dobra wstępna lista podobiektów - zdecydowanie musisz się zastanowić, czy faktycznie należą do siebie.Jest to dużo pracy i jest trochę trudne do wykonania, a Twój kod stanie się nieco mniej elastyczny, ponieważ trudniej będzie zidentyfikować podobiekt (podinterface), który należy przekazać do funkcji, szczególnie jeśli podobiekty się pokrywają.
Podział
User
stanie się naprawdę brzydki, jeśli podobiekty będą się nakładać, wtedy ludzie będą się zastanawiać, który z nich wybrać, jeśli wszystkie wymagane pola będą się nakładać. Jeśli podzielisz się hierarchicznie (np. Masz to,UserMarketSegment
co ma , między innymiUserLocation
), ludzie nie będą mieli pewności, na jakim poziomie piszą funkcję: czy zajmuje się danymi użytkownika naLocation
poziomie czy naMarketSegment
poziomie? Nie pomaga to z czasem zmienić, tzn. Powracasz do zmiany sygnatur funkcji, czasem w całym łańcuchu wywołań.Innymi słowy: Chyba, że naprawdę znasz swoją domenę i nie masz jasnego pojęcia, z jakim modułem radzimy sobie w jakich aspektach
User
, naprawdę nie warto poprawiać struktury programu.źródło
To naprawdę interesujące pytanie. To zależy.
Jeśli uważasz, że twoja metoda może się w przyszłości zmienić wewnętrznie, aby wymagać różnych parametrów obiektu użytkownika, z pewnością powinieneś przekazać całość. Zaletą jest to, że kod zewnętrzny dla metody jest następnie chroniony przed zmianami w metodzie pod względem używanych parametrów, co, jak mówisz, spowodowałoby kaskadę zmian z zewnątrz. Przekazywanie całego użytkownika zwiększa enkapsulację.
Jeśli jesteś pewien, że nigdy nie będziesz musiał używać niczego innego niż powiedzieć e-mail użytkownika, powinieneś to przekazać. Zaletą tego jest to, że możesz następnie użyć tej metody w szerszym zakresie kontekstów: możesz na przykład użyć za pomocą firmowego adresu e-mail lub e-maila, który ktoś właśnie wpisał. Zwiększa to elastyczność.
Jest to część szerszej gamy pytań na temat budowania klas, które mają mieć szeroki lub wąski zakres, w tym czy wstrzykiwać zależności i czy mieć globalnie dostępne obiekty. W tej chwili istnieje niefortunna tendencja do myślenia, że węższy zakres jest zawsze dobry. Jednak zawsze występuje kompromis między enkapsulacją a elastycznością, jak w tym przypadku.
źródło
Uważam, że najlepiej jest przekazać jak najmniejszą liczbę parametrów i tyle, ile to konieczne. Ułatwia to testowanie i nie wymaga tworzenia skrzynek całych obiektów.
W twoim przykładzie, jeśli zamierzasz używać tylko identyfikatora użytkownika lub nazwy użytkownika, to wszystko, co powinieneś przekazać. Jeśli ten wzorzec powtarza się kilka razy, a rzeczywisty obiekt użytkownika jest znacznie większy, radzę stworzyć dla tego celu mniejszy interfejs. Mogłoby być
lub
To sprawia, że testowanie z wyśmiewaniem jest o wiele łatwiejsze i od razu wiesz, które wartości są naprawdę używane. W przeciwnym razie często musisz zainicjować złożone obiekty z wieloma innymi zależnościami, chociaż na koniec potrzebujesz tylko jednej lub dwóch właściwości.
źródło
Oto coś, co napotkałem od czasu do czasu:
User
(lubProduct
cokolwiek innego), który ma wiele właściwości, mimo że metoda wykorzystuje tylko kilka z nich.User
obiektu. Tworzy instancję i inicjuje tylko właściwości, których faktycznie potrzebuje metoda.User
argumentem, musisz znaleźć wywołania tej metody, aby dowiedzieć się, skądUser
pochodzi, abyś wiedział, które właściwości są wypełnione. Czy to „prawdziwy” użytkownik z adresem e-mail, czy może właśnie został utworzony, aby przekazać identyfikator użytkownika i niektóre uprawnienia?Jeśli utworzysz
User
i wypełnisz tylko kilka właściwości, ponieważ są to te, których potrzebuje metoda, wówczas osoba wywołująca naprawdę wie więcej o wewnętrznym działaniu metody niż powinna.Co gorsza, kiedy mają instancję
User
, trzeba wiedzieć, skąd pochodzi, aby wiedzieć, jakie właściwości są wypełniane. Nie chcesz tego wiedzieć.Z czasem, gdy programiści zobaczą, że są
User
używane jako kontener dla argumentów metod, mogą zacząć dodawać do niego właściwości w scenariuszach jednorazowego użytku. Teraz robi się brzydko, ponieważ klasa jest zaśmiecona właściwościami, które prawie zawsze będą zerowe lub domyślne.Takie zepsucie nie jest nieuniknione, ale zdarza się raz po raz, gdy mijamy obiekt tylko dlatego, że potrzebujemy dostępu do kilku jego właściwości. Strefa zagrożenia to pierwszy raz, gdy widzisz kogoś, kto tworzy instancję
User
i tylko wypełnia kilka właściwości, aby mogła przekazać ją do metody. Połóż na nim stopę, ponieważ jest to ciemna ścieżka.Tam, gdzie to możliwe, podaj właściwy przykład dla następnego programisty, przekazując tylko to, co musisz przekazać.
źródło