Jaka jest prawdziwa odpowiedzialność klasy?

42

Wciąż zastanawiam się, czy uzasadnione jest używanie czasowników opartych na rzeczownikach w OOP.
Natknąłem się na ten genialny artykuł , choć nadal nie zgadzam się z jego racją.

Aby wyjaśnić problem bardziej szczegółowo, artykuł stwierdza, że ​​nie powinno być na przykład FileWriterklasy, ale ponieważ pisanie jest akcją , powinna być metodą klasy File. Zrozumiesz, że często zależy to od języka, ponieważ programista Ruby prawdopodobnie byłby przeciwny użyciu FileWriterklasy (Ruby używa metody File.opendostępu do pliku), podczas gdy programista Java nie.

Moim osobistym (i tak, bardzo skromnym) punktem widzenia jest to, że złamałoby to zasadę pojedynczej odpowiedzialności. Kiedy programowałem w PHP (ponieważ PHP jest oczywiście najlepszym językiem dla OOP, prawda?), Często używałem tego rodzaju frameworka:

<?php

// This is just an example that I just made on the fly, may contain errors

class User extends Record {

    protected $name;

    public function __construct($name) {
        $this->name = $name;
    }

}

class UserDataHandler extends DataHandler /* knows the pdo object */ {

    public function find($id) {
         $query = $this->db->prepare('SELECT ' . $this->getFields . ' FROM users WHERE id = :id');
         $query->bindParam(':id', $id, PDO::PARAM_INT);
         $query->setFetchMode( PDO::FETCH_CLASS, 'user');
         $query->execute();
         return $query->fetch( PDO::FETCH_CLASS );
    }


}

?>

Rozumiem, że przyrostek DataHandler nie dodaje nic istotnego ; ale chodzi o to, że zasada pojedynczej odpowiedzialności dyktuje nam, że obiekt używany jako model zawierający dane (może być nazywany rekordem) nie powinien również odpowiadać za wykonywanie zapytań SQL i dostępu do bazy danych. To w jakiś sposób unieważnia wzorzec ActionRecord używany na przykład przez Ruby on Rails.

Natknąłem się na ten kod C # (tak, czwarty język obiektowy użyty w tym poście) innego dnia:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

Muszę powiedzieć, że nie ma dla mnie większego sensu, aby klasa Encodinglub Charsetklasa faktycznie kodowała łańcuchy. Powinno to być jedynie reprezentacją tego, czym tak naprawdę jest kodowanie.

W związku z tym sądzę, że:

  • FileOtwieranie, odczytywanie lub zapisywanie plików nie jest klasowym obowiązkiem.
  • XmlSerializacja nie jest obowiązkiem klasowym.
  • UserZapytanie bazy danych nie jest klasowym obowiązkiem.
  • itp.

Jeśli jednak dokonamy ekstrapolacji tych pomysłów, dlaczego mielibyśmy Objectmieć toStringklasę? Przekształcanie się w sznurek nie jest obowiązkiem samochodu ani psa, prawda?

Rozumiem, że z pragmatycznego punktu widzenia pozbycie się toStringmetody upiększania przestrzegania ścisłej SOLIDNEJ formy, która sprawia, że ​​kod jest łatwiejszy do utrzymania przez uczynienie go bezużytecznym, nie jest akceptowalną opcją.

Rozumiem również, że może nie być dokładnej odpowiedzi (która byłaby bardziej esejem niż poważną odpowiedzią) na to pytanie lub że może być oparta na opiniach. Niemniej jednak nadal chciałbym wiedzieć, czy moje podejście faktycznie jest zgodne z zasadą pojedynczej odpowiedzialności.

Jaka jest odpowiedzialność klasy?

Pierre Arlaud
źródło
Obiekty toString to przede wszystkim wygoda, a toString jest zwykle używany do szybkiego przeglądania stanu wewnętrznego podczas debugowania
maniak zapadkowy
3
@ratchetfreak Zgadzam się, ale był to przykład (prawie żart) pokazujący, że pojedyncza odpowiedzialność jest bardzo często łamana w sposób, który opisałem.
Pierre Arlaud,
8
Chociaż zasada pojedynczej odpowiedzialności ma swoich gorących zwolenników, moim zdaniem jest to w najlepszym razie wytyczna, a przy tym bardzo luźna w przypadku przeważającej większości wyborów projektowych. Problem z SRP polega na tym, że kończysz na setkach dziwnie nazwanych klas, sprawia to, że naprawdę trudno jest znaleźć to, czego potrzebujesz (lub dowiedzieć się, czy to, czego potrzebujesz, już istnieje) i uchwycić ogólny obraz. Wolę mieć mniej dobrze nazwanych zajęć, które natychmiast mówią mi, czego się od nich spodziewać, nawet jeśli mają spore obowiązki. Jeśli w przyszłości coś się zmieni, podzielę klasę.
Dunk

Odpowiedzi:

24

Biorąc pod uwagę pewne rozbieżności między językami, może to być trudny temat. Tak więc formułuję następujące komentarze w sposób, który stara się być tak wszechstronny, jak to możliwe w sferze OO.

Po pierwsze, tak zwana „zasada pojedynczej odpowiedzialności” jest odruchem - wyraźnie zadeklarowanym - spójności koncepcji . Czytając literaturę tamtych czasów (około 70 lat) ludzie (i nadal są) starali się zdefiniować, czym jest moduł i jak je zbudować w sposób, który zachowałby ładne właściwości. Powiedzieliby więc, że „tutaj jest kilka struktur i procedur, zrobię z nich moduł”, ale bez żadnych kryteriów, dlaczego ten zestaw arbitralnych rzeczy jest spakowany razem, organizacja może nie mieć sensu - mała „spójność”. Stąd pojawiły się dyskusje na temat kryteriów.

Pierwszą rzeczą, na którą należy tutaj zwrócić uwagę, jest to, że do tej pory debata dotyczyła organizacji i związanych z tym skutków dla utrzymania i zrozumiałości (w przypadku komputera niewiele, jeśli moduł „ma sens”).

Potem wszedł ktoś inny (pan Martin) i zastosował to samo myślenie do jednostki klasy jako kryterium, które należy zastosować, myśląc o tym, co powinno lub nie powinno do niej należeć, promując te kryteria do zasady, która jest omawiana tutaj. Podkreślił, że „klasa powinna mieć tylko jeden powód do zmiany” .

Cóż, wiemy z doświadczenia, że ​​wiele obiektów (i wiele klas), które wydają się robić „wiele rzeczy”, ma bardzo dobry powód do tego. Niepożądanym przypadkiem byłyby klasy, które są rozdęte funkcjonalnością do tego stopnia, że ​​są nieprzenikalne w utrzymaniu itp. I zrozumienie tego drugiego oznacza sprawdzenie, gdzie pan. Martin dążył do tego, kiedy opracowywał ten temat.

Oczywiście po przeczytaniu tego, co pan. Martin napisał, że powinno być jasne, że są to kryteria kierunku i projektu, aby uniknąć problematycznych scenariuszy, a nie w jakikolwiek sposób dążyć do jakiegokolwiek rodzaju zgodności, nie mówiąc już o silnej zgodności, szczególnie gdy „odpowiedzialność” jest źle zdefiniowana (a pytania typu „czy to robi” narusza zasadę? ”są idealnymi przykładami powszechnego zamieszania). Dlatego uważam za niefortunne, że nazywa się to zasadą, wprowadzając ludzi w błąd, próbując doprowadzić do ostatnich konsekwencji, w których nie przyniosłoby to pożytku. Sam Martin omawiał projekty, które „robią więcej niż jedną rzecz”, które prawdopodobnie powinny być zachowane w ten sposób, ponieważ rozdzielenie przyniosłoby gorsze wyniki. Ponadto istnieje wiele znanych wyzwań dotyczących modułowości (i ten temat jest tego przypadkiem), nie jesteśmy w stanie uzyskać dobrych odpowiedzi, nawet na kilka prostych pytań na ten temat.

Jeśli jednak ekstrapolujemy te pomysły, to dlaczego Object miałby mieć klasę toString? Przekształcanie się w sznurek nie jest obowiązkiem samochodu ani psa, prawda?

