Категории: C# | C Sharp

Принципы SOLID C#

Принципы SOLID C# представляют собой набор утверждений, которые описывают архитектуру программных продуктов. То есть, следуя им можно разработать стабильно работающее и масштабируемое приложение, которое будет удобно поддерживать.

Подпишись на группу Вконтакте и Телеграм-канал. Там еще больше полезного контента для программистов.

А на YouTube-канале ты найдешь обучающие видео по программированию. Подписывайся!

Принцип единственной ответственности (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-канал. Там еще больше полезного и интересного для программистов.

Константин Туйков

Disqus Comments Loading...
Поделиться
Опубликовано
Константин Туйков

Свежие публикации

Event C# | События и делегаты C#

События C# в программировании чем-то похожи на события в повседневной жизни. Рождение, первый шаг, первый день в школе и тому…

3 дня тому назад

Топ 7 языков программирования 2020

За последнее десятилетие множество языков программирования вышло в свет. Однако не все они одинаково хорошо прижились в мире разработки ПО.…

4 дня тому назад

C# Array | Работа с массивами в C#

C# Array предоставляет удобные инструменты по работе с массивами. Уточним, что массив – это набор элементов, расположенных друг за другом.…

6 дней тому назад

Эксперименты с входными данными в глубоком обучении

Добрый день, уважаемые читатели. Порой ко мне в голову приходят ненормальные мысли по поводу обучения нейросетей. И я решил -…

7 дней тому назад

List C# | Работа со списками в C#

List C# - списки представляют собой удивительно гибкий инструмент по работе с коллекциями. Одной из главных особенностей списков является возможность…

2 недели тому назад

Что нового в C# 8.0

Microsoft активно развивает язык программирования C Sharp. Работа над последней, восьмой версией началась сразу же после релиза седьмой. Стадия разработки…

3 недели тому назад