Wzór dla klasy, która robi tylko jedną rzecz


24

Powiedzmy, że mam procedurę, która wykonuje różne czynności :

void doStuff(initalParams) {
    ...
}

Teraz odkrywam, że „robienie rzeczy” to dość skomplikowana operacja. Procedura staje się duża, podzielę ją na wiele mniejszych procedur i wkrótce zdaję sobie sprawę, że posiadanie jakiegoś stanu byłoby przydatne podczas robienia rzeczy, więc muszę przekazywać mniej parametrów między małymi procedurami. Tak więc dzielę to na własną klasę:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

A potem nazywam to tak:

new StuffDoer().Start(initialParams);

lub tak:

new StuffDoer(initialParams).Start();

I to jest złe. Korzystając z .NET lub Java API, nigdy nie dzwonię new SomeApiClass().Start(...);, co mnie podejrzewa, że ​​robię to źle. Jasne, mógłbym ustawić konstruktora StuffDoer na prywatny i dodać metodę statycznego pomocnika:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Ale wtedy miałbym klasę, której zewnętrzny interfejs składa się tylko z jednej metody statycznej, co również wydaje się dziwne.

Stąd moje pytanie: czy istnieje dobrze ustalony wzorzec dla tego typu klas, które

  • mieć tylko jeden punkt wejścia i
  • nie mają stanu „zewnętrznie rozpoznawalnego”, tj. stan instancji jest wymagany tylko podczas wykonywania tego jednego punktu wejścia?

Byłem znany z tego, że bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");robiłem takie rzeczy, kiedy wszystko, na czym mi zależało, to ta konkretna informacja, a metody rozszerzenia LINQ nie są dostępne. Działa dobrze i mieści się w if()stanie bez konieczności przeskakiwania przez obręcze. Oczywiście, jeśli chcesz pisać kod w ten sposób, potrzebujesz zbierającego śmieci języka.
CVn

Jeśli jest to niezależne od języka , po co dodawać java i c # ? :)
Steven Jeuris

Jestem ciekawy, jaka jest funkcjonalność tej „jednej metody”? Bardzo pomogłoby to w określeniu najlepszego podejścia w konkretnym scenariuszu, ale interesujące pytanie!
Steven Jeuris

@StevenJeuris: Natknąłem się na ten problem w różnych sytuacjach, ale w tym przypadku jest to metoda, która importuje dane do lokalnej bazy danych (ładuje je z serwera WWW, wykonuje pewne manipulacje i przechowuje je w bazie danych).
Heinzi,

1
Jeśli zawsze wywołujesz metodę podczas tworzenia instancji, dlaczego nie uczynić jej logiką inicjującą wewnątrz konstruktora?
NoChance

Odpowiedzi:


29

Istnieje wzorzec o nazwie Method Object, w którym wyróżniasz pojedynczą, dużą metodę z dużą ilością tymczasowych zmiennych / argumentów w oddzielnej klasie. Robisz to zamiast wyodrębniać części metody w osobne metody, ponieważ potrzebują one dostępu do stanu lokalnego (parametry i zmienne tymczasowe) i nie możesz udostępnić stanu lokalnego za pomocą zmiennych instancji, ponieważ byłyby lokalne dla tej metody (i metody wyodrębnione z niego) i pozostałyby nieużywane przez resztę obiektu.

Zamiast tego metoda staje się własną klasą, parametry i zmienne tymczasowe stają się zmiennymi instancji tej nowej klasy, a następnie metoda zostaje podzielona na mniejsze metody nowej klasy. Klasa wynikowa ma zwykle tylko jedną metodę instancji publicznej, która wykonuje zadanie zawarte w klasie.


1
Brzmi dokładnie tak, jak to robię. Dzięki, przynajmniej teraz mam na to imię. :-)
Heinzi

2
Bardzo trafny link! Pachnie mi jak anty-wzór. Jeśli twoja metoda jest tak długa, być może jej części można wyodrębnić w klasy do ponownego użycia lub ogólnie lepiej poddać refaktoryzacji? Nie słyszałem też wcześniej o tym schemacie, nie mogę też znaleźć wielu innych zasobów, co sprawia, że ​​uważam, że na ogół nie jest on tak często wykorzystywany.
Steven Jeuris,


1
@StevenJeuris Nie sądzę, że to anty-wzór. Anty-wzorzec polegałby na zaśmiecaniu refaktoryzowanych metod parametrami partii zamiast przechowywania tego stanu, który jest wspólny dla tych metod w zmiennej instancji.
Alfredo Osorio

@AlfredoOsorio: No cóż, zaśmieca zrefaktoryzowane metody z wieloma parametrami. Mieszkają w obiekcie, więc jest to lepsze niż przekazywanie ich wszystkich dookoła, ale jest to gorsze niż refaktoryzacja komponentów wielokrotnego użytku, które wymagają jedynie ograniczonego podzbioru parametrów.
Jan Hudec