Pozwólcie, że zatrzymam się, aby powiedzieć coś o tym toString: istnieje fundamentalna rzecz często zaniedbywana, gdy dokonuje się przejścia myśli z modułów do klas i zastanawia się, jakie metody powinny należeć do klasy. Chodzi o dynamiczną wysyłkę (aka, późne wiązanie, „polimorfizm”).

W świecie, w którym nie ma „metod zastępujących”, wybór pomiędzy „obj.toString ()” lub „toString (obj)” jest kwestią samej preferencji składni. Jednak w świecie, w którym programiści mogą zmieniać zachowanie programu, dodając podklasę z wyraźną implementacją istniejącej / zastąpionej metody, wybór ten nie jest już gustem: uczynienie procedury metodą może również uczynić ją kandydatem do zastąpienia, i to samo może nie dotyczyć „bezpłatnych procedur” (języki, które obsługują wiele metod, mają wyjście z tej dychotomii). W związku z tym nie jest to już tylko dyskusja na temat organizacji, ale także na temat semantyki. Wreszcie, do której klasy jest przypisana metoda, staje się również decydującą decyzją (w wielu przypadkach do tej pory mamy niewiele więcej niż wytyczne, które pomogą nam zdecydować, do czego należą rzeczy,

Wreszcie, mamy do czynienia z językami, które podejmują okropne decyzje projektowe, na przykład zmuszając do stworzenia klasy dla każdego drobiazgu. Tak więc, co kiedyś było kanonicznym powodem i głównymi kryteriami posiadania obiektów (a zatem w klasie, a więc klasach), czyli posiadania tych „obiektów”, które są rodzajem „zachowań, które zachowują się jak dane” , ale chroń ich konkretną reprezentację (jeśli w ogóle) przed bezpośrednią manipulacją za wszelką cenę (i to jest główna wskazówka dotycząca tego, co powinno być interfejsem obiektu z punktu widzenia jego klientów), zostaje zamazane i zdezorientowane.

Thiago Silva
źródło
31

[Uwaga: Będę tu mówić o przedmiotach. Przede wszystkim chodzi o programowanie obiektowe, a nie klasy.]

Odpowiedzialność za obiekt zależy głównie od modelu domeny. Zwykle istnieje wiele sposobów modelowania tej samej domeny, a ty wybierzesz jeden lub drugi sposób w zależności od tego, w jaki sposób system będzie używany.

Jak wszyscy wiemy, metodologia „czasownik / rzeczownik”, której często naucza się na kursach wprowadzających, jest absurdalna, ponieważ tak bardzo zależy od tego, jak sformułujesz zdanie. Możesz wyrazić prawie wszystko jako rzeczownik lub czasownik, głosem aktywnym lub pasywnym. Zależność od modelu domeny jest niewłaściwa, powinien być świadomym wyborem projektu, czy coś jest przedmiotem lub metodą, a nie tylko przypadkową konsekwencją sformułowania wymagań.

Jednak to nie wskazują, że, tak jak masz wiele możliwości wyrażania samego w języku angielskim, masz wiele możliwości wyrażania samo w modelu domeny ... i żadna z tych możliwości jest z natury bardziej poprawne niż inni. To zależy od kontekstu.

Oto przykład, który jest również bardzo popularny we wprowadzających kursach OO: konto bankowe. Który jest zwykle modelowany jako obiekt z balancepolem i transfermetodą. Innymi słowy: saldo konta to dane , a przelew to operacja . To rozsądny sposób na modelowanie konta bankowego.

Tyle że nie tak modeluje się konta bankowe w prawdziwym oprogramowaniu bankowym (a tak naprawdę banki nie działają w prawdziwym świecie). Zamiast tego masz kupon transakcji, a saldo konta jest obliczane przez dodanie (i odjęcie) wszystkich kuponów transakcji dla konta. Innymi słowy: transfer to dane, a saldo to operacja ! (Co ciekawe, sprawia to, że twój system jest całkowicie funkcjonalny, ponieważ nigdy nie musisz modyfikować salda, zarówno obiekty konta, jak i obiekty transakcji nigdy nie muszą się zmieniać, są niezmienne).

Co do twojego konkretnego pytania toString, zgadzam się. Bardzo wolę rozwiązanie Haskell Showable klasy typu . (Scala uczy nas, że klasy typów pięknie pasują do OO.) To samo z równością. Równość bardzo często nie jest własnością obiektu, ale kontekstu, w którym obiekt jest używany. Pomyśl tylko o liczbach zmiennoprzecinkowych: jaki powinien być epsilon? Znowu Haskell ma Eqklasę typu.

Jörg W Mittag
źródło
Lubię twoje porównania Haskell, dzięki czemu odpowiedź jest trochę bardziej… świeża. Co byś powiedział na temat projektów, które pozwalają ActionRecordbyć zarówno modelem, jak i requesterem bazy danych? Bez względu na kontekst wydaje się, że łamie zasadę pojedynczej odpowiedzialności.
Pierre Arlaud,
5
„... ponieważ tak bardzo zależy od tego, jak sformułujesz zdanie”. Tak! Och, i kocham twój przykład bankowy. Nigdy nie wiedziałem, że już w latach osiemdziesiątych uczyłem się programowania funkcyjnego :) (projektowanie zorientowane na dane i niezależne od czasu przechowywanie, gdzie wagi itp. Są obliczalne i nie powinny być przechowywane, a czyjś adres nie powinien być zmieniany, ale przechowywany jako nowy fakt) ...
Marjan Venema
1
Nie powiedziałbym, że metodologia Czasownik / Rzeczownik jest „niedorzeczna”. Powiedziałbym, że uproszczone projekty zawiodą i że bardzo trudno jest je naprawić. Ponieważ, na przykład, czy interfejs API RESTful nie jest metodologią czasownik / rzeczownik? Gdzie, dla dodatkowego stopnia trudności, czasowniki są bardzo ograniczone?
user949300,
@ user949300: Fakt, że zestaw czasowników jest naprawiony, dokładnie unika wspomnianego problemu, a mianowicie, że można formułować zdania na wiele różnych sposobów, tym samym uzależniając to, co jest rzeczownikiem, a co czasownikiem od stylu pisania, a nie analizy domeny .
Jörg W Mittag
8

