Zajęcia z jedną metodą - najlepsze podejście?


172

Powiedzmy, że mam klasę, która ma wykonywać jedną funkcję. Po wykonaniu funkcji można go zniszczyć. Czy jest jakiś powód, aby preferować jedno z tych podejść?

// Initialize arguments in constructor
MyClass myObject = new MyClass(arg1, arg2, arg3);
myObject.myMethod();

// Pass arguments to method
MyClass myObject = new MyClass();
myObject.myMethod(arg1, arg2, arg3);

// Pass arguments to static method
MyClass.myMethod(arg1, arg2, arg3);

Celowo nie rozumiałem szczegółów, próbując uzyskać wskazówki dotyczące różnych sytuacji. Ale tak naprawdę nie miałem na myśli prostych funkcji bibliotecznych, takich jak Math.random (). Myślę bardziej o zajęciach, które wykonują określone, złożone zadanie, ale wymagają do tego tylko jednej (publicznej) metody.

Odpowiedzi:


264

Kiedyś uwielbiałem klasy użytkowe wypełnione metodami statycznymi. Dokonali wielkiej konsolidacji metod pomocniczych, które w przeciwnym razie powodowałyby piekło redundancji i konserwacji. Są bardzo łatwe w użyciu, bez tworzenia instancji, bez utylizacji, po prostu odpal i zapomnij. Myślę, że to była moja pierwsza nieświadoma próba stworzenia architektury zorientowanej na usługi - wiele usług bezstanowych, które po prostu wykonały swoją pracę i nic więcej. Jednak wraz z rozwojem systemu pojawią się smoki.

Polimorfizm
Powiedzmy, że mamy metodę UtilityClass.SomeMethod, która radośnie brzęczy. Nagle musimy nieco zmienić funkcjonalność. Większość funkcji jest taka sama, ale mimo to musimy zmienić kilka części. Gdyby nie była to metoda statyczna, moglibyśmy stworzyć klasę pochodną i zmienić zawartość metody w razie potrzeby. Ponieważ jest to metoda statyczna, nie możemy. Jasne, jeśli musimy tylko dodać funkcjonalność przed lub po starej metodzie, możemy utworzyć nową klasę i wywołać w niej starą - ale to po prostu obrzydliwe.

Problemy z interfejsem
Metod statycznych nie można definiować za pośrednictwem interfejsów z powodów logicznych. A ponieważ nie możemy przesłonić metod statycznych, klasy statyczne są bezużyteczne, gdy musimy je przekazać przez ich interfejs. To sprawia, że ​​nie możemy używać klas statycznych jako części wzorca strategii. Możemy załatać niektóre problemy, przekazując delegatów zamiast interfejsów .

Testowanie
Zasadniczo idzie to w parze z problemami interfejsu opisanymi powyżej. Ponieważ nasza zdolność do wymiany implementacji jest bardzo ograniczona, będziemy mieć również problem z zastąpieniem kodu produkcyjnego kodem testowym. Ponownie możemy je zawinąć, ale będzie to wymagało zmiany dużych części naszego kodu, aby móc akceptować opakowania zamiast rzeczywistych obiektów.

Wspiera obiekty blob
Ponieważ metody statyczne są zwykle używane jako metody użytkowe, a metody narzędziowe zwykle mają różne cele, szybko skończymy z dużą klasą wypełnioną niespójną funkcjonalnością - najlepiej, aby każda klasa miała jeden cel w systemie . Wolałbym mieć pięć razy więcej klas, o ile ich cele są dobrze określone.

Pełzanie parametrów
Po pierwsze, ta mała urocza i niewinna metoda statyczna może przyjmować jeden parametr. Wraz ze wzrostem funkcjonalności dodawanych jest kilka nowych parametrów. Wkrótce dodawane są kolejne parametry, które są opcjonalne, więc tworzymy przeciążenia metody (lub po prostu dodajemy wartości domyślne w językach, które je obsługują). Wkrótce mamy metodę, która przyjmuje 10 parametrów. Tylko pierwsze trzy są naprawdę wymagane, parametry 4-7 są opcjonalne. Ale jeśli określono parametr 6, wymagane jest również wypełnienie 7-9 ... Gdybyśmy utworzyli klasę, której jedynym celem jest zrobienie tego, co zrobiła ta metoda statyczna, moglibyśmy rozwiązać ten problem, przyjmując wymagane parametry w konstruktor i zezwalając użytkownikowi na ustawianie wartości opcjonalnych za pomocą właściwości lub metod w celu ustawienia wielu współzależnych wartości w tym samym czasie. Ponadto, jeśli metoda urosła do takiego stopnia złożoności,

