Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje

Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje Zasada jednej odpowiedzialności, zwana także zasadą jednej odpowiedzialności,
czyli zasada jednolitej zmienności - wyjątkowo śliski facet do zrozumienia i takie nerwowe pytanie na rozmowie z programistą.

Moja pierwsza poważna znajomość z tą zasadą miała miejsce na początku pierwszego roku, kiedy zabrano do lasu młode i zielone, aby z larw zrobić uczniów – prawdziwych uczniów.

W lesie zostaliśmy podzieleni na grupy po 8-9 osób i przeprowadziliśmy konkurs – która grupa najszybciej wypije butelkę wódki, pod warunkiem, że pierwsza osoba z grupy naleje wódkę do szklanki, druga ją wypije, a trzeci ma przekąskę. Jednostka, która zakończyła swoją operację, przesuwa się na koniec kolejki grupy.

Przypadek, w którym rozmiar kolejki był wielokrotnością trzech, był dobrą implementacją SRP.

Definicja 1. Pojedyncza odpowiedzialność.

Oficjalna definicja Zasady Pojedynczej Odpowiedzialności (SRP) stwierdza, że ​​każdy podmiot ma swoją odpowiedzialność i powód istnienia, a ma tylko jedną odpowiedzialność.

Rozważmy obiekt „Pijący” (Wywrót).
Aby wdrożyć zasadę SRP, podzielimy obowiązki na trzy:

  • Jeden nalewa (Operacja wlewania)
  • Jeden pije (Operacja DrinkUp)
  • Jeden ma przekąskę (Operacja TakeBite)

Każdy z uczestników procesu jest odpowiedzialny za jeden element procesu, czyli ma jedną atomową odpowiedzialność - pić, nalewać lub przekąsić.

Otwór do picia jest z kolei fasadą dla tych operacji:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje

Dlaczego?

Programista ludzki pisze kod dla małpoluda, a człowiek-małpa jest nieuważny, głupi i zawsze się spieszy. Potrafi utrzymać i zrozumieć około 3–7 terminów jednocześnie.
W przypadku pijaka istnieją trzy takie określenia. Jeśli jednak napiszemy kod na jednym arkuszu, to będzie zawierał ręce, okulary, bójki i niekończące się spory o politykę. A wszystko to będzie w treści jednej metody. Jestem pewien, że widziałeś taki kod w swojej praktyce. Nie jest to najbardziej humanitarny test dla psychiki.

Z drugiej strony małpolud został zaprojektowany tak, aby symulować w jego głowie obiekty ze świata rzeczywistego. W swojej wyobraźni może je łączyć, składać z nich nowe przedmioty i w ten sam sposób demontować. Wyobraź sobie stary model samochodu. W wyobraźni możesz otworzyć drzwi, odkręcić tapicerkę drzwi i zobaczyć tam mechanizmy podnoszenia szyby, wewnątrz których będą znajdować się koła zębate. Nie da się jednak zobaczyć wszystkich podzespołów maszyny jednocześnie, na jednym „zestawieniu”. Przynajmniej „człowiek-małpa” nie może.

Dlatego ludzcy programiści rozkładają złożone mechanizmy na zestaw mniej złożonych i działających elementów. Można go jednak rozłożyć na różne sposoby: w wielu starych samochodach kanał powietrzny idzie do drzwi, a w nowoczesnych samochodach awaria elektroniki zamka uniemożliwia uruchomienie silnika, co może być problemem podczas napraw.

teraz SRP to zasada wyjaśniająca JAK rozkładać, czyli gdzie narysować linię podziału.

Mówi, że należy dokonać rozkładu zgodnie z zasadą podziału „odpowiedzialności”, czyli według zadań poszczególnych obiektów.

Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje

Wróćmy do picia i korzyści, jakie człowiek-małpa otrzymuje podczas rozkładu:

  • Kod stał się niezwykle przejrzysty na każdym poziomie
  • Kod może pisać kilku programistów jednocześnie (każdy pisze osobny element)
  • Zautomatyzowane testowanie jest uproszczone – im prostszy element, tym łatwiej jest testować
  • Pojawia się skład kodu - możesz go zastąpić Operacja DrinkUp do operacji, podczas której pijak rozlewa płyn pod stół. Lub zastąp operację nalewania operacją polegającą na mieszaniu wina z wodą lub wódki i piwa. W zależności od wymagań biznesowych możesz zrobić wszystko bez dotykania kodu metody Ustawa Tipplera.
  • Z tych operacji możesz złożyć żarłoka (używając tylko Operacja TakeBit), Alkoholik (tylko używając Operacja DrinkUp prosto z butelki) i spełniają wiele innych wymagań biznesowych.