Zbyt często Zasada Jednej Odpowiedzialności staje się Zasadą Zero Odpowiedzialności, a ty kończysz się Anemicznymi Klasami , które nic nie robią (z wyjątkiem seterów i pobierających), co prowadzi do katastrofy Królestwa Nouns .

Nigdy nie osiągniesz ideału, ale lepiej, aby klasa zrobiła trochę za dużo niż za mało.

W twoim przykładzie z kodowaniem, IMO, zdecydowanie powinno być w stanie kodować. Co powinnaś zamiast tego zrobić? Samo posiadanie nazwy „utf” to zero odpowiedzialności. Może nazwa powinna być Enkoderem. Ale, jak powiedział Konrad, dane (które kodują) i zachowanie (robienie tego) należą do siebie .

użytkownik949300
źródło
7

Instancja klasy jest zamknięciem. Otóż ​​to. Jeśli myślisz w ten sposób, wszystkie dobrze zaprojektowane oprogramowanie, na które patrzysz, będzie wyglądać dobrze, a każde źle przemyślane oprogramowanie nie będzie. Pozwól mi się rozwinąć.

Jeśli chcesz napisać coś do zapisu do pliku, pomyśl o wszystkim, co musisz określić (do systemu operacyjnego): nazwa pliku, metoda dostępu (odczyt, zapis, dołącz), ciąg, który chcesz zapisać.

Konstruujesz obiekt File za pomocą nazwy pliku (i metody dostępu). Obiekt File jest teraz zamknięty nad nazwą pliku (w tym przypadku prawdopodobnie przyjąłby go jako wartość tylko do odczytu / const). Instancja File jest teraz gotowa do przyjmowania wywołań metody „write” zdefiniowanej w klasie. To po prostu pobiera argument łańcuchowy, ale w treści metody write, implementacji, istnieje również dostęp do nazwy pliku (lub uchwytu pliku, który został z niego utworzony).

Wystąpienie klasy File powoduje zatem umieszczenie niektórych danych w jakimś złożonym obiekcie blob, dzięki czemu późniejsze użycie tego wystąpienia jest prostsze. Kiedy masz obiekt File, nie musisz się martwić o nazwę pliku, martwisz się tylko o to, jaki ciąg wstawisz jako argument do wywołania write. Powtórzmy - obiekt File zawiera wszystko, czego nie musisz wiedzieć o tym, dokąd właściwie idzie łańcuch, który chcesz napisać.