Wymaganie od konsumentów tworzenia instancji klas bez powodu
Jednym z najczęstszych argumentów jest to, po co żądać, aby konsumenci naszej klasy utworzyli instancję wywołującą tę pojedynczą metodę, nie mając później pożytku z instancji? Tworzenie instancji klasy jest bardzo tanią operacją w większości języków, więc szybkość nie jest problemem. Dodanie dodatkowej linii kodu do konsumenta to niski koszt położenia podwalin pod znacznie łatwiejsze w utrzymaniu rozwiązanie w przyszłości. I wreszcie, jeśli chcesz uniknąć tworzenia instancji, po prostu utwórz pojedynczą otokę swojej klasy, która umożliwia łatwe ponowne użycie - chociaż wymaga to, aby Twoja klasa była bezstanowa. Jeśli nie jest bezstanowy, nadal możesz tworzyć statyczne metody opakowujące, które obsługują wszystko, a jednocześnie zapewniają wszystkie korzyści na dłuższą metę. Wreszcie,

Tylko Sith postępuje absolutnie.
Oczywiście są wyjątki od mojej niechęci do metod statycznych. Prawdziwe klasy użytkowe, które nie stwarzają żadnego ryzyka rozdęcia, są doskonałymi przypadkami dla metod statycznych - na przykład System.Convert. Jeśli twój projekt jest jednorazowy i nie ma wymagań dotyczących przyszłej konserwacji, ogólna architektura naprawdę nie jest bardzo ważna - statyczna lub niestatyczna nie ma tak naprawdę znaczenia - jednak szybkość rozwoju ma znaczenie.

Normy, normy, standardy!
Korzystanie z metod instancji nie ogranicza możliwości używania metod statycznych i na odwrót. O ile istnieje uzasadnienie za różnicowaniem i jest ustandaryzowane. Nie ma nic gorszego niż przeglądanie warstwy biznesowej obejmującej różne metody implementacji.


Jak mówi Mark, polimorfizm, możliwość przekazywania metod jako parametrów „strategii” oraz możliwość konfigurowania / ustawiania opcji to powody, dla których warto używać instancji . Na przykład: ListDelimiter lub DelimiterParser można skonfigurować, aby określić, jakie ograniczniki użyć / zaakceptować, czy przyciąć białe znaki z analizowanych tokenów, czy zawęzić listę, jak traktować pustą / pustą listę ... itd.
Thomas W

4
„W miarę jak system rośnie…” refaktoryzujesz?
Rodney Gitzel,

3
@ user3667089 Ogólne argumenty, które są potrzebne, aby klasa istniała logicznie, te, które powinienem przekazać w konstruktorze. Zwykle są one używane w większości / wszystkich metodach. Argumenty specyficzne dla metody, które przekazałbym w tej konkretnej metodzie.
Mark S. Rasmussen

1
Naprawdę to, co robisz z klasą statyczną, to powrót do surowego, nie zorientowanego obiektowo C (choć zarządzanego pamięcią) - wszystkie funkcje są w przestrzeni globalnej, nie ma this, musisz zarządzać stanem globalnym itp. Więc jeśli lubisz programowanie proceduralne, zrób to. Ale pamiętaj, że tracisz wtedy wiele strukturalnych zalet OO.
Inżynier

1
Większość z tych powodów ma związek ze złym zarządzaniem kodem, a nie z wykorzystaniem metod statycznych. W języku F # i innych językach funkcyjnych wszędzie używamy metod statycznych (funkcji bez stanu instancji) i nie mamy takich problemów. To powiedziawszy, F # zapewnia lepszy paradygmat używania funkcji (funkcje są pierwszej klasy, funkcje mogą być pobierane i częściowo stosowane), co czyni je bardziej wykonalnymi niż w C #. Większy powód do używania klas jest taki, że do tego służy język C #. Wszystkie konstrukcje Dependency Injection w .NET Core obracają się wokół klas wystąpień z deps.
Justin J Stark

89

Wolę sposób statyczny. Ponieważ klasa nie reprezentuje obiektu, nie ma sensu tworzyć jej instancji.

Klasy, które istnieją tylko dla ich metod, powinny pozostać statyczne.


19
-1 „Klasy, które istnieją tylko dla ich metod, powinny pozostać statyczne”.
Rookian

8
@Rookian, dlaczego się z tym nie zgadzasz?
jjnguy

6
przykładem może być wzorzec repozytorium. Klasa zawiera tylko metody. Podążanie za twoim podejściem nie pozwala na używanie interfejsów. => zwiększamy sprzężenie
Rookian

