„Klasa powinna mieć tylko jeden powód do modyfikacji.”

Wprowadzenie

Zasada pojedynczej odpowiedzialności zakłada, że każda klasa powinna mieć tylko jedną odpowiedzialność (jeden powód do zmiany). Stosowanie tej zasady zwiększa ilość klas w programie, ale zmniejsza ilość klas typu „scyzoryk szwajcarski”. Takie klasy to kilkuset linijkowe klasy, które zawierają zbyt dużo funkcjonalności.

Poniżej znajdują się przykłady, które będą pomocne w zrozumieniu pierwszej zasady SOLID.

Błędna struktura klasy
class Person
{
    public string Name { get; set; }
    public string Lastname { get; set; }
    public string City { get; set; }
    public string Street { get; set; }
    public int HouseNumber { get; set; }
    public string Email { get; set; }
    
    public Person(string name, string lastname, string email)
    {
        Name = name;
        Lastname = lastname;
        Email = ValidateEmail(email);
    }
    
    private string ValidateEmail(string email) 
    {
        if (!email.Contains("@") || !email.Contains("."))
        {
            throw new FormatException("Email address has a wrong format!");
        }
        
        return email;
    }
}

Klasa Person nie spełnia w pełni założeń pierwszej zasady SOLID, ponieważ ma więcej niż jedną odpowiedzialność. Klasa Person zajmuje się przechowywaniem informacji o osobie (imię, nazwisko, adres) oraz zawiera w sobie metodę, która służy do walidacji adresu e-mail.

Aby spełnić zasadę pojedynczej odpowiedzialności powinniśmy stworzyć oddzielną klasę, która będzie odpowiedzialna za walidację adresów e-mail. Klasa Person powinna być odpowiedzialna za przechowywanie danych osobowych.

Kolejnym błędem jest to, że klasa Person zawiera atrybuty, które są powiązane z adresem zamieszkania np: City, Street, HouseNumber.

Powinniśmy dodać klasę Address, która pomoże oddzielić odpowiedzialności związane z danymi adresowymi od danych osobowych i adresu e-mail. Dzięki temu każda klasa ma teraz bardziej konkretną odpowiedzialność i jest bardziej elastyczna w przypadku przyszłych zmian i rozszerzeń.

Poprawna struktura klas
class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public int HouseNumber { get; set; }
}

class Person
{
    public string Name { get; set; }
    public string Lastname { get; set; }
    public string Email { get; set; }
    public Address PersonAddress { get; set; }
    
    public Person(string name, string lastname, string email)
    {
        Name = name;
        Lastname = lastname;
        Email = email;
    }
}

class EmailValidator
{
    public void ValidateEmail(string email)
    {
        if (!email.Contains("@") || !email.Contains("."))
        {
            throw new FormatException("Email address has a wrong format!");
        }
    }
}
BlogPost – zła struktura klasy
class BlogPost
{
    private Author $author;
    private string $title;
    private string $content;
    private \DateTime $date;
 
    // ..
 
    public function getData(): array
    {
        return [
            'author' => $this->author->fullName(),
            'title' => $this->title,
            'content' => $this->content,
            'timestamp' => $this->date->getTimestamp(),
        ];
    }
 
    public function printJson(): string
    {
        return json_encode($this->getData());
    }
 
    public function printHtml(): string
    {
        return `<article>
                    <h1>{$this->title}</h1>
                    <article>
                        <p>{$this->date->format('Y-m-d H:i:s')}</p>
                        <p>{$this->author->fullName()}</p>
                        <p>{$this->content}</p>
                    </article>
                </article>`;
    }
}

Kolejny przykład złamania zasady pojedynczej odpowiedzialności jest klasa BlogPost. Klasa BlogPost ma wiele różnych odpowiedzialności, zwłaszcza w kontekście metod printJson() oraz printHtml(). Te metody zajmują się generowaniem danych w różnych formatach (JSON i HTML).

Co powinniśmy zrobić aby została spełniona zasada pojedynczej odpowiedzialności?

Rozbijmy tą klasą na 3 odpowiedzialności:
– przechowywanie informacji o poście na blogu (autor, tytuł, treść, data),
– generowanie danych w formacie JSON,
– generowanie danych w formacie HTML.
Usuniemy metodę drukowania z klasy BlogPost. Dodamy nowy interfejs PrintableBlogPost, który będzie odpowiedzialny za wydrukowanie wpisu na blogu.

BlogPost – poprawna struktura klas
interface PrintableBlogPost
{
    public function print(BlogPost $blogPost);
}

Ten interfejs można zaimplementować na wiele sposobów. Stworzymy dwie nowe klasy, które będą implementowały interfejs PrintableBlogPost. Jedna klasa będzie odpowiedzialna za drukowanie do html, druga klasa będzie odpowiedzialna za drukowanie do formatu json.

class JsonBlogPostPrinter implements PrintableBlogPost
{
    public function print(BlogPost $blogPost) {
        return json_encode($blogPost->getData());
    }
}
 
class HtmlBlogPostPrinter implements PrintableBlogPost
{
    public function print(BlogPost $blogPost) {
        return `<article>
                    <h1>{$blogPost->getTitle()}</h1>
                    <article>
                        <p>{$blogPost->getDate()->format('Y-m-d H:i:s')}</p>
                        <p>{$blogPost->getAuthor()->fullName()}</p>
                        <p>{$blogPost->getContent()}</p>
                    </article>
                </article>`;
    }
}
Podsumowanie

Podsumowując, klasy i metody powinny być odpowiedzialne tylko za jedną funkcjonalność oprogramowania. Jednak należy pamiętać, że korzystanie z zasady SRP nie oznacza, że klasa musi mieć tylko jedną metodę i wykonywać tylko jedną rzecz. Jeżeli klasa będzie odpowiedzialna tylko za jedną funkcjonalność, to w przypadku jej modyfikacji, te zmiany nie będą wymuszać wprowadzenia zmian w innych klasach.

Pamiętajmy, że zasada pojedynczej odpowiedzialności nie odnosi się tylko do klas. Należy ją stosować także w odniesieniu do modułów, komponentów, bibliotek.

Dobrze zaprojektowane klasy sprawiają, że cała struktura naszej aplikacji jest czytelniejsza, lepiej zaprojektowana.


0 komentarzy

Dodaj komentarz

Avatar placeholder

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