Większość problemów, które chcesz rozwiązać, jest podzielona na warstwy w ten sposób - rzeczy tworzone z góry, rzeczy tworzone na początku każdej iteracji jakiegoś rodzaju pętli procesowej, następnie różne rzeczy deklarowane w dwóch połówkach if if, a następnie rzeczy tworzone w początek jakiejś sub-pętli, potem rzeczy zadeklarowane jako część algorytmu w tej pętli itp. Gdy dodajesz coraz więcej wywołań funkcji do stosu, tworzysz coraz bardziej szczegółowy kod, ale za każdym razem będziesz mieć ułożył dane w warstwach niżej w stos, tworząc jakąś ładną abstrakcję, która ułatwia dostęp. Zobacz, co robisz, używając „OO” jako pewnego rodzaju struktury zarządzania zamykaniem dla funkcjonalnego podejścia programistycznego.

Skrótowe rozwiązanie niektórych problemów wiąże się ze zmiennym stanem obiektów, który nie jest tak funkcjonalny - manipulujesz zamknięciami z zewnątrz. Robisz niektóre rzeczy w swojej klasie „globalne”, przynajmniej w powyższym zakresie. Za każdym razem, gdy piszesz setera - staraj się ich unikać. Twierdzę, że w tym przypadku odpowiedzialność za swoją klasę lub inne klasy, w których ona działa, jest błędna. Ale nie przejmuj się zbytnio - czasami pragmatyczne jest wprowadzanie nieco zmiennego stanu, aby szybko rozwiązać niektóre rodzaje problemów, bez nadmiernego obciążenia poznawczego i faktycznie cokolwiek zrobić.

Podsumowując, aby odpowiedzieć na twoje pytanie - jaka jest prawdziwa odpowiedzialność klasy? Ma to na celu zaprezentowanie platformy opartej na pewnych podstawowych danych w celu osiągnięcia bardziej szczegółowego rodzaju operacji. Jest to hermetyzacja połowy danych zaangażowanych w operację, która zmienia się rzadziej niż druga połowa danych (myśl curry ...). Np. Nazwa pliku vs ciąg do zapisania.

Często te enkapsulacje wyglądają powierzchownie jak rzeczywisty obiekt / obiekt w domenie problemowej, ale nie daj się zwieść. Kiedy tworzysz klasę samochodów, tak naprawdę nie jest to samochód, to zbiór danych, który tworzy platformę, na której można osiągnąć pewne rzeczy, które możesz chcieć zrobić w samochodzie. Utworzenie reprezentacji ciągu (toString) to jedna z tych rzeczy - wyplucie wszystkich wewnętrznych danych. Pamiętaj, że czasami klasa samochodów może nie być odpowiednią klasą, nawet jeśli domeną problemową są samochody. W przeciwieństwie do Królestwa rzeczowników, są to operacje, czasowniki, na których powinna opierać się klasa.

Benedykt
źródło
4

Rozumiem również, że może nie być dokładnej odpowiedzi (która byłaby bardziej esejem niż poważną odpowiedzią) na to pytanie lub że może być oparta na opiniach. Niemniej jednak nadal chciałbym wiedzieć, czy moje podejście faktycznie jest zgodne z zasadą pojedynczej odpowiedzialności.

Chociaż tak się dzieje, niekoniecznie musi stanowić dobry kod. Poza prostym faktem, że jakakolwiek ślepo przestrzegana reguła prowadzi do złego kodu, zaczynasz od niewłaściwej przesłanki: obiekty (programujące) nie są obiektami (fizycznymi).

Obiekty to zestaw spójnych pakietów danych i operacji (a czasem tylko jeden z dwóch). Podczas gdy często modelują one rzeczywiste obiekty świata, różnica między komputerowym modelem czegoś a samą rzeczą wymaga różnic.

Wybierając tę ​​twardą linię między „rzeczownikiem”, który reprezentuje rzecz, a innymi rzeczami, które ją konsumują, stajesz w obliczu kluczowej korzyści z programowania obiektowego (posiadanie funkcji i stanu razem, aby funkcja mogła chronić niezmienniki stanu) . Co gorsza, próbujesz przedstawić świat fizyczny w kodzie, który, jak pokazała historia, nie będzie działał dobrze.

