Принципы SOLID C# представляют собой набор утверждений, которые описывают архитектуру программных продуктов. То есть, следуя им можно разработать стабильно работающее и масштабируемое приложение, которое будет удобно поддерживать.
Принцип единственной ответственности (S)
Согласно этому принципу класс разрабатывается с одной четко определенной целью. По сути своей, любой класс – это инструмент. Соответственно, все элементы класса должны быть направлены на решение одной задачи. Разрабатывая класс для всего и сразу, мы рискуем получить кучу проблем при дальнейшей его поддержке.
/// <summary>
/// Класс для чтения из базы данных.
/// </summary>
public class DataBaseReader
{
/// <summary>
/// Получить запись из БД.
/// </summary>
/// <param name="id"> Идентификатор записи.</param>
/// <returns> Запись БД.</returns>
public object GetRecord(Guid id)
{
return new object();
}
/// <summary>
/// Сформировать отчет.
/// </summary>
public void CreateReport()
{
}
}
Казалось бы всё корректно. Однако данный класс, изначально созданный для работы с базой данных, содержит метод для формирования отчета. Это нарушает принцип единственной ответственности. Корректным было бы создание двух отдельных классов: для чтения данных и для формирования отчетов.
/// <summary>
/// Класс для чтения из базы данных.
/// </summary>
public class DataBaseReader
{
/// <summary>
/// Получить запись из БД.
/// </summary>
/// <param name="id"> Идентификатор записи.</param>
/// <returns> Запись БД.</returns>
public object GetRecord(Guid id)
{
return new object();
}
}
/// <summary>
/// Класс для формирования отчетов.
/// </summary>
public class ReportsBuilder
{
/// <summary>
/// Сформировать отчет.
/// </summary>
public void CreateReport()
{
}
}
Принцип открытости/закрытости (O)
Данный принцип гласит о том, что разрабатываемые классы должны быть открыты для расширений, но закрыты для изменений. То есть, если класс был реализован и протестирован, мы не должны его изменять. Разумеется, изменения могут вноситься, при наличии веских для того причины. При этом, любой класс может бессчетное число раз расширяться при помощи механизма наследования.
/// <summary>
/// Класс для генерации отчетов.
/// </summary>
public class ReportBuilder
{
/// <summary>
/// Сгенерировать отчет.
/// </summary>
/// <param name="reportType"> Тип отчета.</param>
public void CreateReport(ReportType reportType)
{
switch(reportType)
{
case ReportType.excel:
// Генерация отчета Excel.
break;
case ReportType.pdf:
// Генерация отчета Pdf.
break;
case ReportType.xml:
// Генерация отчета Xml.
break;
}
}
}
При подобном подходе к разработке при необходимости добавить еще один вид отчета, мы будем вынуждены вносить изменения в реализованный класс. А внося изменения можем повредить работающий код. Однако, создав базовый класс для генерации отчетов, мы можем добавлять новые виды созданием классов реализаций, не внося никаких изменений в ранее разработанные блоки кода.
/// <summary>
/// Класс для генерации отчетов.
/// </summary>
public class BaseReportBuilder
{
/// <summary>
/// Сгенерировать отчет.
/// </summary>
public virtual void CreateReport()
{
// Базовая реализация генерации отчетов.
}
}
/// <summary>
/// Класс для генерации отчетов в excel.
/// </summary>
public class ExcelReportsBuilder : BaseReportBuilder
{
/// <summary>
/// Сгенерировать отчет.
/// </summary>
public override void CreateReport()
{
}
}
/// <summary>
/// Класс для генерации отчетов в pdf.
/// </summary>
public class PdfReportsBuilder : BaseReportBuilder
{
/// <summary>
/// Сгенерировать отчет.
/// </summary>
public override void CreateReport()
{
}
}
/// <summary>
/// Класс для генерации отчетов в xml.
/// </summary>
public class XmlReportsBuilder : BaseReportBuilder
{
/// <summary>
/// Сгенерировать отчет.
/// </summary>
public override void CreateReport()
{
}
}
Принцип подстановки Лисков (L)
Этот принцип говорит о том, что мы должны иметь возможность работать с любым производным от родительского классом так же, как с родительским. Иными словами, дочерние классы не должны нарушать определения родительского класса и его поведение.
/// <summary>
/// Класс "человек".
/// </summary>
public class Person
{
/// <summary>
/// Получить работу.
/// </summary>
public virtual void GetJob()
{
}
/// <summary>
/// Родиться.
/// </summary>
public virtual void SeeTheLight()
{
}
}
/// <summary>
/// Ребенок.
/// </summary>
public class Child : Person
{
/// <summary>
/// Получить работу.
/// </summary>
public override void GetJob()
{
throw new NotImplementedException("Ребенок не может работать.");
}
}
/// <summary>
/// Взрослый.
/// </summary>
public class Adult : Person
{
/// <summary>
/// Получить работу.
/// </summary>
public override void GetJob()
{
base.GetJob();
}
}
Кажется, в данном фрагменте кода всё логично, однако он нарушает принцип подстановки Лисков. В классе Child вызов метода GetJob приводит к стопроцентному возникновению исключения. Конкретно для данного примера самым логичным будет вынесение метода GetJob в класс Adult.
Принцип разделения интерфейсов (I)
Согласно этому принципу клиенты не должны принудительно внедрять интерфейсы, которые ими не используются.
/// <summary>
/// Персонаж.
/// </summary>
public interface ICharacter
{
/// <summary>
/// Защищаться.
/// </summary>
/// <returns> Количество отраженного урона. </returns>
int Defend();
/// <summary>
/// Атаковать в ближнем бою.
/// </summary>
/// <returns> Количество наносимого урона. </returns>
int MeleeAtack();
/// <summary>
/// Выстрелить.
/// </summary>
/// <returns> Количество наносимого урона. </returns>
int Shoot();
/// <summary>
/// Атаковать заклинанием.
/// </summary>
/// <returns> Количество наносимого урона. </returns>
int CastSpell();
}
/// <summary>
/// Маг.
/// </summary>
public class Wizard : ICharacter
{
// Реализация логики поведения мага.
}
/// <summary>
/// Воин.
/// </summary>
public class Swordsman : ICharacter
{
// Реализация логики поведения воина.
}
/// <summary>
/// Лучник.
/// </summary>
public class Arch : ICharacter
{
// Реализация логики поведения лучника.
}
В данном фрагменте кода маг получает доступ к физическим атакам, коими он не пользуется, а воин и лучник – к магии, которой они так же не владеют. То есть, классы вынуждены реализовывать то, чем пользоваться не будут. А потому, выделим дополнительные интерфейсы, чтобы разложить всё по полочкам.
/// <summary>
/// Персонаж.
/// </summary>
public interface ICharacter
{
/// <summary>
/// Защищаться.
/// </summary>
/// <returns> Количество отраженного урона. </returns>
int Defend();
}
/// <summary>
/// Интерфейс для физического воздействия.
/// </summary>
public interface IPhysicalImpact
{
/// <summary>
/// Атаковать в ближнем бою.
/// </summary>
/// <returns> Количество наносимого урона.</returns>
int MeleeAtack();
/// <summary>
/// Выстрелить.
/// </summary>
/// <returns> Количество наносимого урона.</returns>
int Shoot();
}
/// <summary>
/// Интерфейс для магического воздействия.
/// </summary>
public interface IMagicalImpact
{
/// <summary>
/// Атаковать заклинанием.
/// </summary>
/// <returns> Количество наносимого урона.</returns>
int CastSpell();
}
/// <summary>
/// Маг.
/// </summary>
public class Wizard : ICharacter, IMagicalImpact
{
// Реализация логики поведения мага.
}
/// <summary>
/// Воин.
/// </summary>
public class Swordsman : ICharacter, IPhysicalImpact
{
// Реализация логики поведения воина.
}
/// <summary>
/// Лучник.
/// </summary>
public class Arch : ICharacter, IPhysicalImpact
{
// Реализация логики поведения лучника.
}
Разложив методы по интерфейсам таким образом мы не вынуждаем разработчика реализовывать в классах методы, которые не будут использоваться.
Принцип инверсии зависимостей (D)
Согласно принципу инверсии зависимостей, классы высокого уровня не должны зависеть от низкоуровневых, а абстракции не должны зависеть от деталей. Как правило, высокоуровневые классы отвечают за бизнес-правила / логику программного продукта. Низкоуровневые классы реализуют более мелкие операции: взаимодействие с данными, передача сообщений в систему и т.п.
Внедрение зависимостей может быть выполнено несколькими путями. Рассмотрим их на примере подсистемы уведомлений. Для этого напишем интерфейс для рассылки сообщений и пару классов – реализаций.
/// <summary>
/// Интерфейс для рассылки сообщений.
/// </summary>
public interface IMessenger
{
/// <summary>
/// Отправить сообщение.
/// </summary>
void Send();
}
/// <summary>
/// Класс для рассылки email-сообщений.
/// </summary>
public class Email : IMessenger
{
/// <summary>
/// Отправить сообщение.
/// </summary>
public void Send()
{
}
}
/// <summary>
/// Класс для рассылки СМС-сообщений.
/// </summary>
public class SMS : IMessenger
{
/// <summary>
/// Отправить сообщение.
/// </summary>
public void Send()
{
}
}
А теперь рассмотрим через что же можно внедрить зависимость.
Конструктор.
/// <summary>
/// Уведомление.
/// </summary>
public class Reminding
{
/// <summary>
/// Интерфейс для расслыки сообщений.
/// </summary>
private IMessenger _messenger;
/// <summary>
/// Конструктор уведомления.
/// </summary>
/// <param name="messenger"> Интерфейс для расслыки уведомлений. </param>
public Reminding(IMessenger messenger)
{
_messenger = messenger;
}
/// <summary>
/// Отправить уведомление.
/// </summary>
public void Notify()
{
_messenger.Send();
}
}
Свойства.
/// <summary>
/// Уведомление.
/// </summary>
public class Reminding
{
/// <summary>
/// Интерфейс для расслыки сообщений.
/// </summary>
private IMessenger _messenger;
public IMessenger Messanger
{
set
{
_messenger = value;
}
}
/// <summary>
/// Конструктор уведомления.
/// </summary>
/// <param name="messenger"></param>
public Reminding(IMessenger messenger)
{
_messenger = messenger;
}
/// <summary>
/// Отправить уведомление.
/// </summary>
public void Notify()
{
_messenger.Send();
}
}
Метод.
/// <summary>
/// Уведомление.
/// </summary>
public class Reminding
{
/// <summary>
/// Отправить уведомление.
/// </summary>
/// <param name="messenger"> Интерфейс для отправки сообщений. </param>
public void Notify(IMessenger messenger)
{
messenger.Send();
}
}
Как вы могли заметить, благодаря внедрению зависимостей, класс Reminding не зависит от того, как будет отправляться уведомление. О том, что отправка пойдет как СМС или письмо на электронную почту, система узнает непосредственно при отправке. Благодаря этому, мы в состоянии безболезненно добавлять новые варианты отправки и переключаться между существующими.
Принципы SOLID C# — Итоги
Мы рассмотрели принципы SOLID C# и постарались разобраться, для чего же они нужны. В целом, следуя им при разработке приложения, мы облегчаем себе жизнь в будущем, создавая относительно просто поддерживаемый и масштабируемый программный продукт.
Также рекомендую прочитать статью SOLID в объектно-ориентированном программировании, если нужны дополнительные материалы по этой теме. А также подписывайтесь на группу ВКонтакте, Telegram и YouTube-канал. Там еще больше полезного и интересного для программистов.