13

Powiedziałbym, że przedmiotowa klasa (wykorzystująca pojedynczy punkt wejścia) jest dobrze zaprojektowana. Jest łatwy w użyciu i przedłużany. Dość SOLIDNY.

To samo dotyczy no "externally recognizable" state. To dobre kapsułkowanie.

Nie widzę żadnych problemów z kodem, takich jak:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
I oczywiście, jeśli nie musisz ponownie używać tego samego doer, zmniejsza się do var result = new StuffDoer(initialParams).Calculate(extraParams);.
CVn

Calculate należy zmienić nazwę na doStuff!
shabunc

1
Dodałbym, że jeśli nie chcesz inicjować (nowej) swojej klasy tam, gdzie jej używasz (prawdopodobnie chcesz użyć fabryki), masz możliwość stworzenia obiektu gdzieś indziej w logice biznesowej i przekazania parametrów bezpośrednio do jednej metody publicznej, którą masz. Inną opcją jest użycie Factory i posiadanie na niej metody make, która pobiera parametry i tworzy obiekt ze wszystkimi parametrami za pośrednictwem swojego konstruktora. Następnie wywołujesz metodę publiczną bez parametrów z dowolnego miejsca.
Patkos Csaba,

2
Ta odpowiedź jest zbyt uproszczona. Czy StringComparerklasa, której używasz, jest new StringComparer().Compare( "bleh", "blah" )dobrze zaprojektowana? Co z tworzeniem stanu, gdy nie ma takiej potrzeby? Okej, zachowanie jest dobrze zamknięte (przynajmniej dla użytkownika klasy, więcej na ten temat w mojej odpowiedzi ), ale domyślnie nie oznacza to, że jest to „dobry projekt”. Jeśli chodzi o twój przykład „doer-rezultat”. Miałoby to sens tylko, gdybyś kiedykolwiek używał go doerosobno result.
Steven Jeuris

1
To nie jest jeden powód, by istnieć one reason to change. Różnica może wydawać się subtelna. Musisz zrozumieć, co to znaczy. Nigdy nie utworzy jednej klasy metod
jgauffin,

8

Z punktu widzenia zasad SOLID odpowiedź jgauffina ma sens. Nie należy jednak zapominać o ogólnych zasadach projektowania, np . Ukrywania informacji .

Widzę kilka problemów z danym podejściem:

  • Jak sam zauważyłeś, ludzie nie oczekują użycia słowa kluczowego „new”, gdy utworzony obiekt nie zarządza żadnym stanem . Twój projekt odzwierciedla jego intencję. Osoby korzystające z twojej klasy mogą się mylić co do tego, w jakim stanie zarządza i czy kolejne wywołania metody mogą spowodować inne zachowanie.
  • Z perspektywy osoby korzystającej z klasy stan wewnętrzny jest dobrze ukryty, ale gdy chcesz wprowadzić zmiany w klasie lub po prostu ją zrozumieć, komplikujesz sytuację. Ja już napisałem wiele o problemach widzę metodami rozszczepiania tylko, aby im mniejszy , zwłaszcza podczas przenoszenia stan zakresu klasy. Zmieniasz sposób, w jaki powinien być używany interfejs API, aby mieć mniejsze funkcje! To moim zdaniem zdecydowanie posuwa się za daleko.

Niektóre powiązane odniesienia

Być może głównym argumentem jest to, jak daleko należy rozciągnąć zasadę pojedynczej odpowiedzialności . „Jeśli dojdziesz do tego ekstremum i zbudujesz klasy, które mają jeden powód istnienia, możesz skończyć z tylko jedną metodą na klasę. Spowodowałoby to duży rozrost klas nawet dla najprostszych procesów, powodując, że system byłby trudne do zrozumienia i trudne do zmiany ”.

Kolejne istotne odniesienie do tego tematu: „Podziel swoje programy na metody, które wykonują jedno możliwe do zidentyfikowania zadanie. Zachowaj wszystkie operacje w metodzie na tym samym poziomie abstrakcji ”. - Kent Beck Key tutaj jest „tym samym poziomem abstrakcji”. Nie oznacza to „jednej rzeczy”, ponieważ jest często interpretowane. Ten poziom abstrakcji zależy wyłącznie od kontekstu, dla którego projektujesz.

Jakie jest właściwe podejście?

Trudno powiedzieć bez znajomości konkretnego przypadku użycia. Jest scenariusz, w którym czasami (nie często) stosuję podobne podejście. Kiedy chcę przetworzyć zestaw danych, bez potrzeby udostępniania tej funkcji dla całego zakresu klasy. Napisałem na blogu o tym, w jaki sposób lambdas może jeszcze bardziej poprawić enkapsulację . Zacząłem też pytanie na ten temat tutaj, na temat programistów . Poniżej znajduje się najnowszy przykład zastosowania tej techniki.

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Ponieważ bardzo „lokalny” kod w ForEachpliku nie jest ponownie używany nigdzie indziej, mogę po prostu przechowywać go w dokładnej lokalizacji, w której ma znaczenie. Zarysowanie kodu w taki sposób, że kod, który opiera się na sobie, jest silnie zgrupowane razem, czyni moim zdaniem bardziej czytelnym.

Możliwe alternatywy

  • W języku C # można zamiast tego użyć metod rozszerzenia. Operuj więc argumentem, który przekazujesz bezpośrednio do tej metody „jednej rzeczy”.
  • Sprawdź, czy ta funkcja faktycznie nie należy do innej klasy.
  • Uczyń go funkcją statyczną w klasie statycznej . To najprawdopodobniej jest najbardziej odpowiednie podejście, co znajduje również odzwierciedlenie w ogólnych interfejsach API, o których mowa.

Znalazłem możliwą nazwę tego anty-wzoru, jak nakreślono w innej odpowiedzi, Poltergeists .
Steven Jeuris,

4

Powiedziałbym, że jest całkiem dobry, ale twój interfejs API powinien nadal być metodą statyczną w klasie statycznej, ponieważ tego oczekuje użytkownik. Fakt, że metoda statyczna używa newdo tworzenia obiektu pomocnika i wykonywania pracy, jest szczegółem implementacji, który powinien być ukryty przed tym, kto go wywołuje.


Łał! Udało ci się dojść do sedna sprawy bardziej zwięźle niż ja. :) Dzięki. (oczywiście idę nieco dalej, gdzie możemy się nie zgodzić, ale zgadzam się z ogólną przesłanką)
Steven Jeuris

2
new StuffDoer().Start(initialParams);

Występuje problem z nieświadomymi programistami, którzy mogą korzystać z udostępnionej instancji i korzystać z niej

  1. wiele razy (ok, jeśli poprzednie (może częściowe) wykonanie nie psuje późniejszego wykonania)
  2. z wielu wątków (nie jest OK, wyraźnie powiedziałeś, że ma stan wewnętrzny).

Potrzebuje to więc wyraźnej dokumentacji, że nie jest bezpieczny wątkowo, a jeśli jest lekki (tworzenie go jest szybkie, nie wiąże się z zasobami zewnętrznymi ani dużą ilością pamięci), to jest w porządku, aby utworzyć instancję w locie.

Pomaga w tym ukrywanie go w metodzie statycznej, ponieważ ponowne użycie instancji nigdy się nie zdarza.

Jeśli to ma jakieś drogie inicjalizacji, to może (nie zawsze) będzie korzystne, aby przygotować inicjowanie go raz i używając go wiele razy, tworząc kolejną klasę, która będzie zawierać tylko stan i uczynić go Cloneable. Inicjalizacja utworzy stan, w którym będzie przechowywany doer. start()sklonuje go i przekaże metodom wewnętrznym.

Pozwoliłoby to również na inne rzeczy, takie jak utrzymywanie stanu częściowego wykonania. (Jeśli zajmie to dużo czasu, a czynniki zewnętrzne, np. Awaria zasilania elektrycznego, mogą przerwać wykonanie). Ale te dodatkowe wymyślne rzeczy zwykle nie są potrzebne, więc nie warto.


Słuszne uwagi. Mam nadzieję, że nie przeszkadza ci sprzątanie.
Steven Jeuris

2

Poza moją bardziej złożoną, spersonalizowaną inną odpowiedzią , uważam, że poniższa obserwacja zasługuje na osobną odpowiedź.

Istnieją oznaki, że możesz stosować się do anty-wzorca Poltergeists .

Poltergeists to klasy o bardzo ograniczonych rolach i efektywnych cyklach życia. Często rozpoczynają procesy dla innych obiektów. Przebudowane rozwiązanie obejmuje realokację obowiązków na obiekty o dłuższym okresie życia, które eliminują Poltergeistów.

Objawy i konsekwencje

  • Nadmiarowe ścieżki nawigacji.
  • Przemijające skojarzenia.
  • Klasy bezpaństwowe.
  • Tymczasowe, krótkotrwałe obiekty i klasy.
  • Klasy pojedynczej operacji, które istnieją tylko w celu „rozstawienia” lub „wywołania” innych klas za pomocą tymczasowych skojarzeń.
  • Klasy o nazwach operacji podobnych do sterujących, takich jak start_process_alpha.

Refaktoryzowane rozwiązanie

Pogromcy duchów rozwiązują Poltergeistów, usuwając je całkowicie z hierarchii klas. Jednak po ich usunięciu funkcjonalność „zapewniona” przez poltergeist musi zostać wymieniona. Jest to łatwe dzięki prostemu dostosowaniu w celu korekty architektury.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.