Telastyn
źródło
4

Natknąłem się na ten kod C # (tak, czwarty język obiektowy użyty w tym poście) innego dnia:

byte[] bytes = Encoding.Default.GetBytes(myString);
myString = Encoding.UTF8.GetString(bytes);

Muszę powiedzieć, że nie ma dla mnie większego sensu, aby klasa Encodinglub Charsetklasa faktycznie kodowała łańcuchy. Powinno to być jedynie reprezentacją tego, czym tak naprawdę jest kodowanie.

Teoretycznie tak, ale myślę, że C # stanowi uzasadniony kompromis ze względu na prostotę (w przeciwieństwie do bardziej rygorystycznego podejścia Javy).

Jeśli Encodingtylko reprezentowane (lub zidentyfikowane) - powiedzmy pewnej kodowanie UTF-8 - czym chcesz oczywiście potrzebujemy Encoderklasę, też tak, że można wdrożyć GetBytesna nim - ale wtedy trzeba zarządzać relacji między EncoderS i EncodingS, więc skończyć z naszym dobrym starym

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

tak genialnie zaśmiecone w tym artykule, do którego linkujesz (przy okazji, świetna lektura).

Jeśli dobrze cię rozumiem, pytasz, gdzie jest linia.

Cóż, po przeciwnej stronie spektrum znajduje się podejście proceduralne, które nie jest trudne do wdrożenia w językach OOP za pomocą klas statycznych:

HelpfulStuff.GetUTF8Bytes(myString)

Dużym problemem jest to, że kiedy autor HelpfulStuffodchodzi i ja siedzę przed monitorem, nie mam pojęcia, gdzie GetUTF8Bytesjest wdrożony.

Nie patrzę na to w HelpfulStuff, Utils, StringHelper?

Panie, pomóż mi, jeśli metoda jest faktycznie wdrożona niezależnie we wszystkich trzech z tych ... co się często zdarza, wystarczy, że facet przede mną nie wiedział, gdzie szukać tej funkcji, i wierzył, że nie ma żadnej ale oni wybrali kolejną i teraz mamy ich w obfitości.

Dla mnie właściwy projekt nie polega na spełnianiu abstrakcyjnych kryteriów, ale na mojej łatwości kontynuowania po tym, jak usiądę przed twoim kodem. Teraz jak to robi

EncodersFactory.getDefaultFactory().createEncoder(new UTF8Encoding()).getBytes(myString)

oceń w tym aspekcie? Źle też. To nie jest odpowiedź, którą chcę usłyszeć, gdy krzyczę do mojego kolegi z pracy „jak zakodować ciąg w sekwencji bajtów?” :)

Więc wybrałbym umiarkowane podejście i będę liberalny w kwestii pojedynczej odpowiedzialności. Wolałbym, aby Encodingklasa zarówno identyfikowała rodzaj kodowania, jak i wykonywała faktyczne kodowanie: dane niepowiązane z zachowaniem.

Myślę, że to pasuje do wzoru fasady, który pomaga nasmarować koła zębate. Czy ten legalny wzór narusza SOLID? Powiązane pytanie: czy wzór elewacji narusza SRP?

Zauważ, że może tak być wewnętrznie implementowane pobieranie bajtów kodowanych (w bibliotekach .NET). Klasy po prostu nie są publicznie widoczne i tylko ten „fasada” API jest odsłonięty na zewnątrz. Nie jest tak trudno zweryfikować, czy to prawda, jestem po prostu trochę zbyt leniwy, aby to zrobić teraz :) Prawdopodobnie nie jest, ale to nie unieważnia mojej tezy.

Możesz uprościć publicznie widoczną implementację ze względu na przyjazność, zachowując bardziej surową, kanoniczną implementację wewnętrznie.

Konrad Morawski
źródło
1

