Nie SOLID-nie #03: Liskov Substitution Principle

N

Seria zainspirowana bardzo dobrym kursem SOLID od Jarka Stadnickiego, dostępnym na platformie Udemy – SOLID praktyczny kurs

Nie jest to reklama, ani żadna afiliacja. Wyrażam swoje zdanie 🙂 . Polecam zerknąć. Jarek za pomocą obrazowych, trafnych porównań tłumaczy poszczególne zasady.

 


Spis postów z serii Nie SOLID-nie:

  1. Nie SOLID-nie #01: Single Responsibility Principle
  2. Nie SOLID-nie #02: Open Close Principle
  3. Nie SOLID-nie #03: Liskov Substitution Principle
  4. Nie SOLID-nie #04: Interface Segregation Principle
  5. Nie SOLID-nie #05: Dependency Inversion Principle

Definicja

Na początek, jak zwykle, odrobina teorii. Reguła Liskov brzmi:

 

Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.

 

Jak podaje Wikipedia, zasada ta została sformułowana po raz pierwszy przez Barbarę Liskov i Jannette Wing we wspólnej pracy pt. „A Behavioral Notion of Subtyping„, zaprezentowana przez Panią Liskov w przemówieniu pt. „Data Abstraction and Hierarchy„, a spopularyzowana i podana w obecnym brzmieniu przez Roberta C. Martina w artykule „Principles of Object Oriented Design” oraz książce „Agile Software Development: Principles, Patterns, and Practices

 

Research

W ramach przypomnienia i upewnienia się, czy na pewno dobrze pamiętam o co chodzi w Liskov Substitution Principle, przeczesałem internet w poszukiwaniu jakiegoś sensownego wytłumaczenia czego ta zasada dotyczy i jak jest przedstawiana przez naszych branżowych kolegów. I przyznam, że się troszkę zawiodłem. Najczęściej podawany jest przykład z figurami geometrycznymi lub wydziwiana jest jakaś inna „abstrakcja” która nie ma nic wspólnego z programowaniem. Mam na myśli przykłady takie jak Foo czy klasy Pojazd i Samochód. Postaram się opisać zasadę LSP jak najbardziej w prosty, zrozumiały sposób i jednocześnie pokazać ją używając rzeczywistego przykładu.

 

Anty-przykład

Moją propozycją są klasy SmtpSender i AlertSmtpSender. Realizują one zwykłą funkcjonalność wysyłania wiadomości mailowych.

 

Tak wygląda klasa bazowa SmtpSender

public class SmtpSender
{
    private readonly ISmtpSettings _smtpSettings;

    protected IMailTemplates MailTemplates;

    public SmtpSender(ISmtpSettings smtpSettings, IMailTemplates mailTemplates)
    {
        _smtpSettings = smtpSettings;

        MailTemplates = mailTemplates;
    }

    public virtual async Task<bool> SendMail(string toAddr, string toName,
        string mailSubject, string body)
    {
        using (var memoryStream = new MemoryStream())
        {
            using (var mailMessage = new MailMessage())
            {
                mailMessage.From = new MailAddress(_smtpSettings.SenderAddress,
                    _smtpSettings.SenderName);
                mailMessage.To.Add(new MailAddress(toAddr, toName));
                mailMessage.Subject = mailSubject;
                mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;
                mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
                mailMessage.Body = body;

                using (var smtpClient = new SmtpClient(_smtpSettings.ServerAddress,
                    _smtpSettings.ServerPort))
                {
                    var networkCredential = new NetworkCredential(_smtpSettings.UserName,
                        _smtpSettings.UserPassword);

                    smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
                    smtpClient.EnableSsl = _smtpSettings.UseSsl;
                    smtpClient.UseDefaultCredentials = false;
                    smtpClient.Credentials = networkCredential;

                    await smtpClient.SendMailAsync(mailMessage);

                    return true;
                }
            }
        }
    }
}

 

Tak wygląda klasa pochodna AlertSmtpSender

public class AlertSmtpSender : SmtpSender
{
    private readonly string _machineId;
    private readonly string _creationTime;
    private readonly string _alertName;
    private readonly string _alertInfo;
    private readonly bool _usePlainText;
    

    public string MachineId { get; set; }
    public string CreationTime { get; set; }
    public string AlertName { get; set; }
    public string AlertInfo { get; set; }
    public bool UsePlainText { get; set; }


    public AlertSmtpSender(ISmtpSettings smtpSettings, IMailTemplates mailTemplates)
        : base(smtpSettings, mailTemplates){}

    public override Task<bool> SendMail(string toAddr, string toName,
        string mailSubject, string body)
    {
        var contentReplacements = new Dictionary<string, string>
        {
            {"MACHINEID", _machineId},
            {"ALERTTIME", _creationTime.ToString(CultureInfo.CurrentUICulture)},
            {"ALERTNAME", _alertName},
            {"ALERTINFO", _alertInfo}
        };

        var subject = $"Alert:{_alertName}, machine name:{_machineId}. Subject:{mailSubject}";
        var mailBody = MailTemplates.CreateMailContent("core", "alert", _usePlainText,
            contentReplacements, body);

        return base.SendMail(toAddr, toName, subject, mailBody);
    }
}

 

