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
compress
metodą, która pobiera wejściowe wideo typuRawVideo
i zwraca wyjściowe wideo typuCompressedVideo
). - VideoSplitter (z
split
metodą, która pobiera wejściowe wideo typuRawVideo
i zwraca wektor 2 wyjściowych filmów wideo, każdego typuRawVideo
). - VideoIndexer (z
index
metodą, która pobiera wejściowe wideo typuRawVideo
i zwraca indeks typu wideoVideoIndex
).
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?
źródło
Odpowiedzi:
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!
źródło
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::function
lub 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? ).
źródło
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,
VideoExporter
która ostatecznie powinna być w stanie skompresować wideo. Czystym sposobem byłoby posiadanie interfejsu:które będą realizowane w następujący sposób:
i używane w ten sposób:
Zły alternatywą byłoby mieć
VideoExporter
któ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.źródło
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.
źródło
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 VideoCompressor
a następnie mieć kilka alternatywnych implementacji, na przykładclass H264Compressor implements VideoCompressor
iclass 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.źródło
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.
źródło
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ć
ale OP chce, aby działało w ten sposób:
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 domystring.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.
źródło
Video
i 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 jakVideoCompressor
(i pozwalaVideo
klasie po prostu być klasą danych lub fasadą dlaVideoCompressor
).VideoCompressor
nie reprezentuje obiektu . Nie ma w tym nic złego, po prostu pokazuje limit projektowania obiektowego.Video
klasę, ale metodologia kompresji nie jest w żaden sposób trywialna, więc właściwie podzieliłbym jącompress
na wiele innych metod prywatnych. To jest część powodu mojego pytania, po przeczytaniu Nie twórz klas czasownikówVideo
klasy może być w porządku , ale składałoby się z klas aVideoCompressor
, aVideoSplitter
i innych pokrewnych, które w dobrej formie OO powinny być bardzo spójnymi poszczególnymi klasami.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ś
, te klasy wydają się być usługami (a zatem bezpaństwowcami). Czy myślałeś o tym, aby zrobić z nich singletony?
źródło
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.
źródło