Jak duży jest ok dla klasy?

29

Jestem programistą od dawna (mam 49 lat), ale raczej nowością w programowaniu obiektowym. Czytałem o OO od czasu Eiffla Bertranda Meyera, ale zrobiłem naprawdę niewiele programowania OO.

Chodzi o to, że każda książka na temat projektowania OO zaczyna się od przykładu łodzi, samochodu lub innego wspólnego obiektu, którego często używamy, i zaczynają dodawać atrybuty i metody oraz wyjaśniać, w jaki sposób modelują stan obiektu i co można zrobić za pomocą to.

Zwykle mówią coś w stylu „im lepszy model, tym lepiej reprezentuje obiekt w aplikacji i tym lepiej wychodzi”.

Jak dotąd tak dobrze, ale z drugiej strony znalazłem kilku autorów, którzy podają takie przepisy, jak: „klasa powinna zmieścić się tylko na jednej stronie” (dodałbym „jaki rozmiar monitora?”, Skoro nie próbujemy wydrukować kod!).

Weźmy na przykład PurchaseOrderklasę, która ma skończoną maszynę stanu kontrolującą jej zachowanie, i kolekcję PurchaseOrderItem, jednym z argumentów w pracy jest to, że powinniśmy użyć PurchaseOrderprostej klasy z pewnymi metodami (niewiele więcej niż klasa danych) i mieć PurchaseOrderFSM„klasy ekspert”, który obsługuje skończona maszyna stanu ds PurchaseOrder.

Powiedziałbym, że mieści się w klasyfikacji „Zazdrosna cecha” lub „Niewłaściwa intymność” postu Code Smells Jeffa Atwooda w Coding Horror. Po prostu nazwałbym to zdrowym rozsądkiem. Jeśli mogę wydać, zatwierdzić lub anulować moje zamówienie rzeczywistym zakupu, wówczas PurchaseOrderklasa powinna mieć issuePO, approvePOi cancelPOmetod.

Czy to nie idzie w parze z „maksymalizacją spójności” i „minimalizowaniem łączenia” starych zasad, które rozumiem jako fundamenty OO?

Poza tym, czy to nie pomaga w utrzymaniu klasy?

Miguel Veloso
źródło
17
Nie ma powodu, aby kodować nazwę typu w nazwie metody. Czyli jeśli masz PurchaseOrder, można po prostu wymienić swoje metody issue, approveoraz cancel. POPrzyrostek dodaje tylko wartość ujemną.
dash-tom-bang
2
Tak długo, jak trzymasz się z dala od metod czarnej dziury , powinieneś być w porządku. :)
Mark C
1
Przy obecnej rozdzielczości ekranu powiedziałbym, że ma 145 znaków.
DavRob60,
Dziękujemy wszystkim za komentarze, naprawdę pomogli mi w tym problemie. Gdyby ta strona na to pozwoliła, wybrałbym również odpowiedzi TMN i Lazarus, ale tylko jedna pozwala, więc poszła do Craiga. Z poważaniem.
Miguel Veloso,

Odpowiedzi:

27

Klasa powinna stosować zasadę pojedynczej odpowiedzialności. Większość bardzo dużych klas, które widziałem, robi wiele rzeczy, dlatego są one zbyt duże. Spójrz na każdą metodę i kod decyduje, czy powinien on należeć do tej klasy, czy osobny, duplikat kodu jest wskazówką. Być może masz metodę issuePO, ale czy zawiera ona na przykład 10 linii kodu dostępu do danych? Ten kod prawdopodobnie nie powinien tam być.

Craig
źródło
1
Craig, nie wiedziałem o zasadzie pojedynczej odpowiedzialności i po prostu o niej przeczytałem. Pierwszą rzeczą, jaka przyszła mi do głowy, był zakres odpowiedzialności? w przypadku zamówienia zakupu powiedziałbym, że zarządza jego stanem, i to jest powód dla metod issuePO, zatwierdzaniaPO i anulowaniaPO, ale obejmuje także interakcję z klasą PurchaseOrderItem, która obsługuje stan elementu zamówienia. Nie jestem więc pewien, czy to podejście jest zgodne z SRP. co byś powiedział?
Miguel Veloso
1
+1 za bardzo miłą i zwięzłą odpowiedź. Realistycznie rzecz biorąc, nie może być ostatecznej miary określającej, jak duża powinna być klasa. Stosowania, zapewnia SRP, że klasa jest tak duża, jak to musi być.
S.Robins
1
@MiguelVeloso - Zatwierdzanie zamówienia prawdopodobnie wiąże się z inną logiką i regułami niż wydawanie zamówienia. Jeśli tak, sensowne jest rozdzielenie obowiązków na POApprover i POIssuer. Chodzi o to, że jeśli reguły zmienią się dla jednego, dlaczego chcesz zadzierać z klasą, która zawiera drugą.
Matthew Flynn
12

Moim zdaniem rozmiar klasy nie ma znaczenia, o ile zmienne, funkcje i metody są odpowiednie dla danej klasy.

Mike42
źródło
1
Z drugiej strony duży rozmiar wskazuje, że metody te prawdopodobnie nie są odpowiednie dla danej klasy.
Billy ONeal,
5
@Billy: Powiedziałbym, że duże klasy mają zapach kodu, ale nie są automatycznie złe.
David Thornley,
@David: Właśnie dlatego jest tam słowo „prawdopodobnie” - to ważne :)
Billy ONeal
Musiałbym się nie zgodzić ... Pracuję nad projektem, który ma dość spójną konwencję nazewnictwa i wszystko jest dobrze ułożone ... ale nadal mam klasę, która ma 50 000 wierszy kodu. Jest po prostu za duży :)
Jay
2
Ta odpowiedź pokazuje dokładnie, jak przebiega „droga do piekła” - jeśli nie ograniczy się odpowiedzialności klasy, będzie wiele zmiennych, funkcji i metod, które stają się odpowiednie dla klasy - i łatwo prowadzi to do zbyt dużej ich liczby.
Doc Brown,
7

Największy problem z dużymi klasami dotyczy testowania tych klas lub ich interakcji z innymi klasami. Duża klasa sama w sobie nie stanowi problemu, ale jak wszystkie zapachy, wskazuje na potencjalny problem. Ogólnie rzecz biorąc, gdy klasa jest duża, oznacza to, że klasa robi więcej niż jedna klasa powinna.

Dla analogii samochodu, jeśli klasa carjest zbyt duża, prawdopodobnie oznacza to, że należy ją podzielić na klasy engine, klasy doori klasy windshielditp.

Billy ONeal
źródło
8
I nie zapominajmy, że samochód powinien implementować interfejs pojazdu. :-)
Chris
7

Nie sądzę, aby istniała konkretna definicja zbyt dużej klasy. Korzystałbym jednak z następujących wskazówek, aby ustalić, czy powinienem przeredagować moją klasę, czy ponownie zdefiniować jej zakres:

  1. Czy istnieje wiele zmiennych, które są od siebie niezależne? (Moja osobista tolerancja wynosi w większości przypadków około 10 ~ 20)
  2. Czy istnieje wiele metod zaprojektowanych dla skrzynek narożnych? (Moja osobista tolerancja to ... cóż, to zależy od mojego nastroju, jak sądzę)

W odniesieniu do 1 wyobraź sobie, że określasz rozmiar przedniej szyby, rozmiar koła, model żarówki tylnej i 100 innych szczegółów w swojej klasie car. Chociaż wszystkie są „odpowiednie” do samochodu, bardzo trudno jest prześledzić zachowanie takiej klasy car. A kiedy pewnego dnia Twój kolega przypadkowo (lub, w niektórych przypadkach, celowo) zmieni rozmiar szyby przedniej w oparciu o model żarówki tylnej, to na zawsze dowiesz się, dlaczego szyba nie pasuje już do Twojego samochodu.

W odniesieniu do 2 wyobraź sobie, że próbujesz zdefiniować wszystkie zachowania, jakie może mieć samochód w klasie car, ale car::open_roof()będzie on dostępny tylko dla kabrioletów i car::open_rear_door()nie dotyczy tych samochodów 2-drzwiowych. Chociaż samochody kabrioletowe i 2-drzwiowe są z definicji „samochodami”, wdrożenie ich w klasie carkomplikuje klasę. Klasa staje się trudniejsza w użyciu i trudniejsza w utrzymaniu.

Oprócz tych 2 wytycznych zasugerowałbym jasną definicję zakresu klasy. Po zdefiniowaniu zakresu zaimplementuj klasę ściśle na podstawie definicji. Przez większość czasu ludzie otrzymują „zbyt dużą” klasę z powodu złej definicji zakresu lub z powodu dowolnych funkcji dodanych do klasy

RRR
źródło
7

Powiedz, czego potrzebujesz w 300 liniach

Uwaga: ta ogólna zasada zakłada, że ​​stosujesz podejście „jedna klasa na plik”, które samo w sobie jest dobrym pomysłem na łatwość utrzymania