1
@Rook, generalnie ludzie nie powinni tworzyć klas, które są używane tylko jako metody. W podanym przykładzie metody statyczne nie są dobrym pomysłem. Ale w przypadku prostych metod użytkowych najlepsza jest metoda statyczna.
jjnguy

9
Absolutnie poprawne :) Z twoim warunkiem "dla prostych metod użytkowych" zgadzam się z tobą całkowicie, ale w twojej odpowiedzi sformułowałeś ogólną zasadę, która jest błędna imo.
Rookian

18

Jeśli nie ma powodu, aby utworzyć instancję klasy w celu wykonania funkcji, użyj implementacji statycznej. Po co zmuszać konsumentów tej klasy do tworzenia instancji, gdy nie jest ona potrzebna.


16

Jeśli nie musisz zapisywać stanu obiektu, nie ma potrzeby tworzenia go w pierwszej kolejności. Wybrałbym pojedynczą statyczną metodę, do której przekazujesz parametry.

Ostrzegałbym również przed olbrzymią klasą Utils, która ma dziesiątki niepowiązanych metod statycznych. W pośpiechu może to spowodować dezorganizację i problemy z obsługą. Lepiej jest mieć wiele klas, każda z kilkoma powiązanymi metodami.


6

Powiedziałbym, że format metody statycznej byłby lepszą opcją. Uczyniłbym również klasę statyczną, dzięki czemu nie musisz się martwić o przypadkowe utworzenie instancji klasy.


5

Naprawdę nie wiem, jaka jest tutaj sytuacja, ale spróbuję umieścić ją jako metodę w jednej z klas, do których należą arg1, arg2 lub arg3 - Jeśli możesz semantycznie powiedzieć, że jedna z tych klas będzie właścicielem metoda.


4

Sugerowałbym, że trudno jest odpowiedzieć na podstawie dostarczonych informacji.

Wydaje mi się, że jeśli zamierzasz mieć tylko jedną metodę i natychmiast wyrzucisz klasę, to uczyń ją statyczną klasą, która przyjmuje wszystkie parametry.

Oczywiście trudno jest dokładnie określić, dlaczego musisz utworzyć jedną klasę tylko dla tej jednej metody. Czy jest to typowa sytuacja „klasy usług użyteczności publicznej”, jak większość zakłada? A może implementujesz jakąś klasę reguł, której może być więcej w przyszłości.

Na przykład niech ta klasa jest podłączalna. Następnie chciałbyś utworzyć interfejs dla swojej jednej metody, a następnie chciałbyś, aby wszystkie parametry były przekazywane do interfejsu, a nie do konstruktora, ale nie chciałbyś, aby był statyczny.


3

Czy Twoja klasa może być statyczna?

Jeśli tak, to uczyniłbym to klasą „Narzędzia”, w której umieściłbym wszystkie moje klasy z jedną funkcją.


2
Raczej się nie zgadzam. W projektach, w które jestem zaangażowany, twoja klasa Utilities może zawierać setki niepowiązanych ze sobą metod.
Paul Tomblin

3
@Paul: Ogólnie uzgodniono. Nienawidzę zajęć z dziesiątkami (lub tak, setkami) zupełnie niezwiązanych ze sobą metod. Jeśli trzeba przyjąć takie podejście, przynajmniej podziel je na mniejsze, powiązane zestawy narzędzi (EG, FooUtilities, BarUtilities itp.).
John Rudy

9
jedna klasa - jedna odpowiedzialność.
Andreas Petersson,

3

Jeśli ta metoda jest bezstanowa i nie ma potrzeby jej przekazywania, najbardziej sensowne jest zdefiniowanie jej jako statycznej. Jeśli naprawdę potrzebujesz przekazać tę metodę, możesz rozważyć użycie delegata zamiast jednego z innych proponowanych podejść.


3

W przypadku prostych aplikacji i internalpomocników użyłbym metody statycznej. W przypadku aplikacji ze składnikami podoba mi się platforma Managed Extensibility Framework . Oto fragment dokumentu, który piszę, aby opisać wzorce, które znajdziesz w moich interfejsach API.

  • Usługi
    • Zdefiniowany przez I[ServiceName]Serviceinterfejs.
    • Eksportowane i importowane według typu interfejsu.
    • Pojedyncza implementacja jest dostarczana przez aplikację hosta i wykorzystywana wewnętrznie i / lub przez rozszerzenia.
    • Metody w interfejsie usługi są bezpieczne wątkowo.

Jako wymyślony przykład:

public interface ISettingsService
{
    string ReadSetting(string name);