Natomiast poniżej możecie zobaczyć jak wygląda definicja metody/funkcji która w parametrze przyjmuje obiekt klasy bazowej.

public async void SendNewsletter(SmtpSender sender)
        {
            await sender.SendMail("patryk.kubiela@programista-doswiadczony.pl",
                "Patryk", "Test mail", "Message body");
        }

 

Złamana reguła

W jaki sposób powyższy kod łamie regułę LSP? Otóż przede wszystkim funkcjonalność metody SendMail w klasie pochodnej, totalnie zmienia funkcjonalność metody SendMail z klasy bazowej. Teoretycznie w obu przypadkach zostanie wysłany mail. Jednak diabeł tkwi w szczegółach.

Po pierwsze, mail który zostanie wysłany będzie zupełnie inny i będzie miał inną funkcję biznesową. Jak widać, mail wysłany za pomocą klasy pochodnej, jest swoistym rodzajem alertu, a nie generycznej wiadomości. Działanie klasy pochodnej jest bardziej ograniczone, szczegółowe.

Klasa pochodna AlertSmtpSender, rzecz jasna, może rozszerzać funkcjonalność klasy bazowej, ale wedle reguły LSP, tylko w taki sposób, aby nie „zepsuć” bazowej funkcjonalności.

Po drugie, projektując metodę klasy bazowej, będziemy chcieli obsłużyć wyjątki. Już wiesz co mam na myśli? Otóż metoda SendMail, klasy pochodnej dodając „swoją” funkcjonalność, może generować inne typy wyjątków, które w pewnych wariantach mogą być nieobsłużone, przez co kłopotliwe.

De facto, metoda SendNewsletter nie spodziewa się obiektu konkretnej klasy AlertSmtpSender. Spodziewa się obiektu klasy spełniającej kontrakt (Design by Contract), jaki sugerujemy, że powinien być spełniony definiując tę metodę. Chodzi o to, żebyśmy mogli spokojnie podać jako argument obiekt klasy AlertSmtpSender, czy może jakiejś np. ErrorSmtpSender i nie bać się, że metoda SendNewsletter zostanie wywołana niepoprawnie, lub wygeneruje jakiś błąd, lub wygeneruje błąd nie taki jaki trzeba.

Oczywiście tylko w przypadku jeśli zachowaliśmy odpowiednią higienę projektując klasę pochodną, przestrzegając Liskov Substitution Principle.

Po trzecie, skoro już mówimy o zawieraniu kontraktu, może i liczba parametrów które przyjmują obie metody SendMail, jest taka sama, a ich typ ten sam, to jednak są one inne. Obie metody spodziewają się biznesowo inncyh parametrów. Przez co efekt końcowy, rezultat, będzie odbiegał od spodziewanego. Dużo łatwiej i przyznam, że lepiej, byłoby to pokazać na liczbach.

 

Słowem zakończenia

Wiem, że LSP nie mówi jedynie o funkcjach używających wskażników, czy referencji, króte mogą używać obiektów tych typów, niezależnie od tego czy jest to klasa bazowa czy pochodna. Wiem, że można (a może powinno się?) rozumieć tę regułę odrobinę szerzej, ale jestem zdania, że programista powinien wiedzieć po co stosuję regułę i czy napewno powinien ją zastosować. Tylko tyle. Zwłaszcza dzisiaj, kiedy nie ma miejsca na „sztukę dla sztuki”.

Dlatego skłaniam się ku najbardziej prostej definicji Liskov Substitution Principle. Tej, którą cytuję na samym początku.

 

Tyle i aż tyle.

 

Już pisałem, ale skoro jest to ważne, powtórzę.

Klasa pochodna może rozszerzać funkcjonalność bazowej, może ją zmieniać, natomiast ważne jest, aby nie miało to wpływu na działanie programu i wynik działania programu.

 


Programiści, tłumacząc regułę LSP, prześcigają się w wymyślaniu interpretacji tej reguły. Nie zawsze trafnie. Jeśli chcecie się szybko dowiedzieć na temat LSP czegoś więcej (dokładniej), polecam podobny post, a raczej serię postów dotyczących zasad SOLID, na blogu Tomka Sitarka.

Tak jak ja, Tomek zaczyna przygodę z blogowaniem, ale mam wrażenie, że jego wiedza jest lepiej uporządkowana, a przede wszystkim, potrafi ją lepiej ode mnie przekazać na łamach swojego bloga. LINK. Macie dzięki temu możliwość zgromadzić wiedzę z dwóch miejsc, jak i porównać dwa różne podejścia do tematu.

Polecam również inne wpisy Tomka!


 

A zatem do dzieła! Napisz komentarz! Dodaj coś od siebie!

Czołem!

 



 

About the author

Add comment

By Patryk

Autor serwisu

Patryk

Społecznościowe

Instagram

Newsletter



Historycznie

Tagi