piątek, 8 listopada 2019

SOLID - O jak Open-closed principle

Druga z zasad SOLIDa Open-closed principle mówi o tym, że klasy, moduły czy funkcje powinny być otwarte na rozszerzanie a zamknięte na modyfikacje.
Ktoś może zadać pytanie czy oznacza to, że jeżeli kod zostanie raz napisany to nie może już być zmieniony np. podczas refaktoringu? Oczywiście, że nie!
Zasadę tę lepiej odnieść do możliwości, które udostępnia nam język C#. Budując bibliotekę, dla przykładu do zapisu plików na dysku twardym, chcemy udostępnić końcowemu użytkownikowi informację w jaki sposób powinien jej używać. Najlepszym kontraktem jest interfejs, np:

    public interface IFile
    {
        void SaveFile(object content, string path);
    }


Klienci w swoich aplikacjach implementują następnie metody korzystające z metody SaveFile z naszej biblioteki. Może to być klasa zapisująca pliki w formacie XML, JSON czy jakimkolwiek innym.
Co stanie się jeżeli stwierdzimy, że nasza biblioteka podczas tworzenia pliku powinna dokonywać logowania?

    public interface IFile
    {
        void SaveFile(object content, string path, ILog logger);
    }

Każda aplikacja, korzystająca z naszej biblioteki nie będzie mogła się skompilować bez podania argumentu logger. Łamiemy tym samym zasadę Open-closed principle. Dwa powodujemy duże niezadowolenie naszych klientów, którzy muszą poświęcić czas na poprawki w kodzie.

Strategy Pattern

Wzorzec strategi pozwala w łatwy sposób zrozumieć znaczenie używania interfejsów. Mówiąc skrótowo wzorzec ten opisuje sposób jak opakować algorytmy w klasy i wymieniać ich implementację podczas działania programu. Spójrzmy na poniższy przykład:

    public class ReportProcessor
    {
        public void DoReport(string format)
        {
            var reportFormatters = new Dictionary<string, IReportFormatter>
            {
                ["xml"] = new XmlFormatter(),
                ["json"] = new JsonFormatter()
            };

            var report = new Report(reportFormatters[format]);
            report.FormatReport(someReport);
            //Rest of code
        }
    }

    public class Report
    {
        private readonly IReportFormatter _reportFormatter;

        public Report(IReportFormatter reportFormatter)
        {
            _reportFormatter = reportFormatter ?? throw new ArgumentNullException(nameof(reportFormatter));
        }

        public string FormatReport(object content)
        {
            return $"Weekly report:{Environment.NewLine}{_reportFormatter.Format(content)}";
        }
    }

    public interface IReportFormatter
    {
        string Format(object content);
    }

    public class XmlFormatter : IReportFormatter
    {
        public string Format(object content)
        {
            return "Xml format";
        }
    }

    public class JsonFormatter : IReportFormatter
    {
        public string Format(object content)
        {
            return "Json format";
        }
    }


Template Method

Drugi z wzorców projektowych, który ułatwia tworzenie kodu zgodnego z Open-close principle. Wzorzec ten definiuje szkielet algorytmu. Konkretne implementacje algorytmu (bądź jego części) zdefiniowane są w klasach dziedziczących. 
Wzorzec ten opiera się na dziedziczeniu. Można powiedzieć, że większość programistów obiektowych języków używa go na co dzień, być może nawet sobie z tego nie zdając sprawy :)
Powyższy przykład z raportem w wydaniu Template Method mógłby wyglądać jak poniżej:

    public class TemplatePattern
    {
        public void DoReport(string format)
        {
            Report xmlReport = new XmlFormatter();
            xmlReport.FormatReport(someReport);

            Report jsonReport = new JsonFormatter();
            jsonReport.FormatReport(someReport);
        }
    }

    public abstract class Report
    {
        public string FormatReport(object content)
        {
            return $"Weekly report:{FormatData(content)}";
        }

        public abstract string FormatData(object content);
    }

    public class XmlFormatter : Report
    {
        public override string FormatData(object content)
        {
            return "Xml format";
        }
    }

    public class JsonFormatter : Report
    {
        public override string FormatData(object content)
        {
            return "Json format";
        }
    }

Jak rozpoznać naruszenie tej zasady?

Odnalezienie naruszenia tej zasady jest bardzo proste. Jeżeli widzimy, że w naszej metodzie jest sporo konstrukcji if then i przy każdej edycji musimy dodać kolejną, jest to bardzo dobry kandydat do refaktoringu. Może warto wydzielić konkretny kod do klas "algorytmów" a w miejscu pierwotnym zawrzeć tylko konfigurację wyboru odpowiedniej implementacji?

Brak komentarzy:

Prześlij komentarz