Czytam o typowych zapachach kodu w książce Martin Fowler Refactoring . W tym kontekście zastanawiałem się nad wzorem, który widzę w bazie kodu i czy można obiektywnie uznać go za anty-wzór.
Wzorzec to taki, w którym obiekt jest przekazywany jako argument do jednej lub więcej metod, z których wszystkie zmieniają stan obiektu, ale żadna z nich nie zwraca obiektu. Opiera się więc na charakterze referencyjnym (w tym przypadku) C # / .NET.
var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);
Uważam, że (zwłaszcza gdy metody nie są odpowiednio nazwane) muszę przyjrzeć się takim metodom, aby zrozumieć, czy stan obiektu się zmienił. To sprawia, że rozumienie kodu jest bardziej złożone, ponieważ muszę śledzić wiele poziomów stosu wywołań.
Chciałbym zaproponować ulepszenie takiego kodu, aby zwracał inny (sklonowany) obiekt z nowym stanem lub cokolwiek, co jest potrzebne do zmiany obiektu w miejscu wywołania.
var something1 = new Thing();
// ...
// Let's return a new instance of Thing
var something2 = Foo(something1);
// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);
// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);
Wydaje mi się, że pierwszy wzór jest wybierany na podstawie założeń
- że jest to mniej pracy lub wymaga mniej wierszy kodu
- że pozwala nam zarówno zmienić obiekt, jak i zwrócić inną informację
- że jest bardziej wydajny, ponieważ mamy mniej przypadków
Thing
.
Ilustruję alternatywę, ale aby ją zaproponować, trzeba argumentować przeciwko oryginalnemu rozwiązaniu. Jakie argumenty można przedstawić, aby stwierdzić, że oryginalne rozwiązanie jest anty-wzorcem?
A co, jeśli w ogóle, jest nie tak z moim alternatywnym rozwiązaniem?
źródło
Odpowiedzi:
Tak, oryginalne rozwiązanie jest anty-wzorcem z powodów, które opisujesz: utrudnia rozumowanie o tym, co się dzieje, obiekt nie jest odpowiedzialny za swój własny stan / implementację (zerwanie enkapsulacji). Dodałbym również, że wszystkie te zmiany stanu są niejawnymi umowami metody, co czyni tę metodę kruchą w obliczu zmieniających się wymagań.
To powiedziawszy, twoje rozwiązanie ma swoje wady, z których najbardziej oczywistym jest to, że klonowanie obiektów nie jest świetne. W przypadku dużych obiektów może być wolny. Może to prowadzić do błędów, w których inne części kodu trzymają się starych odniesień (co prawdopodobnie ma miejsce w opisywanej bazie kodu). Uczynienie tych obiektów niezmiennymi rozwiązuje przynajmniej kilka z tych problemów, ale jest bardziej drastyczną zmianą.
O ile obiekty nie są małe i nieco przejściowe (co czyni ich dobrymi kandydatami na niezmienność), byłbym skłonny po prostu przenieść więcej przejścia stanu do samych obiektów. To pozwala ukryć szczegóły implementacji tych przejść i ustalić bardziej rygorystyczne wymagania dotyczące tego, kto / co / gdzie te zmiany stanu występują.
źródło
Bar(something)
(i modyfikować stansomething
), uczyńBar
członkiemsomething
typu.something.Bar(42)
jest bardziej prawdopodobne, że mutujesomething
, jednocześnie umożliwiając korzystanie z narzędzi OO (stan prywatny, interfejsy itp.) w celu ochronysomething
stanuWłaściwie, to jest prawdziwe zapachy kodu. Jeśli masz obiekt podlegający zmianom, udostępnia metody zmiany jego stanu. Jeśli masz wywołanie takiej metody osadzonej w zadaniu kilku instrukcji, dobrze jest przefiltrować to zadanie do własnej metody - co pozostawia dokładną opisaną sytuację. Ale jeśli nie masz nazw metod takich jak
Foo
iBar
, ale metody, które wyjaśniają, że zmieniają obiekt, nie widzę tutaj problemu. Myśleć olub
lub
lub
lub coś w tym rodzaju - nie widzę tu powodu, aby zwracać sklonowany obiekt dla tych metod, a także nie ma powodu, aby sprawdzać ich implementację, aby zrozumieć, że zmienią one stan przekazywanego obiektu.
Jeśli nie chcesz efektów ubocznych, spraw, aby twoje obiekty były niezmienne, wymusi metody takie jak powyższe, aby zwrócić zmieniony (sklonowany) obiekt bez zmiany oryginalnego.
źródło
Tak, zobacz http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/, gdzie znajduje się jeden z wielu przykładów ludzi wskazujących, że nieoczekiwane skutki uboczne są złe.
Zasadniczo podstawową zasadą jest to, że oprogramowanie jest zbudowane warstwowo, a każda warstwa powinna przedstawiać następną możliwie najczystszą abstrakcję. A czysta abstrakcja to taka, w której musisz mieć jak najmniej na uwadze, aby z niej korzystać. Nazywa się to modułowością i dotyczy wszystkiego - od pojedynczych funkcji po protokoły sieciowe.
źródło
ForEach<T>
robi.Przede wszystkim nie zależy to od „natury referencyjnej”, lecz od obiektów będących zmiennymi typami referencyjnymi. W językach niefunkcjonalnych prawie zawsze tak będzie.
Po drugie, to, czy jest to problem, czy nie, zależy zarówno od obiektu, jak i od tego, jak ściśle zmiany w różnych procedurach są ze sobą powiązane - jeśli nie dokonasz zmiany w Foo i spowoduje to awarię Bar, to problem. Niekoniecznie zapach kodu, ale jest to problem z Foo, Bar lub Coś (prawdopodobnie Bar tak jak powinien sprawdzać jego wejście, ale może to być Coś w niewłaściwym stanie, któremu powinien zapobiegać).
Nie powiedziałbym, że wznosi się do poziomu anty-wzoru, ale raczej coś, o czym należy pamiętać.
źródło
Twierdziłbym, że nie ma różnicy między
A.Do(Something)
modyfikowaniemsomething
asomething.Do()
modyfikowaniemsomething
. W obu przypadkach powinno być jasne z nazwy wywoływanej metody, którasomething
zostanie zmodyfikowana. Jeśli nie jest jasne z nazwy metody, niezależnie od tego, czysomething
jest to parametrthis
, czy część środowiska, nie należy go modyfikować.źródło
Myślę, że w niektórych sytuacjach można zmienić stan obiektu. Na przykład mam listę użytkowników i chcę zastosować różne filtry do listy przed zwróceniem jej do klienta.
I tak, możesz uczynić to ładnym, przenosząc filtrowanie na inną metodę, dzięki czemu uzyskasz coś w stylu:
Gdzie
Filter(users)
można wykonać powyższe filtry.Nie pamiętam, gdzie dokładnie się z tym spotkałem, ale myślę, że nazywano to potokiem filtrującym.
źródło
Nie jestem pewien, czy proponowane nowe rozwiązanie (kopiowania obiektów) jest wzorem. Problemem, jak wskazałeś, jest zła nomenklatura funkcji.
Powiedzmy, że piszę złożoną operację matematyczną jako funkcję f () . I udokumentować, że f () jest funkcją, która odwzorowuje
NXN
sięN
, a algorytm za nim. Jeśli funkcja jest nazwana niewłaściwie, nieudokumentowana i nie ma towarzyszących jej przypadków testowych, musisz zrozumieć kod, w którym to przypadku kod jest bezużyteczny.O twoim rozwiązaniu, kilka spostrzeżeń:
X
stałaY
pof()
, aleX
to faktycznieY
,) i ewentualnie niezgodność czasową.Problem, który próbujesz rozwiązać, jest poprawny; jednak nawet przy ogromnej inżynierii, problem jest omijany, a nie rozwiązywany.
źródło