(Och, wygląda na to, że jest to już zasada OCP i naruszyłem odpowiedzialność za ten post)

I oczywiście wady:

  • Będziemy musieli stworzyć więcej typów.
  • Pijak pije po raz pierwszy kilka godzin później, niż by to zrobił w przeciwnym razie.

Definicja 2. Zmienność ujednolicona.

Pozwólcie, panowie! Klasa pijąca ma również jedną odpowiedzialność – pije! Ogólnie rzecz biorąc, słowo „odpowiedzialność” jest pojęciem niezwykle niejasnym. Ktoś jest odpowiedzialny za losy ludzkości i ktoś jest odpowiedzialny za wychowanie pingwinów przewróconych na biegunie.

Rozważmy dwie implementacje pijącego. Pierwsza z nich, o której mowa powyżej, zawiera trzy klasy – nalewanie, napój i przekąskę.

Drugi jest napisany zgodnie z metodologią „Naprzód i tylko naprzód” i zawiera całą logikę tej metody działać:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

Obie te zajęcia z punktu widzenia zewnętrznego obserwatora wyglądają dokładnie tak samo i dzielą tę samą odpowiedzialność za „picie”.

Dezorientacja!

Następnie wchodzimy do Internetu i znajdujemy inną definicję SRP – zasadę pojedynczej zmienności.

SCP stwierdza, że ​​„Moduł ma jeden i tylko jeden powód do zmiany„. Oznacza to, że „Odpowiedzialność jest powodem do zmiany”.

(Wygląda na to, że ludzie, którzy wymyślili pierwotną definicję, byli pewni telepatycznych zdolności człowieka-małpy)

Teraz wszystko się układa. Osobno możemy zmieniać procedury nalewania, picia i podjadania, ale w samym pijącym możemy zmieniać jedynie kolejność i skład operacji, np. przesuwając przekąskę przed wypiciem lub dodając odczytanie toastu.

W podejściu „Naprzód i tylko naprzód” wszystko, co można zmienić, zmienia się tylko w metodzie działać. Może to być czytelne i skuteczne, gdy jest mało logiki i rzadko się zmienia, ale często kończy się okropnymi metodami składającymi się z 500 wierszy każda i zawierającymi więcej stwierdzeń „jeśli”, niż jest to wymagane, aby Rosja przystąpiła do NATO.

Definicja 3. Lokalizacja zmian.

Pijący często nie rozumieją, dlaczego obudzili się w cudzym mieszkaniu lub gdzie jest ich telefon komórkowy. Czas dodać szczegółowe rejestrowanie.

Zacznijmy logowanie z procesem zalewania:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

Hermetyzując to w Operacja wlewania, postępowaliśmy mądrze z punktu widzenia odpowiedzialności i hermetyzacji, ale teraz mylimy się z zasadą zmienności. Oprócz samej operacji, która może ulec zmianie, zmienne staje się także samo logowanie. Będziesz musiał oddzielić i utworzyć specjalny rejestrator do operacji nalewania:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

Uważny czytelnik to zauważy ZalogujPo, ZalogujPrzed и Przy błędzie można również zmieniać indywidualnie i analogicznie do poprzednich kroków utworzą trzy klasy: WlaćLoggerPrzed, WlaćLoggerPo и Rejestrator błędów przelewu.

I pamiętając, że na pijącego czekają trzy operacje, otrzymujemy dziewięć klas rejestrowania. W rezultacie cały krąg picia składa się z 14 (!!!) klas.

Hiperbola? Ledwie! Człowiek-małpa z granatem rozkładającym rozbije „nalewak” na karafkę, szklankę, operatorów nalewania, wodociąg, fizyczny model zderzenia cząsteczek i przez kolejny kwartał będzie próbował rozwikłać zależności bez zmienne globalne. I uwierz mi, nie przestanie.

To właśnie w tym momencie wielu dochodzi do wniosku, że SRP to bajki z różowych królestw i odchodzi bawić się w makaron...

... nie dowiadując się nigdy o istnieniu trzeciej definicji Srp:

„Zasada pojedynczej odpowiedzialności to stwierdza rzeczy, które są podobne do zmian, powinny być przechowywane w jednym miejscu„. Lub "To, co zmienia się razem, powinno być przechowywane w jednym miejscu"

Oznacza to, że jeśli zmienimy logowanie operacji, to musimy to zmienić w jednym miejscu.

To bardzo ważny punkt - ponieważ wszystkie powyższe wyjaśnienia SRP mówiły, że konieczne jest zmiażdżenie typów podczas ich kruszenia, to znaczy nałożyły „górną granicę” wielkości obiektu, a teraz mówimy już o „dolnym limicie”. Innymi słowy, SRP wymaga nie tylko „miażdżenia podczas kruszenia”, ale także nie przesadzania – „nie miażdż zazębiających się rzeczy”. Oto wielka bitwa pomiędzy brzytwą Ockhama a małpoludem!

Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje

Teraz pijący powinien poczuć się lepiej. Oprócz tego, że nie ma konieczności dzielenia rejestratora IPourLogger na trzy klasy, możemy także połączyć wszystkie rejestratory w jeden typ:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

A jeśli dodamy czwarty typ operacji, to logowanie do niej jest już gotowe. A sam kod operacji jest czysty i wolny od zakłóceń infrastrukturalnych.

W rezultacie mamy 5 zajęć dotyczących rozwiązywania problemu picia:

  • Operacja zalewania
  • Operacja picia
  • Operacja zagłuszania
  • Rejestrator
  • Fasada pijaka

Każdy z nich odpowiada ściśle za jedną funkcjonalność i ma jeden powód zmiany. Wszystkie zasady podobne do zmian znajdują się w pobliżu.

Przykład z życia wzięty

Kiedyś napisaliśmy usługę automatycznej rejestracji klienta b2b. I pojawiła się metoda BOGA dla 200 linii o podobnej treści:

  • Przejdź do 1C i utwórz konto
  • Mając to konto przejdź do modułu płatności i tam je utwórz
  • Sprawdź, czy na serwerze głównym nie zostało utworzone konto z takim kontem
  • Stwórz nowe konto
  • Dodaj wyniki rejestracji w module płatności i numer 1c do usługi wyników rejestracji
  • Dodaj informacje o koncie do tej tabeli
  • Utwórz numer punktu dla tego klienta w usłudze punktowej. Przekaż numer konta 1c tej usłudze.

Na tej liście znajdowało się około 10 kolejnych operacji biznesowych z fatalną łącznością. Prawie każdy potrzebował obiektu konta. W połowie połączeń potrzebny był identyfikator punktu i nazwa klienta.

Po godzinie refaktoryzacji udało nam się rozdzielić kod infrastruktury i niektóre niuanse pracy z kontem na osobne metody/klasy. Metoda Boga ułatwiła to, ale pozostało 100 linii kodu, które po prostu nie chciały zostać rozplątane.

Dopiero po kilku dniach stało się jasne, że istotą tej „lekkiej” metody jest algorytm biznesowy. I że oryginalny opis specyfikacji technicznych był dość skomplikowany. I to właśnie próba rozbicia tej metody na kawałki naruszy SRP, a nie odwrotnie.

Formalizm.

Czas zostawić naszego pijaka w spokoju. Osusz łzy – na pewno kiedyś do tego wrócimy. Teraz sformalizujmy wiedzę z tego artykułu.

Formalizm 1. Definicja SRP

  1. Rozdziel elementy tak, aby każdy z nich odpowiadał za jedną rzecz.
  2. Odpowiedzialność oznacza „powód do zmiany”. Oznacza to, że każdy element ma tylko jeden powód do zmiany, jeśli chodzi o logikę biznesową.
  3. Potencjalne zmiany w logice biznesowej. musi być zlokalizowany. Elementy zmieniające się synchronicznie muszą znajdować się w pobliżu.

Formalizm 2. Niezbędne kryteria autotestu.

Nie widziałem wystarczających kryteriów spełnienia SRP. Ale są niezbędne warunki:

1) Zadaj sobie pytanie, co robi ta klasa/metoda/moduł/usługa. musisz odpowiedzieć na to pytanie za pomocą prostej definicji. ( Dziękuję Brightori )

wyjaśnienia

Czasami jednak bardzo trudno jest znaleźć prostą definicję

2) Naprawienie błędu lub dodanie nowej funkcji wpływa na minimalną liczbę plików/klas. Idealnie - jeden.