To nie jest absolutna zasada, ale jeśli widzę ponad 200 linii kodu w jednej klasie, jestem podejrzliwy. Włącza się 300 linii i dzwonków ostrzegawczych. Kiedy otwieram czyjś kod i znajduję 2000 wierszy, wiem, że to czas refaktoryzacji, nawet nie czytając pierwszej strony.

Sprowadza się to głównie do łatwości konserwacji. Jeśli Twój sprzeciw jest tak złożony, że nie możesz wyrazić jego zachowania w 300 liniach, zrozumienie go przez kogoś innego będzie bardzo trudne.

Przekonuję się, że naginam tę regułę w Modelu -> ViewModel -> Przeglądaj świat, w którym tworzę „obiekt zachowania” dla strony interfejsu użytkownika, ponieważ opisywanie pełnej serii zachowań na stronie interfejsu użytkownika zajmuje często ponad 300 linii Strona. Nadal jednak jestem nowy w tym modelu programowania i znajduję dobre sposoby na refaktoryzację / ponowne wykorzystywanie zachowań między stronami / modelami wyświetlania, co stopniowo zmniejsza rozmiar moich modeli ViewModels.

Jay Beavers
źródło
Moje osobiste wytyczne to także około 300 linii. +1
Marcie
Myślę, że OP szuka heurystyki i to jest dobre.
neontapir,
Dzięki, warto stosować dokładne liczby jako wytyczne.
Czy Sheppard
6

Przejdźmy do tego wstecz:

Czy to nie idzie w parze z „maksymalizacją spójności” i „minimalizowaniem łączenia” starych zasad, które rozumiem jako fundamenty OO?

W rzeczywistości jest to kamień węgielny programowania, którego OO jest tylko jednym paradygmatem.

Pozwolę Antoine de Saint-Exupéry odpowiedzieć na twoje pytanie (bo jestem leniwy;)):

Perfekcję osiąga się nie wtedy, gdy nie ma już nic do dodania, ale kiedy nie ma już nic do zabrania.

Im prostsza klasa, tym większa pewność, że masz rację, tym łatwiej będzie w pełni ją przetestować.

Matthieu M.
źródło
3

Jestem z OP w klasyfikacji Feature Envy. Nic nie irytuje mnie bardziej niż posiadanie szeregu klas z wieloma metodami, które działają na elementach wewnętrznych innych klas. OO sugeruje, aby modelować dane i operacje na tych danych. Nie oznacza to, że umieścisz operacje w innym miejscu niż dane! Stara się zebrać dane i te metody razem .

Przy tak powszechnych modelach cari personbyłoby to jak wprowadzenie kodu do wykonania połączenia elektrycznego, a nawet zakręcenie rozrusznika, w personklasie. Mam nadzieję, że byłoby to oczywiście złe dla każdego doświadczonego programisty.

Naprawdę nie mam idealnej wielkości klasy lub metody, ale wolę, aby moje metody i funkcje były dopasowane na jednym ekranie, aby móc zobaczyć całość w jednym miejscu. (Mam Vima skonfigurowanego do otwierania 60-liniowego okna, które akurat znajduje się dokładnie wokół liczby linii na wydrukowanej stronie.) Są miejsca, w których nie ma to większego sensu, ale działa dla mnie. Lubię stosować się do tych samych wytycznych dotyczących definicji klas i starać się, aby moje pliki były dłuższe niż kilka „stron”. Coś ponad 300, 400 linii zaczyna się nieporęczne (głównie dlatego, że klasa zaczęła przyjmować zbyt wiele bezpośrednich obowiązków).

dash-tom-bang
źródło
+1 - dobre ogólne zasady, ale nie autorytarne. Powiedziałbym jednak, że to, że klasa jest rozbita, nie oznacza, że ​​różne klasy powinny dotykać się nawzajem swoich prywatnych członków.
Billy ONeal
Nie sądzę, że różne klasy powinny kiedykolwiek dotykać swoich prywatnych części. Chociaż dostęp do „przyjaciela” jest wygodny do uruchomienia i uruchomienia, (dla mnie) jest krokiem naprzód w kierunku lepszego projektu. Ogólnie rzecz biorąc, również unikam akcesoriów, ponieważ w zależności od elementów wewnętrznych innej klasy jest inny Zapach Kodowy .
dash-tom-bang
1

Gdy definicja klasy ma kilkaset metod, jest za duża.

C Johnson
źródło
1

W pełni zgadzam się z zastosowaniem zasady pojedynczej odpowiedzialności dla klas, ale jeśli jest to przestrzegane i skutkuje dużymi klasami, niech tak będzie. Rzeczywistym celem powinno być to, aby poszczególne metody i właściwości nie były zbyt duże, cykliczna złożoność powinna być tam wskazówką, a następnie może to prowadzić do wielu prywatnych metod, które zwiększą ogólną wielkość klasy, ale pozostawiają ją czytelną i testowalną na poziomie szczegółowym. Jeśli kod pozostaje czytelny, rozmiar nie ma znaczenia.

Łazarz
źródło
1

Wystawianie zamówienia jest często skomplikowanym procesem, który wymaga więcej niż tylko zamówienia. W takim przypadku utrzymanie logiki biznesowej w osobnej klasie i uproszczenie zamówienia jest prawdopodobnie dobrym rozwiązaniem. Ogólnie jednak masz rację - nie chcesz klas „struktury danych”. Na przykład issue()metoda klasy biznesowej „POManager” prawdopodobnie powinna wywołać issue()metodę zamówienia zakupu . Organizacja producentów może następnie ustawić swój status na „Wydane” i zanotować datę wydania, podczas gdy POManager może następnie wysłać powiadomienie na rachunki do zapłaty, aby poinformować ich, że może oczekiwać faktury, oraz kolejne powiadomienie o zapasach, aby wiedzieli, że coś nadchodzi i oczekiwana data.

Każdy, kto popycha „reguły” dla metody / klasy, oczywiście nie spędził wystarczająco dużo czasu w prawdziwym świecie. Jak wspomnieli inni, często jest to wskaźnik refaktoryzacji, ale czasami (szczególnie w pracy typu „przetwarzanie danych”) naprawdę trzeba napisać klasę za pomocą jednej metody obejmującej ponad 500 linii. Nie rozłączaj się.

TMN
źródło
Myślę, że POManager byłby „kontrolerem”, a Zamówienie Zakupu byłoby „modelem” we wzorze MVC, prawda?
Miguel Veloso
0

Znalazłem kilku autorów, którzy podają takie przepisy, jak „klasa powinna zmieścić się na jednej stronie”

Jeśli chodzi o wielkość fizyczną klasy, zobaczenie dużej klasy jest oznaką zapachu kodu, ale niekoniecznie oznacza, że ​​zawsze są zapachy. (Zwykle tak jest). Jak ująłby to pirat z Piratów z Karaibów, jest to coś, co nazwalibyście „wytycznymi” niż rzeczywiste zasady. Próbowałem kiedyś ściśle przestrzegać „nie więcej niż stu LOC w klasie”, a rezultatem było mnóstwo niepotrzebnej nadmiernej inżynierii.

Zwykle mówią coś w stylu „im lepszy model, tym lepiej reprezentuje obiekt w aplikacji i tym lepiej wychodzi”.

Zwykle rozpoczynam od tego swoje projekty. Co ta klasa robi w prawdziwym świecie? Jak moja klasa powinna odzwierciedlać ten obiekt zgodnie z jego wymaganiami? Dodajemy tutaj trochę stanu, pole tam, metodę pracy na tych polach, dodajemy jeszcze kilka i voila! Mamy klasę robotniczą. Teraz wypełnij podpisy odpowiednimi implementacjami, a my jesteśmy gotowi, tak przynajmniej myślisz. Teraz mamy ładną, dużą klasę ze wszystkimi potrzebnymi funkcjami. Problem polega jednak na tym, że nie bierze się pod uwagę sposobu działania prawdziwego świata. Rozumiem przez to, że nie uwzględnia „ZMIANY”.

Powodem, dla którego klasy są najlepiej dzielone na mniejsze części, jest to, że trudno jest później zmienić duże klasy bez wpływu na istniejącą logikę lub wprowadzenia nowego błędu lub dwóch przypadkowo lub po prostu utrudniając ponowne użycie wszystkiego. To tutaj pojawiają się refaktoryzacja, wzorce projektowe, SOLID itp., A efektem końcowym jest zwykle mała klasa pracująca na innych mniejszych, bardziej szczegółowych podklasach.

Również w twoim przykładzie dodanie IssuePO, ApprovePO i Cancel PO do klasy PurchaseOrder wydaje się nielogiczne. Zamówienia zakupu nie wydają się, nie zatwierdzają ani nie anulują. Jeśli PO powinien mieć metody, wówczas metody powinny działać na swoim stanie, a nie na klasie jako całości. To naprawdę tylko klasy danych / rekordów, nad którymi pracują inne klasy.

Jonn
źródło
0

Wystarczająco duży, aby pomieścić całą logikę niezbędną do wykonania jego zadania.

Wystarczająco mały, aby można go było utrzymać i przestrzegać zasady pojedynczej odpowiedzialności .

RMalke
źródło