W Javie abstrakcja używana do reprezentowania plików jest strumieniem, niektóre strumienie są jednokierunkowe (tylko do odczytu lub tylko do zapisu), inne są dwukierunkowe. Dla Ruby klasa File to abstrakcja reprezentująca pliki systemu operacyjnego. Pytanie więc, jaka jest jego jedyna odpowiedzialność? W Javie FileWriter odpowiada za zapewnienie jednokierunkowego strumienia bajtów podłączonego do pliku systemu operacyjnego. W Ruby, odpowiedzialnością File jest zapewnienie dwukierunkowego dostępu do pliku systemowego. Oboje wypełniają SRP, po prostu mają różne obowiązki.

Przekształcanie się w sznurek nie jest obowiązkiem samochodu ani psa, prawda?

Jasne, czemu nie? Oczekuję, że klasa samochodów będzie w pełni reprezentować abstrakcję samochodu. Jeśli ma sens zastosowanie abstrakcji samochodu tam, gdzie potrzebny jest ciąg, to w pełni oczekuję, że klasa Car będzie obsługiwać konwersję na ciąg.

Aby odpowiedzieć bezpośrednio na większe pytanie, obowiązkiem klasy jest działanie jako abstrakcja. To zadanie może być skomplikowane, ale o to właśnie chodzi, abstrakcja powinna ukrywać złożone rzeczy za łatwiejszym w użyciu, mniej złożonym interfejsem. SRP jest wytyczną projektową, więc skupiasz się na pytaniu „co reprezentuje ta abstrakcja?”. Jeśli wiele różnych zachowań jest koniecznych do pełnego wyodrębnienia pojęcia, niech tak będzie.

kamieniste
źródło
0

Klasa może mieć wiele obowiązków. Przynajmniej w klasycznym OOP zorientowanym na dane.

Jeśli zastosujesz zasadę pojedynczej odpowiedzialności w pełnym zakresie, otrzymasz projekt zorientowany na odpowiedzialność, w którym każda metoda niezwiązana z pomocą należy do własnej klasy.

Wydaje mi się, że nie ma nic złego w wydobywaniu złożoności z klasy podstawowej, jak w przypadku File i FileWriter. Zaletą jest to, że uzyskujesz wyraźne oddzielenie kodu odpowiedzialnego za określoną operację, a także że możesz zastąpić (podklasę) tylko ten fragment kodu zamiast zastępować całą klasę bazową (np. Plik). Niemniej jednak stosowanie tego systematycznie wydaje mi się przesadą. Po prostu dostajesz znacznie więcej klas do obsługi i dla każdej operacji musisz wywołać odpowiednią klasę. Elastyczność wiąże się z pewnym kosztem.

Te wyodrębnione klasy powinny mieć bardzo opisowych nazw oparte na działaniu, które zawierają, na przykład <X>Writer, <X>Reader, <X>Builder. Nazwy jak <X>Manager, w <X>Handlerogóle nie opisują operacji, a jeśli są tak nazywane, ponieważ zawierają więcej niż jedną operację, to nie jest jasne, co udało się osiągnąć. Oddzieliłeś funkcjonalność od jej danych, nadal łamiesz zasadę pojedynczej odpowiedzialności, nawet w tej wyodrębnionej klasie, a jeśli szukasz określonej metody, możesz nie wiedzieć, gdzie jej szukać (jeśli jest w <X>lub <X>Manager).

Teraz twoja sprawa z UserDataHandler jest inna, ponieważ nie ma klasy bazowej (klasy, która zawiera rzeczywiste dane). Dane dla tej klasy są przechowywane w zewnętrznym zasobie (bazie danych) i używasz interfejsu API, aby uzyskać do nich dostęp i manipulować nimi. Twoja klasa użytkownika reprezentuje użytkownika wykonawczego, który jest naprawdę czymś innym niż użytkownik stały, a rozdzielenie obowiązków (obaw) może się tu przydać, szczególnie jeśli masz dużo logiki związanej z wykonaniem użytkownika.

Prawdopodobnie tak nazwałeś UserDataHandler, ponieważ w tej klasie jest po prostu tylko funkcjonalność i nie ma prawdziwych danych. Możesz jednak również wyciągnąć wnioski z tego faktu i uznać dane w bazie danych za należące do klasy. Dzięki tej abstrakcji możesz nazwać klasę na podstawie tego, czym jest, a nie na podstawie tego, co robi. Możesz nadać mu nazwę UserRepository, co jest dość sprytne i sugeruje użycie wzorca repozytorium .

klimat
źródło