wyjaśnienia

Ponieważ odpowiedzialność (za funkcję lub błąd) jest zawarta w jednym pliku/klasie, wiesz dokładnie, gdzie szukać i co edytować. Przykładowo: funkcja zmiany wyniku operacji logowania będzie wymagała zmiany jedynie rejestratora. Nie ma potrzeby przeglądania reszty kodu.

Innym przykładem jest dodanie nowej kontrolki interfejsu użytkownika, podobnej do poprzednich. Jeśli zmusza Cię to do dodania 10 różnych elementów i 15 różnych konwerterów, wygląda na to, że przesadziłeś.

3) Jeśli kilku programistów pracuje nad różnymi funkcjami Twojego projektu, prawdopodobieństwo konfliktu scalania, czyli prawdopodobieństwo, że ten sam plik/klasa zostanie zmieniona przez kilku programistów w tym samym czasie, jest minimalne.

wyjaśnienia

Jeżeli dodając nową operację „Nalej wódkę pod stół” trzeba wpłynąć na rejestrator, operację picia i nalewania, to wygląda na to, że obowiązki są krzywo podzielone. Oczywiście nie zawsze jest to możliwe, ale powinniśmy starać się zmniejszyć tę liczbę.

4) Gdy zadasz pytanie wyjaśniające na temat logiki biznesowej (od programisty lub menedżera), przechodzisz ściśle do jednej klasy/pliku i otrzymujesz informacje tylko stamtąd.

wyjaśnienia

Funkcje, reguły lub algorytmy są pisane zwięźle, każda w jednym miejscu i nie są rozproszone flagami po całej przestrzeni kodu.

5) Nazewnictwo jest jasne.

wyjaśnienia

Nasza klasa lub metoda jest odpowiedzialna za jedną rzecz, a odpowiedzialność znajduje odzwierciedlenie w jej nazwie

AllManagersManagerService - najprawdopodobniej klasa Boga
LocalPayment - prawdopodobnie nie

Formalizm 3. Metodologia rozwoju metodą Ockhama.

Na początku projektowania małpolud nie zna i nie wyczuwa wszystkich subtelności rozwiązywanego problemu i może popełnić błąd. Błędy możesz popełniać na różne sposoby:

  • Spraw, aby obiekty były zbyt duże, łącząc różne obowiązki
  • Przeformułowanie poprzez podzielenie jednej odpowiedzialności na wiele różnych typów
  • Błędnie zdefiniuj granice odpowiedzialności

Warto pamiętać o zasadzie: „lepiej popełnić duży błąd” lub „jeśli nie jesteś pewien, nie dziel tego na części”. Jeśli na przykład twoja klasa zawiera dwa obowiązki, to nadal jest zrozumiałe i można je podzielić na dwie części przy minimalnych zmianach w kodzie klienta. Złożenie szkła z kawałków szkła jest zazwyczaj trudniejsze ze względu na rozproszenie kontekstu w kilku plikach oraz brak niezbędnych zależności w kodzie klienta.

Czas to zakończyć

Zakres SRP nie ogranicza się do OOP i SOLID. Dotyczy metod, funkcji, klas, modułów, mikroserwisów i usług. Dotyczy to zarówno rozwoju „figax-figax-and-prod”, jak i „nauki o rakietach”, dzięki czemu świat wszędzie staje się trochę lepszy. Jeśli się nad tym zastanowić, jest to niemal podstawowa zasada wszelkiej inżynierii. Inżynieria mechaniczna, systemy sterowania, a właściwie wszystkie złożone systemy budowane są z komponentów, a „niedostateczna fragmentacja” pozbawia projektantów elastyczności, „nadmierna fragmentacja” pozbawia projektantów wydajności, a nieprawidłowe granice pozbawiają ich rozsądku i spokoju ducha.

Zasada pojedynczej odpowiedzialności. Nie tak proste, jak się wydaje

SRP nie zostało wynalezione przez naturę i nie jest częścią nauk ścisłych. Przełamuje nasze biologiczne i psychologiczne ograniczenia.To po prostu sposób na kontrolowanie i rozwijanie złożonych systemów za pomocą mózgu małpy-człowieka. Mówi nam, jak rozłożyć system. Oryginalne sformułowanie wymagało sporej dawki telepatii, ale mam nadzieję, że ten artykuł rozwieje część zasłony dymnej.

Źródło: www.habr.com

Dodaj komentarz