Oto pierwszy artykuł poświęcony zasadom SOLID w programowaniu obiektowym (można je również odnieść do programowania np. strukturalnego).

Na początek zajmę się zasadą, która kryje się pod pierwszą literą akronimu SOLID. W skrócie tym „S” oznacza Single Responsibility Principle (SRP), czyli Zasadę Pojedynczej Odpowiedzialności. Podstawą tego podejścia jest to, że każda klasa (lub moduł) powinna mieć tylko jedną odpowiedzialność. Robert C. Martin (znany jako Uncle Bob) sformułował SRP m.in. w sposób następujący:

Klasa powinna mieć jeden i tylko jeden powód do zmiany.<span class="su-quote-cite">Robert C. 'Uncle Bob' Martin</span>

Jest również nieco bardziej skomplikowana definicja:

Każdy moduł lub klasa powinna ponosić odpowiedzialność za pojedynczą część funkcjonalności zapewnianej przez oprogramowanie, a odpowiedzialność ta powinna być całkowicie zawarta w klasie, module lub funkcji.<span class="su-quote-cite">Robert C. 'Uncle Bob' Martin</span>

SRP — po co i dlaczego?

Klasa „od wszystkiego” może się okazać, że tak naprawdę jest do niczego. Kod, który odpowiada za wiele rzeczy na raz, wraz z powstawaniem kolejnych funkcjonalności w programie, z czasem staje się coraz trudniejszy w zrozumieniu i zarazem utrzymaniu.

Przykład z życia: są dwaj studenci – student 1 i student 2. Student 1 posiada jeden zeszyt (lub plik tekstowy), w którym zapisuje notatki ze wszystkich przedmiotów, natomiast student 2 najważniejsze informacje z każdego przedmiotu notuje w każdym zeszycie/pliku oddzielnie. Chyba nie muszę tłumaczyć, któremu z nich będzie łatwiej doszukać się niezbędnych informacji w celu przygotowania się do kolokwium/egzaminu 😊.

SRP na przykładzie

Podobnie, jak w poprzednich artykułach poświęconych dobrym praktykom, przedstawię zastosowanie SRP w przykładowym kodzie. Dla odmiany, tym razem będzie to nie Java, a C++.

Mam tutaj klasę Potato, czyli ziemniak. Jaki jest ziemniak, każdy widzi – można np. obrać go, a następnie ugotować w garnku i dodać do obiadu. Możemy też takiego ziemniaczka upiec w ognisku albo posiekać na kawałki i zrobić z niego frytki. Już mniejsza o zastosowanie tego warzywa, spójrzcie tylko, jak wygląda kod przed zastosowaniem SRP.

#ifndef SRPEXAMPLE_POTATO_H
#define SRPEXAMPLE_POTATO_H

#include <iostream>

using namespace std;


class Potato {

    string type;
    float weight;

public:
    Potato(string type, float weight) {
        this->type = type;
        this->weight = weight;
    }
    
    void peel();

    void cook();
    
    string getType();

    float getWeight();
};


#endif 

Jak widać w powyższym kodzie, nasz ziemniak potrafi sam się obrać i nawet ugotować 😆. Niby wszystko ładnie, pięknie, ale co jeśli chciałbym zrobić to samo z innym warzywem (np. marchewką)? Albo zaimplementować wiele innych metod, chociażby taką, która pokroi ziemniaka na frytki? Spójrzcie w takim razie, jak wygląda kod po refaktoryzacji.

#ifndef SRPEXAMPLE_VEGETABLE_H
#define SRPEXAMPLE_VEGETABLE_H

#include <iostream>

using namespace std;


class Vegetable {

    const string NAME;

public:
    Vegetable(string name) : NAME(name) {}

    string getName();

    virtual string getType() = 0;

    virtual float getWeight() = 0;
};


#endif

.................................

#ifndef SRPEXAMPLE_POTATO_H
#define SRPEXAMPLE_POTATO_H

#include <iostream>
#include "Vegetable.h"

using namespace std;


class Potato : public Vegetable {

    string type;
    float weight;

public:
    Potato(string type, float weight) : Vegetable("ziemniak") {
        this->type = type;
        this->weight = weight;
    }

    string getType() override;

    float getWeight() override;

};


#endif

.................................

#ifndef SRPEXAMPLE_KNIFE_H
#define SRPEXAMPLE_KNIFE_H

#include <iostream>
#include "Vegetable.h"

using namespace std;


class Knife {

    int length;

public:
    Knife(int length) {
        this->length = length;
    }

    void peel(Vegetable *vegetable);

};


#endif

.................................

#ifndef SRPEXAMPLE_COOKINGPOT_H
#define SRPEXAMPLE_COOKINGPOT_H

#include <iostream>
#include "Vegetable.h"


class CookingPot {

    float capacity;

public:
    CookingPot(float capacity) {
        this->capacity = capacity;
    }

    void cook(Vegetable *vegetable);
};


#endif 

Utworzyłem klasę abstrakcyjną Vegetable, w której umieściłem abstrakcyjne metody pobierające kolejno odmianę i wagę warzywa (stąd słowo kluczowe virtual), które są implementowane przez klasę Potato – dziedziczącej po Vegetable. Metody peelcook przeniosłem do klas KnifeCookingPot (czyli noża i garnka).

Taki kod z pewnością jest bardziej elastyczny pod względem potencjalnych zmian i łatwiejszy w utrzymaniu. Teraz każda klasa może zająć się rozwiązaniem tylko jednego problemu. Gdybym chciał zmienić jedną z klas, to już niekoniecznie będę musiał z tego powodu zmieniać inną klasę.

Podsumowanie

Zastosowanie Zasady Pojedynczej Odpowiedzialności może znacznie ułatwić uporządkowanie kodu – szczególnie pod względem odpowiedzialności poszczególnych klas/modułów. Tym niemniej, nie zawsze będzie prosta we wdrożeniu (zwykle bywa to kłopotliwe, co wiem z własnego doświadczenia 🙂). Ponadto może również się zdarzyć, że pełne zastosowanie tej zasady będzie niemożliwe, stąd programista będzie musiał ją naruszyć. Dlatego to rozwiązanie (jak wiele innych) nie jest idealne, ale na pewno bywa pomocne w wielu projektach – zarówno w samym pisaniu kodu, jak i jego późniejszym utrzymaniu.


0 komentarzy

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *