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