UniLecs. SOLID — принципы объектно-ориентированного программирования
UniLecsОпределение
SOLID - это аббревиатура 5-ти основных принципов проектирования в объектно-ориентированном программировании:
- Single responsibility (SRP, принципы единственной ответственности),
- Open-closed (OCP, принцип открытости/закрытости),
- Liskov substitution (LSP, Принцип подстановки Барбары Лисков),
- Interface segregation (ISP, принцип разделения интерфейсов),
- Dependency inversion (DIP, Принцип инверсии зависимостей)
Аббревиатура SOLID была предложена Робертом Мартином, автором нескольких книг, широко известных в сообществе разработчиков. Использование в совокупности данных принципов позволяет повысить вероятность того, что программист создаст систему, которую будет легко поддерживать и расширять в течение долгого времени.
Single Responsibility Principle (SRP, Принцип единственной ответственности)
Этот принцип обозначает, что каждый объект должен иметь одну обязанность, и эта обязанность должна быть полностью инкапсулирована в класс. В терминах ООП можно также сказать, что ваш класс не должен быть похож на "швейцарский нож".
Давайте разберем на примере:
Пример:
namespace SOLID { public class Developer { public int ID { get;set; } public string Name { get;set; } public void Add(Developer dev) { // TODO: добавляем разработчика в базу данных } public void GenerateWorkingHoursReport() { // TODO: генерируем рабочие часы разработчика за посл.месяц } } }
В данном примере класс Developer не соот-т принципу SRP, т.к. класс несет две ответственности:
- Добавление разработчика в базу данных
- Генерация отчета по рабочим часам за посл. месяц
В данном случае класс Developer не должен нести ответственность за генерацию отчета по рабочим часам, т.к. эта функция в будущем может измениться, например, нужно будет генерировать рабочие часы по неделям или дням, либо генерировать в другом формате. В этом случае придется редактировать класс Developer.
Поэтому, согласно принципу SRP, вы должны создать второй класс, например, DeveloperReport, для генерации подобных отчетов.
namespace SOLID { public class Developer { public int ID { get;set; } public string Name { get;set; } public void Add(Developer dev) { // TODO: добавляем разработчика в базу данных } } public class DeveloperReport { public void GenerateWorkingHoursReport() { // TODO: генерируем рабочие часы разработчика за посл. месяц } } }
Open-closed (OCP, принцип открытости/закрытости)
Этот принцип означает, что программные объекты (классы, модули, функции и т.п.) должны быть открыты для расширения, но закрыты для модификации/изменения. Т.е. эти объекты могут менять свое поведение без изменения их исходного кода. В терминах ООП можно также сказать, что принцип закрытости означает, что уже разработанный класс, прошедший модульное тестирование не должен подвергаться изменениям. Если же нужно расширить функциональность вашего класса, то в ООП языках используется наследование классов, а также методы расширения (например в C#).
Пример:
namespace SOLID { public class DeveloperReport { public string TimeReportType { get; set; } public void GenerateWorkingHoursReport() { switch (TimeReportType) { case "CSV": // TODO: генерируем отчет в формате CSV break; case "WORD": // TODO: генерируем отчет в формате WORD break; } } } }
Согласно принципу OCP, этот класс составлен неверно, т.к. в случае необходимости добавления нового функционала, например, генерации отчета в формате Excel, нужно будет модифицировать класс. А принцип OCP гласит, что класс закрыт от модификаций, но открыт для расширения. Давайте посмотрим, как следует это делать.
namespace SOLID { public class DeveloperReport { public virtual void GenerateWorkingHoursReport() { // Базовая реализация } } public class DeveloperCSVReport: DeveloperReport { public override void GenerateWorkingHoursReport() { // Генерация отчета в формате CSV } } public class DeveloperPDFReport: DeveloperReport { public override void GenerateWorkingHoursReport() { // Генерация отчета в формате PDF } } }
В данном случае функционал легко расширяется путем создания нового класса и наследования его от базового.
Liskov Substitution Principle (LSP, Принцип подстановки Лисков)
В формулировке Роберта Мартина: «функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа не зная об этом». Также можно сказать, что поведение наследуемых классов не должно противоречить поведению, заданному базовым классом, то есть поведение наследуемых классов должно быть ожидаемым для кода, использующего переменную базового типа.
Пример:
namespace SOLID { public abstract class Developer { public int Id { get;set; } public string Name { get;set; } public virtual string[] GetProgramerCertifications(int Id) { return new string[] { "Base Cert" }; } public virtual string GetWorkAlias(int Id) { return "Base Dev alias"; } } public class JuniorDeveloper : Developer { public override string[] GetProgramerCertifications(int Id) { throw new NotImplementedException(); } public override string GetWorkAlias(int Id) { return "Junior Dev alias"; } } public class SeniorDeveloper : Developer { public override string[] GetProgramerCertifications(int Id) { return new string[] { "Senior Java Cert", "Senior C# Cert" }; } public override string GetWorkAlias(int Id) { return "Senior Dev alias"; } } }
Вроде выглядит вполне прилично. Но ...
List<Developer> list = new List<Developer>() { new JuniorDeveloper(), new SeniorDeveloper() }; string[] allDevCerts = new string[] {}; foreach (var dev in list) { allDevCerts.Add(dev.GetProgramerCertifications(dev.Id)) }
Тут у нас проблема, т.к. у класса JuniorDeveloper не определен метод, возвращающий сертификаты разработчика. И в данном примере мы получим необработанное исключение. Как раз потому, что данный пример не соот-т принципу подстановки Лисков. Давайте приведем все в порядок.
public interface IWork { string GetWorkAlias(int Id); } public interface IDeveloperCert { string[] GetProgramerCertifications(int Id); } public class JuniorDeveloper : IWork { public string GetWorkAlias(int Id) { return "Junior Dev alias"; } } public class SeniorDeveloper : IWork, IDeveloperCert { public string[] GetProgramerCertifications(int Id) { return new string[] { "Senior Java Cert", "Senior C# Cert" }; } public string GetWorkAlias(int Id) { return "Senior Dev alias"; } }
Решение очень простое - мы просто разбили функционал на два интерфейса IWork и IDeveloperCert. Теперь класс JuniorDeveloper требует реализации только IWork. И в таком случае мы соблюдаем принцип LSP.
Interface Segregation Principle (ISP, принцип разделения интерфейсов)
В формулировке Роберта Мартина: «клиенты не должны зависеть от методов, которые они не используют». Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться клиенты, которые этот метод не используют.
Пример:
public interface IDeveloper { void AddDeveloper(); }
Предположим, что все объекты класса Developer наследуют этот интерфейс для сохранения данных в базу данных. Но появилось новое бизнес требование, выдавать данные по senior-разработчикам.
public interface IDeveloper { void AddDeveloper(); void DisplaySeniorDeveloper(); }
И тут явно неверная логика. Класс JuniorDeveloper наследует этот интерфейс, но не использует метод DisplaySeniorDeveloper(). Поэтому правильным решением будет разделение логики на 2 интерфейса и разделение ответственности.
public interface IDeveloper { void AddDeveloper(); } public interface ISeniorDeveloper { void DisplaySeniorDeveloper(); }
Таким образом, обеспечивается разделение интерфейсов.
Dependency Inversion Principle (DIP, Принцип инверсии зависимостей)
Означает, что модули верхних уровней не должны зависеть от модулей нижних уровней, а оба типа модулей должны зависеть от абстракций; сами абстракции не должны зависеть от деталей, а вот детали должны зависеть от абстракций.
Пример:
public class ExcelReport { public void Generate() { // генерация excel-отчета } } public class DeveloperReport { private ExcelReport excelReport; public DeveloperReport() { excelReport = new ExcelReport(); } public void GenerateExcelReport() { excelReport.Generate(); } }
Как мы видим, класс DeveloperReport имеет сильную зависимость от класса ExcelReport, потому что он генерирует только отчеты типа excel. Если нам нужно будет добавить генерацию отчетов другого типа, то нужно переделать эту систему, так как данный пример показывает сильную зависимость и не соот-т принципу инверсии зависимостей.
public interface IReport { void Generate(); } public class ExcelReport : IReport { public void Generate() { // генерация excel отчета } } public class WordReport : IReport { public void Generate() { // генерация word отчета } } public class DeveloperReport { private IReport report; public DeveloperReport() { report = new ExcelReport(); } public void GenerateReport() { report.Generate(); } }
Мы все еще имеем зависимость класса DeveloperReport от класса ExcelReport, но она уже не такая сильная. Полное решение проблемы устраняется принципом внедрения зависимостей (DI), ктр реализуется, например, с помощью различных библиотек.
Ссылки: