Projektowanie klasy tak, aby przyjmowała całe klasy jako parametry, a nie poszczególne właściwości

30

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 userIdjako 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 Userobiekt, 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.

Jimbo
źródło
11
@gnat: To zdecydowanie nie jest duplikat. Możliwy duplikat dotyczy łączenia łańcuchów metod w celu sięgnięcia głęboko do hierarchii obiektów. To pytanie nie wydaje się w ogóle o to pytać.
Greg Burghardt
2
Druga forma jest często używana, gdy liczba przekazywanych parametrów stała się niewygodna.
Robert Harvey
12
Jedyną rzeczą, która mi się nie podoba w przypadku pierwszego podpisu jest to, że nie ma gwarancji, że identyfikator użytkownika i nazwa użytkownika faktycznie pochodzą od tego samego użytkownika. Jest to potencjalny błąd, którego można uniknąć, przenosząc użytkownika wszędzie. Ale decyzja naprawdę zależy od tego, co robią wywołane metody z argumentami.
17 z 26
9
Słowo „parsuj” nie ma sensu w kontekście, w którym go używasz. Miałeś na myśli „pass”?
Konrad Rudolph
5
Co o Iin SOLID? MyConstructorw zasadzie mówi teraz: „Potrzebuję Guida string”. Dlaczego więc nie mieć interfejsu zapewniającego Guidai a string, niech Userimplementuje ten interfejs i pozwala MyConstructorzależeć od instancji implementującej ten interfejs? A jeśli potrzeba MyConstructorzmiany, 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”.
Corak

Odpowiedzi:

31

Nie ma absolutnie nic złego w przekazywaniu całego Userobiektu 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 wymaga User.

Przekazywanie prostych typów danych jest przyjemne, dopóki nie znaczą one czegoś innego niż są. Rozważ ten przykład:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

I przykładowe użycie:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

Czy potrafisz dostrzec wadę? Kompilator nie może. Przekazywany „identyfikator użytkownika” jest tylko liczbą całkowitą. Nazywamy zmienną, userale inicjujemy jej wartość z blogPostRepositoryobiektu, który prawdopodobnie zwraca BlogPostobiekty, a nie Userobiekty - ale kod się kompiluje, co kończy się błędnym błędem czasu wykonywania.

Rozważmy teraz ten zmieniony przykład:

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

Być może Barmetoda używa tylko „identyfikatora użytkownika”, ale podpis metody wymaga Userobiektu. Wróćmy teraz do tego samego przykładowego użycia, co poprzednio, ale zmień go, aby przekazać „użytkownika” w:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

Teraz mamy błąd kompilatora. blogPostRepository.FindMetoda zwraca BlogPostobiekt, który my nazywamy sprytnie „user”. Następnie przekazujemy tego „użytkownika” do Barmetody i natychmiast otrzymujemy błąd kompilatora, ponieważ nie możemy przekazać BlogPostmetody do metody, która akceptuje User.

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 Userobiekt, 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ę Userzmienia w klasie.

Greg Burghardt
źródło
6
Powiedziałbym, że twoje rozumowanie samo w sobie wskazuje na przekazywanie pól, ale posiadanie tych pól jest trywialnymi opakowaniami wokół rzeczywistej wartości. W tym przykładzie użytkownik ma pole typu UserID, a UserID ma jedno pole o wartości całkowitej. Teraz deklaracja Bar mówi od razu, że Bar nie używa wszystkich informacji o Użytkowniku, tylko jego ID, ale nadal nie możesz popełniać żadnych głupich błędów, takich jak przekazywanie liczby całkowitej, która nie pochodzi z UserID do Bar.
Ian
(Cd.) Oczywiście tego rodzaju styl programowania jest dość żmudny, szczególnie w języku, który nie ma ładnego wsparcia składniowego (Haskell jest dobry dla tego stylu, na przykład, ponieważ można po prostu dopasować na „ID UserID”) .
Ian
5
@Ian: Myślę, że zawijanie identyfikatora w swoim własnym typie rolek wokół oryginalnego problemu podniesionego przez OP, który jest zmianami strukturalnymi w klasie User, powoduje konieczność refaktoryzacji wielu podpisów metod. Przekazanie całego obiektu użytkownika rozwiązuje ten problem.
Greg Burghardt
@Ian: Chociaż szczerze mówiąc, nawet pracując w C # bardzo kusiło mnie, by owijać Ids i tego typu w Struct, żeby dać trochę więcej przejrzystości.
Greg Burghardt
1
„nie ma nic złego w przekazywaniu wskaźnika w jego miejsce”. Lub odniesienie, aby uniknąć wszystkich problemów ze wskaźnikami, na które możesz natknąć się.
Yay295
17

Czy mam rację, sądząc, że jest to naruszenie zasady otwartej / zamkniętej?

Nie, to nie jest naruszenie tej zasady. Zasada ta dotyczy niezmieniania Userw sposób wpływający na inne części kodu, które z niej korzystają. Twoje zmiany Usermogą być takim naruszeniem, ale nie jest to powiązane.

Czy ta praktyka narusza jakieś inne zasady? Perahaps inwersji zależności?

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.

Czy naruszane są inne zasady inne niż SOLID, takie jak podstawowe zasady programowania obronnego?

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:

Występują problemy z koniecznością niepotrzebnej analizy nowej wartości w dół o pięć poziomów w łańcuchu, a następnie zmiany wszystkich odniesień do wszystkich pięciu istniejących metod ...

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 z pewnością plan przestrzegania tej zasady, o ile jest to wykonalne, obejmowałby strategie zmniejszające potrzebę przyszłych zmian?

Ale potem wędrujesz na terytorium YAGNI i nadal będzie ono prostopadłe do zasady. Jeśli masz metodę, Fooktóra przyjmuje nazwę użytkownika, a następnie chcesz Foorównież wybrać datę urodzenia, zgodnie z zasadą, dodajesz nową metodę; Foopozostaje 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 Userbezpoś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.

David Arno
źródło
Występują problemy z koniecznością niepotrzebnej analizy nowej wartości w dół o pięć poziomów w łańcuchu, a następnie zmiany wszystkich odniesień do wszystkich pięciu istniejących metod. Dlaczego zasada Otwarta / Zamknięta miałaby zastosowanie tylko do klasy użytkownika, a nie także do klasy, którą aktualnie edytuję, która jest również wykorzystywana przez inne klasy? Wiem, że zasada dotyczy przede wszystkim unikania zmian, ale z pewnością plan przestrzegania tej zasady, o ile jest to wykonalne, obejmowałby strategie zmniejszające potrzebę przyszłych zmian?
Jimbo
@Jimbo, zaktualizowałem swoją odpowiedź, aby spróbować odpowiedzieć na Twój komentarz.
David Arno
Doceniam twój wkład. BTW. Nawet Robert C Martin nie akceptuje zasady Open / Closed, która ma twardą zasadę. Jest to ogólna zasada, która nieuchronnie zostanie złamana. Stosowanie zasady to ćwiczenie polegające na próbie przestrzegania jej w takim stopniu, w jakim jest to możliwe. Dlatego wcześniej użyłem słowa „praktyczny”.
Jimbo
Przekazywanie parametrów użytkownika nie jest odwróceniem zależności, a nie samego użytkownika.
James Ellis-Jones
@ JamesEllis-Jones, Odwrócenie zależności odwraca zależności od „zapytaj” do „powiedz”. Jeśli przekażesz Userinstancję, 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ę.
David Arno
10

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.

Telastyn
źródło
+1, ale nie jestem pewien co do frazy „możesz przekazać więcej informacji”. Po zdaniu (użytkownik użytkownika) przekazujesz minimum informacji, odniesienie do jednego obiektu. To prawda, że ​​można użyć odwołania, aby uzyskać więcej informacji, ale oznacza to, że kod wywołujący nie musi go uzyskać. Po przejściu (identyfikator GUID, ciąg nazwa użytkownika) wywoływana metoda zawsze może wywołać User.find (identyfikator użytkownika) w celu znalezienia publicznego interfejsu obiektu, więc tak naprawdę niczego nie ukrywasz.
dcorking
5
@orkorking: „ Gdy przekazujesz (użytkownik użytkownika) przekazujesz minimum informacji, odniesienie do jednego obiektu ”. Przekazujesz maksymalną informację związaną z tym obiektem: cały obiekt. „ wywoływana metoda zawsze może wywołać User.find (identyfikator użytkownika) ...”. W dobrze zaprojektowanym systemie nie byłoby to możliwe, ponieważ dana metoda nie miałaby dostępu do User.find(). W zasadzie nie powinno nawet byćUser.find . Znalezienie użytkownika nigdy nie powinno być obowiązkiem User.
David Arno
2
@corking - enh. To, że mijasz referencje, które okazuje się małe, jest technicznym zbiegiem okoliczności. Łączymy całość Userz 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.
Telastyn
@DavidArno być może jest to klucz do jasnej odpowiedzi na OP. Czyją odpowiedzialnością powinno być znalezienie użytkownika? Czy istnieje nazwa zasady projektowania oddzielającej szukacz / fabrykę od klasy?
dcorking
1
@orkorking Powiedziałbym, że jest to jedna z implikacji zasady jednolitej odpowiedzialności. Wiedza, gdzie są przechowywane Użytkownicy i jak je odzyskać za pomocą identyfikatora to osobne obowiązki, których Userklasa nie powinna mieć. Może istnieć coś UserRepositorypodobnego, który zajmuje się takimi rzeczami.
Hulk
3

Ten projekt jest zgodny ze wzorcem obiektu parametru . Rozwiązuje problemy wynikające z posiadania wielu parametrów w sygnaturze metody.

Czy mam rację, sądząc, że jest to naruszenie zasady otwartej / zamkniętej?

Nie. Zastosowanie tego wzorca włącza zasadę otwarcia / zamknięcia (OCP). Na przykład klasy pochodne Usermożna podać jako parametr, który wywołuje inne zachowanie w klasie konsumującej.

Czy ta praktyka narusza jakieś inne zasady?

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:

Ta klasa ujawnia wszystkie informacje o użytkowniku, jego identyfikatorze, nazwie, poziomach dostępu do każdego modułu, strefie czasowej itp.

Problem dotyczy wszystkich informacji . Jeśli Userklasa 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ącej UserAuthenticationwłaściwość User.Idi User.Namesą istotne, ale nie istotne User.Timezone.

Zasada Segregacja Interfejs (ISP) jest również naruszona z podobne rozumowanie, ale dodaje inną perspektywę. Przykład: załóżmy, że klasa konsumująca UserManagementwymaga User.Namepodzielenia właściwości User.LastNamei należy w tym celu zmodyfikować User.FirstNameklasę 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 Userklasa z niego wywodziła:

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

Każdy interfejs powinien ujawniać podzbiór powiązanych właściwości Userklasy 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ącej UserAuthenticationużyj IUserAuthenticationInfozamiast User. Następnie, jeśli to możliwe, podziel Userklasę na wiele konkretnych klas, używając interfejsów jako „wzornika”.

Theo Lenndorff
źródło
1
Gdy użytkownik się komplikuje, następuje kombinatoryczna eksplozja możliwych podinterpretacji, na przykład, jeśli użytkownik ma tylko 3 właściwości, istnieje 7 możliwych kombinacji. Twoja propozycja brzmi nieźle, ale jest niewykonalna.
user949300
1. Analitycznie masz rację. Jednak w zależności od tego, w jaki sposób modelowana jest domena, fragmenty powiązanych informacji mają tendencję do grupowania. Praktycznie nie jest konieczne zajmowanie się wszystkimi możliwymi kombinacjami interfejsów i właściwości. 2. Przedstawione podejście nie miało być rozwiązaniem uniwersalnym, ale może powinienem dodać do odpowiedzi trochę więcej „możliwych” i „można”.
Theo Lenndorff
2

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).

Dom
źródło
2

Sprawdźmy tylko poszczególne aspekty SOLID:

  • Jedna odpowiedzialność: być może naruszona, jeśli ludzie omijają tylko sekcje klasy.
  • Otwarty / zamknięty: Nieistotne, gdzie przekazywane są sekcje klasy, tylko tam, gdzie przekazywany jest pełny obiekt. (Myślę, że właśnie tam zaczyna się dysonans poznawczy: musisz zmienić kod, ale sama klasa wydaje się w porządku).
  • Zastępstwo Łiskowa: Nie ma problemu, nie wykonujemy podklas.
  • Odwrócenie zależności (zależy od abstrakcji, a nie konkretnych danych). Tak, to naruszone: ludzie nie mają abstrakcji, wyjmują konkretne elementy klasy i przekazują to. Myślę, że to jest główny problem tutaj.

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 Userobiekt 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 Useri 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ł Userstanie 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, UserMarketSegmentco ma , między innymi UserLocation), ludzie nie będą mieli pewności, na jakim poziomie piszą funkcję: czy zajmuje się danymi użytkownika na Location poziomie czy na MarketSegmentpoziomie? 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.

Toolforger
źródło
1

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.

James Ellis-Jones
źródło
1

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ć

interface IIdentifieable
{
    Guid ID { get; }
}

lub

interface INameable
{
    string Name { get; }
}

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.

t3chb0t
źródło
1

Oto coś, co napotkałem od czasu do czasu:

  • Metoda przyjmuje argument typu User(lub Productcokolwiek innego), który ma wiele właściwości, mimo że metoda wykorzystuje tylko kilka z nich.
  • Z jakiegoś powodu część kodu musi wywołać tę metodę, nawet jeśli nie ma ona w pełni wypełnionego Userobiektu. Tworzy instancję i inicjuje tylko właściwości, których faktycznie potrzebuje metoda.
  • Zdarza się to kilka razy.
  • Po chwili, gdy natrafisz na metodę z Userargumentem, musisz znaleźć wywołania tej metody, aby dowiedzieć się, skąd Userpochodzi, 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 Useri 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ą Useruż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ę Useri 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ć.

Scott Hannen
źródło