Czy klasy z tylko jedną (publiczną) metodą stanowią problem?

51

Obecnie pracuję nad projektem oprogramowania, który wykonuje kompresję i indeksowanie nagrań z monitoringu wideo. Kompresja polega na dzieleniu obiektów tła i pierwszego planu, a następnie zapisywaniu tła jako obrazu statycznego, a pierwszego planu jako duszka.

Niedawno rozpocząłem przegląd niektórych klas, które zaprojektowałem dla tego projektu.

Zauważyłem, że istnieje wiele klas, które mają tylko jedną metodę publiczną. Niektóre z tych klas to:

  • VideoCompressor (z compressmetodą, która pobiera wejściowe wideo typu RawVideoi zwraca wyjściowe wideo typu CompressedVideo).
  • VideoSplitter (z splitmetodą, która pobiera wejściowe wideo typu RawVideoi zwraca wektor 2 wyjściowych filmów wideo, każdego typu RawVideo).
  • VideoIndexer (z indexmetodą, która pobiera wejściowe wideo typu RawVideoi zwraca indeks typu wideo VideoIndex).

Uważam siebie instancji każdą klasę tylko do wykonywania połączeń, takich jak VideoCompressor.compress(...), VideoSplitter.split(...), VideoIndexer.index(...).

Na pierwszy rzut oka wydaje mi się, że nazwy klas wystarczająco opisują ich zamierzoną funkcję i są w rzeczywistości rzeczownikami. Odpowiednio ich metody są również czasownikami.

Czy to rzeczywiście problem?

yjwong
źródło
14
To zależy od języka. W języku opartym na wielu paradygmatach, takim jak C ++ lub Python, klasy te nie istnieją. Ich „metody” powinny być funkcjami wolnymi.
3
@delnan: nawet w C ++ zwykle używasz klas do tworzenia modułów, nawet jeśli nie potrzebujesz pełnych „możliwości OO”. Rzeczywiście istnieje alternatywa zamiast przestrzeni nazw do grupowania „wolnych funkcji” razem w module, ale nie widzę w tym żadnych korzyści.
Doc Brown
5
@DocBrown: C ++ ma do tego przestrzeń nazw. W rzeczywistości w nowoczesnym C ++ często używasz bezpłatnych funkcji dla metod, ponieważ mogą one być (statycznie) przeciążone dla wszystkich argumentów, podczas gdy metody mogą być przeciążone tylko dla wywołującego.
Jan Hudec
2
@DocBrown: To bardzo sedno pytania. Moduły wykorzystujące przestrzenie nazw posiadające tylko jedną „zewnętrzną” metodę działają doskonale, gdy funkcja jest wystarczająco złożona. Ponieważ przestrzenie nazw nie udają, że reprezentują cokolwiek i nie udają, że są zorientowane obiektowo. Klasy tak, ale takie klasy to tak naprawdę tylko przestrzenie nazw. Oczywiście w Javie nie możesz mieć funkcji, więc taki jest wynik. Wstyd w Javie.
Jan Hudec
2
Powiązane (prawie duplikat): programmers.stackexchange.com/q/175070/33843
Heinzi

Odpowiedzi:

87

Nie, to nie jest problem, wręcz przeciwnie. Jest to znak modułowości i wyraźnej odpowiedzialności klasy. Interfejs Lean jest łatwy do uchwycenia z punktu widzenia użytkownika tej klasy i zachęca do luźnego łączenia. Ma to wiele zalet, ale prawie nie ma wad. Chciałbym, aby w ten sposób zaprojektowano więcej komponentów!

Doktor Brown
źródło
6
To jest poprawna odpowiedź, a ja ją poparłem, ale możesz ją poprawić, wyjaśniając niektóre zalety. Myślę, że instynkt OP mówi mu, że to problem, to coś innego ... to znaczy, że VideoCompressor to naprawdę interfejs. Mp4VideoCompressor jest klasą i powinien być wymienny z innym VideoCompressorem bez kopiowania kodu VideoSplittera do nowej klasy.
pdr
1
Przepraszam, ale się mylisz. Klasa z jedną metodą powinna być po prostu samodzielną funkcją.
Miles Rout
3
@MilesRout: Twój komentarz pokazuje, że źle zrozumiałeś pytanie - OP napisał o klasie z jedną metodą publiczną, a nie z jedną metodą (i rzeczywiście miał na myśli klasę z wieloma metodami prywatnymi, jak powiedział nam tutaj w jednym komentarzu).
Doc Brown
2
„Klasa z jedną metodą powinna być po prostu samodzielną funkcją.” Ogólne założenie o tym, czym jest ta metoda. Mam klasę z jedną publiczną metodą. ZA TO jest wiele metod w tej klasie plus 5 innych klas, które absolutnie nie mają pojęcia o kliencie. Doskonałe zastosowanie SRP zapewni czysty, prosty interfejs warstwowej struktury klas, z prostotą przejawiającą się aż do szczytu.
radarbob
2
@Andy: pytanie brzmiało: „czy to problem”, a nie „czy jest to zgodne z określoną definicją OO”. Co więcej, nie ma absolutnie żadnego znaku w pytaniu, że OP oznacza klasy bez stanu.
Doc Brown,
26

Nie jest już zorientowany obiektowo. Ponieważ te klasy nic nie reprezentują, są tylko naczyniami dla funkcji.

To nie znaczy, że to źle. Jeśli funkcjonalność jest wystarczająco złożona lub gdy jest ogólna (tzn. Argumentami są interfejsy, a nie konkretne typy końcowe), sensowne jest umieszczenie tej funkcjonalności w osobnym module .

Stamtąd zależy od twojego języka. Jeśli język ma wolne funkcje, powinny to być moduły eksportujące funkcje. Po co udawać, że to klasa, skoro tak nie jest. Jeśli język nie ma wolnych funkcji, takich jak np. Java, wówczas tworzysz klasy za pomocą jednej publicznej metody. To pokazuje granice projektowania obiektowego. Czasami funkcjonalne jest po prostu lepiej dopasowane.

Jest jeden przypadek, w którym możesz potrzebować klasy z jedną metodą publiczną, ponieważ musi ona implementować interfejs z jedną metodą publiczną. Może to być wzorzec obserwatora lub zastrzyk zależności lub cokolwiek innego. Tutaj znowu zależy od języka. W językach, które mają funktory pierwszej klasy (C ++ ( std::functionlub parametr szablonu), C # (delegate), Python, Perl, Ruby ( proc), Lisp, Haskell, ...) wzorce te używają typów funkcji i nie wymagają klas. Java nie ma (jeszcze w wersji 8) typów funkcji, więc korzystasz z interfejsów pojedynczych metod i odpowiadających im klas pojedynczych metod.

Oczywiście nie opowiadam się za pisaniem pojedynczej wielkiej funkcji. Powinien mieć prywatne podprogramy, ale mogą być prywatne dla pliku implementacji (statyczna lub anonimowa przestrzeń nazw na poziomie pliku w C ++) lub prywatnej klasy pomocniczej, która jest tworzona tylko w ramach funkcji publicznej ( przechowywać dane czy nie? ).

Jan Hudec
źródło
12
„Po co udawać, że to klasa, skoro tak nie jest”. Posiadanie obiektu zamiast funkcji swobodnej umożliwia stan i podtytuły. Może to okazać się cenne w świecie, w którym zmieniają się wymagania. Wcześniej czy później trzeba grać z ustawieniami kompresji wideo lub podać alternatywne algorytmy kompresji. Wcześniej słowo „klasa” po prostu mówi nam, że ta funkcja należy do łatwo rozszerzalnego i wymiennego modułu oprogramowania, z wyraźną odpowiedzialnością. Czy nie do tego tak naprawdę dąży OO?
PRZYJEDŹ Z
1
Cóż, jeśli ma tylko jedną metodę publiczną (i w pewnym sensie nie sugeruje żadnych chronionych), tak naprawdę nie można jej rozszerzyć. Oczywiście spakowanie parametrów kompresji może mieć sens, w którym to przypadku staje się obiektem funkcji (niektóre języki mają dla nich osobne wsparcie, inne nie).
Jan Hudec
3
Jest to podstawowe połączenie między obiektami i funkcjami: funkcja jest izomorficzna w stosunku do obiektu za pomocą jednej metody i bez pól. Zamknięcie jest izomorficzne dla obiektu za pomocą jednej metody i niektórych pól. Funkcja selektora zwracająca jedną z kilku funkcji, które wszystkie zamykają się w tym samym zestawie zmiennych, jest izomorficzna dla obiektu. (W ten sposób obiekty są kodowane w JavaScript, z wyjątkiem tego, że używasz struktury danych w słowniku zamiast funkcji selektora.)
Jörg W Mittag
4
„Nie jest już zorientowany obiektowo. Ponieważ te klasy nic nie reprezentują, są tylko naczyniami dla funkcji.” - To jest źle. Twierdziłbym, że takie podejście jest WIĘCEJ obiektowe. OO nie oznacza, że ​​reprezentuje rzeczywisty obiekt. Duża część programowania dotyczy abstrakcyjnych pojęć, ale czy to oznacza, że ​​nie możesz zastosować podejścia OO, aby je rozwiązać? Absolutnie nie. Chodzi bardziej o modułowość i abstrakcję. Każdy obiekt ma jedną odpowiedzialność. Ułatwia to obejście programu i wprowadzanie zmian. Za mało miejsca, aby wymienić liczne zalety OOP.
Despertar
2
@ Phoshi: Tak, rozumiem. Nigdy nie twierdziłem, że funkcjonalne podejście również nie zadziała. Jednak nie jest to oczywiście temat. Kompresor wideo lub transmogryfikator wideo lub cokolwiek innego jest wciąż idealnym kandydatem na obiekt.
PRZYJEDŹ
13

Mogą istnieć powody, aby wyodrębnić daną metodę do dedykowanej klasy. Jednym z tych powodów jest zezwolenie na wstrzyknięcie zależności.

Wyobraź sobie, że masz klasę o nazwie, VideoExporterktóra ostatecznie powinna być w stanie skompresować wideo. Czystym sposobem byłoby posiadanie interfejsu:

interface IVideoCompressor
{
    Stream compress(Video video);
}

które będą realizowane w następujący sposób:

class MpegVideoCompressor : IVideoCompressor
{
    // ...
}

class FlashVideoCompressor : IVideoCompressor
{
    // ...
}

i używane w ten sposób:

class VideoExporter
{
    // ...
    void export(Destination destination, IVideoCompressor compressor)
    {
        // ...
        destination = compressor(this.source);
        // ...
    }
    // ...
}

Zły alternatywą byłoby mieć VideoExporterktóry ma mnóstwo metod publicznych i wykonuje całą pracę, w tym prasowanie. Szybko stałoby się koszmarem konserwacji, co utrudniłoby dodanie obsługi innych formatów wideo.

Arseni Mourzenko
źródło
2
Twoja odpowiedź nie rozróżnia metod publicznych i prywatnych. Można to rozumieć jako zalecenie, aby umieścić cały kod klasy tylko jedną metodą, ale myślę, że nie o to ci chodziło?
Doc Brown
2
@DocBrown: Prywatne metody nie dotyczą tego pytania. Można je umieścić na wewnętrznej klasie pomocniczej lub cokolwiek innego.
Jan Hudec
2
@ JanHudec: obecnie tekst brzmi: „Złą alternatywą byłoby mieć VideoExporter, który ma wiele metod” - ale powinno być „Złą alternatywą byłoby mieć VideoExporter, który ma wiele publicznych metod”.
Doc Brown
@DocBrown: Zgadzam się tutaj.
Jan Hudec
2
@DocBrown: dziękuję za uwagi. Zredagowałem swoją odpowiedź. Początkowo myślałem, że założono, że pytanie (a więc moja odpowiedź) dotyczy wyłącznie metod publicznych. Wygląda na to, że nie jest to tak oczywiste.
Arseni Mourzenko
6

Jest to znak, że chcesz przekazać funkcje jako argumenty do innych funkcji. Zgaduję, że twój język (Java?) Go nie obsługuje; jeśli tak jest, to nie tyle wada projektu, co wada w wybranym języku. Jest to jeden z największych problemów z językami, które wymagają, aby wszystko było klasą.

Jeśli tak naprawdę nie przekazujesz tych fałszywych funkcji, po prostu potrzebujesz funkcji swobodnej / statycznej.

Doval
źródło
1
Od wersji Java 8 masz lambdy, więc możesz przekazywać funkcje.
Silviu Burcea
Racja, ale nie zostanie to oficjalnie wydane na chwilę, a niektóre miejsca pracy powoli przechodzą na nowsze wersje.
Doval
Data premiery to marzec. Również kompilacje EAP były bardzo popularne dla JDK 8 :)
Silviu Burcea
Chociaż lambda Java 8 są po prostu skrótowym sposobem definiowania obiektów za pomocą tylko jednej metody publicznej, innej niż zapisanie odrobiny kodu z podstawowymi kodami, nie mają tutaj żadnej różnicy.
Jules
5

Wiem, że jestem spóźniony na imprezę, ale jak się wydaje, wszyscy tęsknili za wskazaniem:

Jest to dobrze znany wzorzec projektowy o nazwie: Wzorzec strategii .

Wzór strategii jest stosowany, gdy istnieje kilka możliwych strategii rozwiązania pod-problemu. Zazwyczaj definiujesz interfejs, który wymusza kontrakt na wszystkich implementacjach, a następnie używasz jakiejś formy wstrzykiwania zależności, aby zapewnić Ci konkretną strategię.

Na przykład w tym przypadku możesz mieć, interface VideoCompressora następnie mieć kilka alternatywnych implementacji, na przykład class H264Compressor implements VideoCompressori class XVidCompressor implements VideoCompressor. Z PO nie jest jasne, że w grę wchodzi interfejs, nawet jeśli nie jest, może być tak, że oryginalny autor pozostawił otwarte drzwi do wdrożenia strategii w razie potrzeby. Co samo w sobie jest również dobrym projektem.

Problemem, który OP nieustannie tworzy instancje klas w celu wywołania metody, jest problem z tym, że nie używa ona poprawnie wstrzykiwania zależności i wzorca strategii. Zamiast tworzenia instancji tam, gdzie jest to potrzebne, klasa zawierająca powinna mieć element członkowski z obiektem strategii. I ten element powinien zostać wstrzyknięty, na przykład do konstruktora.

W wielu przypadkach wzorzec strategii skutkuje klasami interfejsów (jak pokazano) za pomocą tylko jednej doStuff(...)metody.

Emily L.
źródło
1
public interface IVideoProcessor
{
   void Split();

   void Compress();

   void Index();
}

To, co masz, jest modułowe i to dobrze, ale jeśli zgrupujesz te obowiązki w IVideoProcessor, prawdopodobnie byłoby to bardziej sensowne z punktu widzenia DDD.

Z drugiej strony, jeśli dzielenie, kompresowanie i indeksowanie nie było w żaden sposób powiązane, to trzymałbym je jako osobne komponenty.

CodeART
źródło
Ponieważ powód, dla którego każda z tych funkcji wymaga zmiany, jest nieco inny, twierdzę, że takie połączenie razem narusza SRP.
Jules
0

To jest problem - pracujesz z funkcjonalnego aspektu projektu, a nie z danych. W rzeczywistości masz 3 niezależne funkcje, które zostały OO-ified.

Na przykład masz klasę VideoCompressor. Dlaczego pracujesz z klasą przeznaczoną do kompresji wideo - dlaczego nie masz klasy Video z metodami do kompresji danych (wideo), które zawiera każdy obiekt tego typu?

Projektując systemy OO, najlepiej jest tworzyć klasy reprezentujące obiekty, a nie klasy reprezentujące działania, które można zastosować. W dawnych czasach klasy były nazywane typami - OO było sposobem na rozszerzenie języka o obsługę nowych typów danych. Jeśli myślisz o OO w ten sposób, masz lepszy sposób na zaprojektowanie swoich zajęć.

EDYTOWAĆ:

pozwól, że spróbuję wyjaśnić się trochę lepiej, wyobraź sobie klasę ciągów, która ma metodę konkat. Możesz zaimplementować taką funkcję, w której każdy obiekt utworzony w klasie zawiera dane ciągu, więc możesz to powiedzieć

string mystring("Hello"); 
mystring.concat("World");

ale OP chce, aby działało w ten sposób:

string mystring();
string result = mystring.concat("Hello", "World");

teraz są miejsca, w których klasa może być użyta do przechowywania zbioru powiązanych funkcji, ale to nie jest OO, to przydatny sposób korzystania z funkcji OO języka, aby pomóc lepiej zarządzać bazą kodu, ale nie jest w żaden sposób „OO Design”. Obiekt w takich przypadkach jest całkowicie sztuczny, po prostu używany w ten sposób, ponieważ język nie oferuje nic lepszego do zarządzania tego rodzaju problemem. na przykład. W językach takich jak C # użyłbyś klasy statycznej, aby zapewnić tę funkcjonalność - ponownie wykorzystuje mechanizm klasy, ale nie musisz już tworzyć instancji obiektu, aby wywołać na nim metody. Skończysz z metodami, string.IsNullOrEmpty(mystring)które moim zdaniem są słabe w porównaniu do mystring.isNullOrEmpty().

Tak więc, jeśli ktoś pyta „jak zaprojektować moje klasy”, zalecam zastanowienie się nad danymi, które będzie zawierać klasa, a nie funkcjami, które ona zawiera. Jeśli wybierzesz „klasa to kilka metod”, to w końcu piszesz kod w stylu „lepszego C”. (co niekoniecznie jest złą rzeczą, jeśli poprawiasz kod C), ale nie da ci to najlepiej zaprojektowanego programu OO.

gbjbaanb
źródło
9
-1, całkowicie się nie zgadzam. Początkujący projekt OO działa tylko z obiektami danych takimi jak a Videoi ma tendencję do przepełnienia takich klas funkcjami, które często kończą się niechlujnym kodem z> 10K LOC na klasę. Zaawansowany projekt OO dzieli funkcjonalność na mniejsze jednostki, takie jak VideoCompressor(i pozwala Videoklasie po prostu być klasą danych lub fasadą dla VideoCompressor).
Doc Brown
5
@DocBrown: W tym momencie nie jest to jednak projekt obiektowy, ponieważ VideoCompressornie reprezentuje obiektu . Nie ma w tym nic złego, po prostu pokazuje limit projektowania obiektowego.
Jan Hudec
3
ale początkujący OO, w którym 1 funkcja zamienia się w klasę, wcale tak naprawdę nie jest OO i tylko zachęca do masowego oddzielenia, które kończy się na tysiącu plików klas, których nikt nie może utrzymać. Myślę, że najlepiej jest myśleć o klasie jako o „opakowaniu danych”, a nie „opakowaniu funkcji”, które pozwolą początkującemu lepiej zrozumieć, jak myśleć o programach OO.
gbjbaanb
4
@DocBrown właściwie dokładnie podkreśliło moją obawę; Rzeczywiście mam Videoklasę, ale metodologia kompresji nie jest w żaden sposób trywialna, więc właściwie podzieliłbym ją compressna wiele innych metod prywatnych. To jest część powodu mojego pytania, po przeczytaniu Nie twórz klas czasowników
yjwong
2
Myślę, że posiadanie Videoklasy może być w porządku , ale składałoby się z klas a VideoCompressor, a VideoSplitteri innych pokrewnych, które w dobrej formie OO powinny być bardzo spójnymi poszczególnymi klasami.
Eric King
-1