    void WriteSetting(string name, string value);
}

[Export]
public class ObjectRequiringSettings
{
    [Import]
    private ISettingsService SettingsService
    {
        get;
        set;
    }

    private void Foo()
    {
        if (SettingsService.ReadSetting("PerformFooAction") == bool.TrueString)
        {
            // whatever
        }
    }
}

2

Po prostu zrobiłbym wszystko w konstruktorze. tak:

new MyClass(arg1, arg2, arg3);// the constructor does everything.

lub

MyClass my_object(arg1, arg2, arg3);

A jeśli chcesz zwrócić cokolwiek poza obiektem typu MyClass?
Bill the Lizard

13
Uważam za złą praktykę umieszczanie logiki wykonywania w konstruktorze. Dobry rozwój dotyczy semantyki. Z semantycznego punktu widzenia konstruktor istnieje po to, aby zbudować obiekt, a nie wykonywać innych zadań.
mstrobl

bill, jeśli potrzebujesz zwracanej wartości, przekaż referencję do ret vvalue: MyClass myObject (arg1, arg2, arg3, retvalue); mstrobl, jeśli obiekt nie jest potrzebny, po co go tworzyć? i ta sztuczka może ci pomóc w niektórych przypadkach.

Jeśli obiekt nie ma stanu, tj. Nie ma zmiennej, to jego instancja nie przyniesie żadnej alokacji - ani na stercie, ani na stosie. Możesz myśleć o obiektach w C ++ jako o wywołaniach funkcji C z ukrytym pierwszym parametrem wskazującym na strukturę.
mstrobl

mstrobl, działa podobnie jak funkcja ac na sterydach, ponieważ możesz zdefiniować funkcje prywatne, których może używać ctor. możesz także użyć zmiennych składowych klasy, aby pomóc w przekazywaniu danych do funkcji prywatnych.

0

Jeszcze jedną ważną kwestią do rozważenia jest to, czy system działałby w środowisku wielowątkowym i czy byłoby bezpieczne wątkowo mieć metodę statyczną lub zmienne ...

Należy zwrócić uwagę na stan systemu.


0

Może uda wam się wspólnie uniknąć tej sytuacji. Spróbuj refaktoryzować, aby uzyskać arg1.myMethod1(arg2, arg3). Zamień arg1 na arg2 lub arg3, jeśli ma to większy sens.

Jeśli nie masz kontroli nad klasą arg1, udekoruj ją:

class Arg1Decorator
    private final T1 arg1;
    public Arg1Decorator(T1 arg1) {
        this.arg1 = arg1;
    }
    public T myMethod(T2 arg2, T3 arg3) {
        ...
    }
 }

 arg1d = new Arg1Decorator(arg1)
 arg1d.myMethod(arg2, arg3)

Rozumowanie jest takie, że w przypadku OOP dane i metody przetwarzania tych danych należą do siebie. Plus masz wszystkie zalety, o których wspomniał Mark.


0

Myślę, że jeśli właściwości twojej klasy lub instancji klasy nie będą używane w konstruktorach lub w twoich metodach, metody nie powinny być zaprojektowane jako wzorzec „statyczny”. metoda statyczna powinna być zawsze traktowana jako pomoc.


0

W zależności od tego, czy chcesz tylko coś zrobić, czy coś zrobić i zwrócić, możesz to zrobić:

public abstract class DoSomethingClass<T>
{
    protected abstract void doSomething(T arg1, T arg2, T arg3);
}

public abstract class ReturnSomethingClass<T, V>
{
    public T value;
    protected abstract void returnSomething(V arg1, V arg2, V arg3);
}

public class DoSomethingInt extends DoSomethingClass<Integer>
{
    public DoSomethingInt(int arg1, int arg2, int arg3)
    {
        doSomething(arg1, arg2, arg3);
    }

    @Override
    protected void doSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        // ...
    }
}

public class ReturnSomethingString extends ReturnSomethingClass<String, Integer>
{
    public ReturnSomethingString(int arg1, int arg2, int arg3)
    {
        returnSomething(arg1, arg2, arg3);
    }

    @Override
    protected void returnSomething(Integer arg1, Integer arg2, Integer arg3)
    {
        String retValue;
        // ...
        value = retValue;
    }
}

public class MainClass
{
    static void main(String[] args)
    {
        int a = 3, b = 4, c = 5;

        Object dummy = new DoSomethingInt(a,b,c);  // doSomething was called, dummy is still around though
        String myReturn = (new ReturnSomethingString(a,b,c)).value; // returnSomething was called and immediately destroyed
    }
}
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.