Какво е SOLID?
Това са 5 принципа в обектно-ориентираното програмиране, като първата буква на всеки един от тях образува SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
Тези принципи, когато се комбинират заедно, улесняват разработването на лесно поддържан и разширяем софтуер.
Преди да преминем на въпроса, нека първо да обобщим SOLID принципите:
Single Responsibility Principle (SRP) / Принцип за единствена отговорност – Един клас трябва да има една единствена причина да бъде променен.
Open-Closed Principle (OCP) / Принцип отворен/затворен – Един клас трябва да бъде отворен за разширение, но затворен за модификации.
Liskov Substitution Principle (LSP) / Принцип на заместване на Лисков – Всеки един клас трябва да може да бъде заменен с подклас без извикващият го код да знае за промяната.
Interface Segregation Principle (ISP) / Принцип за разделяне на интерфейсите – Повече специфични интерфейси са по-добре, отколкото един общ интерфейс.
Dependency Inversion Principle (DIP) / Принцип на обръщане на зависимостите – Кодът (класът) трябва да зависи от абстракции, а не от конкретни имплементации.
Принципът Single Responsibility
Този принцип може да определи дали един клас изпълнява твърде много задачи. Класът трябва да прави едно нещо и само едно нещо, и да го прави добре. И ако един клас прави едно нещо, много по-лесно е кодът да се разбере, когато се чете от човек. Вероятността този код в бъдеще да се промени е минимална, но дори и това да се наложи, най-вероятно няма да възникнат непредвидени странични ефекти от тази промяна.
Но не винаги е ясно каква задача трябва да изпълнява даден код.
Ето един пример. От много години разработчиците създават класове за работа с бази данни. Тези класове са се използвали за операции като Създаване, Четене, Промяна и Изтриване на данни.
Един ден някой направил заключение, че Създаването, Промяната и Изтриването си приличат в това, че променят данните.
Четенето е различно действие.
То не променя данните, а просто ни казва какви са те. Затова следвайки принципа, това действие трябва да е в отделен клас. След всичко това, те казали, че много по-вероятно е да се наложи да се промени кода, който чете данните за различни цели, отколкото да се промени при промяна на данните.
Може да имаме нужда от един ред, много редове, обобщени данни, данни, сортирани по различни критерии, суми, брой редове, групиране и други. Това е довело до понятието, наречено Command Query Responsibility Separation (CQRS).
Други хора се противопоставили на тази идея, твърдейки, че това е задача за достъп до данни, поради тази причина трябва да бъде в един клас.
Кое твърдение е правилно?
Отговорът е, че и двете твърдения са верни. Принципът CQRS може да доведе до твърде ненужна сложност.
Като общо правило, ако трябма да се промени код на две или повече места, в един или няколко класове, за да се поправи едно нещо (проблем, бъг), тази част от кода може би трябва да се изнесе в отделни класове, следователно трябва да се приложи принципа Single Responsibility.
Принципът Open-Closed
Този принцип се отнася до добавянето на нова функционалност в една програма без да се променя вече написан код или дори асембли. Причината за това е, че всеки път, когато се променя даден код, се появява и риска от възникването на бъгове в съществуваща функционалност.
Нека да дадем за пример класа String в .NET.
Има много операции, които можем да изпълним върху един низ. Имаме string.Length, string.Split, string.Substring и много други. Какво ще направим, ако трябва да обърнем всички символи в даден низ, така че вместо Dot Net трябва да получим teN toD?
Ако променим класа String, съществува вероятност случайно да модифицираме съществуващ код. Вместо това, по-добре е да създадем разширен метод, които да съществува в друго асембли.
Принципът Liskov Substitution
Сега нека погледнем странно наречения принцип Liskov Substitution.
Този принцип казва, че яко заменим клас, който използваме, с подклас, кодът трябва да продължи да работи.
Общ пример за това е клас за логване на събития. Нека помислим за места, където може да се записват събитията. Това може да бъде база данни, файл на диска, уеб услуга, използвайки Windows Event Log и други. Файловете на диска могат да се записват в различни формати: чист текст, JSON, XML, HTML и други.
Но кодът, който извиква метода за запис, не трябва да извиква различни методи за всеки един от вариантите за запис на събитията. Той трябва винаги да извиква един метод, а специфичната инстанция на класа, който ще записва събитията, трябва да се погрижи за останалите действия.
Принципът Interface Segregation
Някога обръщали ли сте внимание на различните интерфейси в класа List в .Net?
Класът List имплементира 8 различни интерфейса.
Защо го прави? За да се съобрази с четвъртия принцип на SOLID – Interface Segregation.
Помислете за функционалността, дефинирана във всеки един от тези интерфейси. Сега си представете ако тези функционалности бяха дефинирани в един общ интерфейс. Колко сложен би бил този интерфейс? Колко различни неща би правил този общ интерфейс? Би било невъзможно този интерфейс да бъде поддържан.
А сега си представете ако трябва да имате клас, който няма нужда да имплементира интерфейса IReadOnlyList. Как бихте постигнали това? Основно, бихте имали код, който не прави нищо. А защо ви е да имате такъв код?
Решението на проблема е да имаме много специализирани интерфейси, за да може потребителите да зависят само от този интерфейс, от който имат нужда.
Принципът Dependency Inversion
Нека се върнем на горния пример за запис на събития. Как указваме на класа кой точно запис да използва? Това е работа на принципа Dependency Inversion.
Когато инстанцираме клас, ние подаваме инстанция на класа, които искаме да използваме през конструктора.
public interface ILogger
{
void WriteLog(string message);
}
public class DatabaseLogger : ILogger
{
public void WriteLog(string message)
{
// Format message
// Build parameter
// Open connection
// Send command
// Close connection
}
}
public class TextFileLogger : ILogger
{
public void WriteLog(string message)
{
// Format message
// Open text file
// Write message
// Close text file
}
}
public class Main
{
public Main()
{
var processor = new Processor(new TextFileLogger());
}
}
public class Processor
{
private readonly ILogger _logger;
public Processor(ILogger logger)
{
_logger = logger;
}
public void RunProcessing()
{
try
{
// Do the processing
}
catch (Exception ex)
{
_logger.WriteLog(ex.Message);
}
}
}
Най-отгоре е дефиниран интерфейс, последван от два класа – DatabaseLogger и TextFileLogger, които имплементират интерфейса ILogger.
В Main класа се създава инстанция на TextFileLogger и се подава на конструктора на класа Processor. Това се нарича Dependency Injection. Класът Processor зависи от инстанция на ILogger и вместо да я създава всеки път, инстанцията се вмъква в конструктора. Това, също така, не обвързва специфичната инстанция на logger в класа Processor.
След това инстанцията на logger се използва в Try/Catch блок. Ако искате да промените типа на логъра, просто трябва да промените извикващата го програма, като инстанцирате DatabaseLogger и го подадете като параметър в конструктора.
Заключение
Заедно с много други концепции в разработването на софтуер, SOLID принципите също имат място във вашата кутия с инструменти. Тези принципи не трябва просто да бъдат научени, а трябва да се знае кога и как да се използват. Научавайки SOLID и използвайки SOLID правилно по време на разработването, гарантира че вашият софтуер ще бъде качествен, лесно поддържан и разширяем.
Източници:
- https://bg.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%B0%D0%BD%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%B8%D1%80%D0%B0%D0%BD%D0%B5)
- http://www.dotnetcurry.com/software-gardening/1365/solid-principles