ISP (interfejs zasada segregacji) mówi, że klient nie powinien być zmuszony do zależeć od metody nie używa. Korzyści są liczne i wyraźne. Twoje podejście całkowicie szanuje dostawcę usług internetowych i to dobrze.

Innym podejściem również w odniesieniu do dostawcy usług internetowych jest na przykład utworzenie interfejsu dla każdej metody (lub zestawu metod o wysokiej spójności), a następnie posiadanie jednej klasy implementującej wszystkie te interfejsy. To, czy jest to lepsze rozwiązanie, zależy od scenariusza. Zaletą tego jest to, że przy użyciu wstrzykiwania zależności można mieć klienta z różnymi współpracownikami (po jednym na każdy interfejs), ale ostatecznie wszyscy współpracownicy będą wskazywać tę samą instancję obiektu.

Przy okazji, powiedziałeś

Stwarzam instancję każdej klasy

, te klasy wydają się być usługami (a zatem bezpaństwowcami). Czy myślałeś o tym, aby zrobić z nich singletony?

diegomtassis
źródło
3
Co do reszty, podczas gdy zasada separacji interfejsów jest dobrym wzorcem, to pytanie dotyczy implementacji, a nie interfejsów, więc nie jestem pewien, czy ma to znaczenie.
Jan Hudec
3
Nie zamierzam wchodzić, aby dyskutować, czy singleton jest wzorem, czy antipatternem. Zasugerowałem możliwe rozwiązanie (ma nawet nazwę) swojego problemu, od niego zależy, czy pasuje.
diegomtassis
Jeśli chodzi o to, czy dostawca usług internetowych jest odpowiedni, czy nie, pytanie brzmi: „czy klasy z tylko jedną metodą stanowią problem?”, Przeciwnie, jest to klasa z wieloma metodami, która staje się problemem w momencie, gdy tworzysz klient zależy bezpośrednio od tego ... dokładnie taki przykład Roberta Martina, który zajęło mu sformułowanie dostawcy usług internetowych.
diegomtassis
-1

Tak, jest problem. Ale nie poważne. Mam na myśli to, że możesz tak ustrukturyzować swój kod i nic złego się nie stanie, można go utrzymać. Ale istnieją pewne słabości tego rodzaju strukturyzacji. Na przykład rozważmy, czy reprezentacja twojego wideo (w twoim przypadku jest zgrupowane w klasie RawVideo) zmieni się, będziesz musiał zaktualizować wszystkie swoje klasy operacyjne. Lub weź pod uwagę, że możesz mieć wiele przedstawień wideo, które różnią się w czasie wykonywania. Następnie musisz dopasować reprezentację do konkretnej klasy „operacyjnej”. Również subiektywnie denerwujące jest przeciąganie wokół zależności dla każdej operacji, którą chcesz wykonać na czymś innym. i aktualizować listę zależności przekazywanych za każdym razem, gdy uznasz, że operacja nie jest już potrzebna lub potrzebujesz nowej operacji.

Jest to także naruszenie SRP. Niektóre osoby po prostu traktują SRP jako przewodnik do podziału obowiązków (i sięgają za daleko, traktując każdą operację odrębną odpowiedzialnością), ale zapominają, że SRP jest również przewodnikiem po grupowaniu obowiązków. I zgodnie z obowiązkami SRP, które zmieniają się z tego samego powodu, powinny być zgrupowane razem, aby w przypadku zmiany zlokalizować jak najmniejszą liczbę klas / modułów. Jeśli chodzi o duże klasy, nie ma problemu z posiadaniem wielu algorytmów w tej samej klasie, o ile algorytmy te są powiązane (tzn. Dzielą się wiedzą, która nie powinna być znana poza tą klasą). Problemem są duże klasy, które mają algorytmy, które nie są w żaden sposób powiązane i zmieniają się / zmieniają z różnych powodów.

ddiukariew
źródło