Почти каждый технический специалист, с которым я сталкивался, кто работал с объектно-ориентированным кодом, кажется, гордится тем, что он пишет SOLID код. Не твердый в том смысле, что код стабильный, а скорее SOLID, как в применении пяти основных принципов . На днях я спросил человека, как часть кода соответствует принципу замещения Лискова (L в SOLID), и он не смог объяснить это. И они только что хвастались тем, что все время писали SOLID-код. И я просто подумал, может, просто, может быть, я мог бы что-нибудь сделать, чтобы прояснить кое-что из этого. Итак, приступим!

О SOLID, кажется, стоит писать. Можно найти несколько статей о SOLID, и сама Википедия - хороший источник, чтобы узнать больше о SOLID. Однако я подумал, что мне стоит попробовать. А поскольку большую часть своей профессиональной карьеры я писал на C #, это должно быть весело.

Что такое ТВЕРДЫЙ?

Набор из пяти основных принципов объектно-ориентированного программирования и дизайна

SOLID - это мнемоническая аббревиатура, обозначающая следующие пять основных принципов:

  1. Принцип единой ответственности
  2. Принцип открытости / закрытости
  3. Принцип замены Лискова
  4. Принцип разделения интерфейса
  5. Принцип инверсии зависимостей

Предполагается, что эти принципы при совместном применении помогут нам создать систему, которую легко поддерживать и расширять с течением времени. Следует отметить, что это всего лишь рекомендации, которым можно следовать независимо от языка или структуры по нашему выбору.

Мы рассмотрим пять принципов, а сегодня давайте рассмотрим первый, который называется Принцип единой ответственности.

Что такое принцип единой ответственности?

У модуля должна быть только одна причина для изменения.

Принцип единой ответственности (SRP ) гласит, что модуль должен нести ответственность за отдельную часть функциональных возможностей, предоставляемых программным обеспечением, и эта ответственность должна быть полностью им инкапсулирована. Все его услуги должны строго соответствовать этой ответственности.

Проще говоря, мы можем сказать, что модуль должен иметь очень узкую часть ответственности во всей системе. Или, как говорят некоторые, у модуля должно быть не более одной причины для изменения. Хм? Узкая ответственность, причина для перемен. Все это довольно расплывчато. Как мы узнаем, есть ли достаточная причина для изменения? Или если ответственность достаточно узкая? А что за модуль? Кроме того, есть вопросы посерьезнее. Почему? Как это на самом деле мне помогает?

Давайте разберемся с этими вещами.

Почему единственная ответственность?

Это просто следствие понимания того, как создавать поддерживаемое программное обеспечение.

Если у модуля есть только одна ответственность, он может быть очень надежным. Легче проверить, работает ли он должным образом, и легче рассуждать об изменениях в модуле, если он несет единственную ответственность.

Приведу здесь аналогию с конвейером. Согласно Википедии, сборочная линия - это производственный процесс, в котором детали добавляются по мере того, как сборочный полуфабрикат перемещается с рабочей станции на рабочую станцию, где детали добавляются последовательно до тех пор, пока не будет произведена окончательная сборка.

Подумайте об этом: большой процесс, разбитый на маленькие шаги. Происходит небольшой шаг, затем результат переносится на другой маленький шаг. Происходит маленький шаг, затем результат переносится на еще один маленький шаг. Это сборочная линия.

Это привело к значительному сокращению затрат и более высокой специализации. Вместо того, чтобы рабочий должен знать все шаги, он мог сделать один шаг, и только шаг. Им было легче овладеть одним маленьким шагом, и они могли делать это постепенно лучше.

Конечно, были и недостатки. Если делать одно и то же снова и снова, одни и те же суставы подвергаются нагрузке. И рабочие никогда не чувствовали вдохновения, их работа была повседневной, однообразной и скучной.

Применим это к нашей программной системе. Давайте сделаем каждого работника сборочной линии модулем, а каждую сборочную линию - системой программного обеспечения. Программная система - это большая сущность, которую можно разбить на множество маленьких шагов или, обязанностей. Когда обязанности невелики, модуль можно постепенно улучшать, т. Е. модуль можно сделать более надежным. Легче тестировать, проверять, улучшать и оптимизировать. Вот почему.

Но недостатки? Конечно, они тоже приходят. Модуль никогда не вызовет вдохновения, и те же суставы почувствуют напряжение. Но модуль не для живого человека, чтобы почувствовать стресс или недостаток вдохновения! Мы хотим, чтобы наш модуль выполнял свою работу, а не становился Леонардо да Винчи! (если, конечно, его задача не стать да Винчи)

Итак, поэтому принцип единой ответственности (SRP).

Что такое модуль в SRP?

Сначала мы должны понять, о чем говорит SRP, только потом мы сможем его применить.

Верно-верно. Итак, теперь мы знаем, почему мы должны использовать SRP, а также как он нам помогает. Но здесь говорится о модулях, а что это такое? Ответ прост. Давайте воспользуемся помощью словаря:

модуль (сущ.) - каждый из набора стандартизованных частей или независимых единиц, которые можно использовать для создания более сложной конструкции, например, предмета мебели или здания.

Проще говоря, модуль - это стандартизированная независимая единица, которую можно использовать для создания чего-либо сложного. Любая единица кода, которая повторно используется, компоновка и подчиняется контракту в программном обеспечении, может рассматриваться как модуль. Поскольку SOLID обычно применяется в объектно-ориентированном программировании, а нас в первую очередь интересует C #, такой единицей кода, скорее всего, будет класс. В функциональном программировании я почти уверен, что модуль будет функцией. Но мы также можем сделать так, чтобы модуль на C # соответствовал функции - в конце концов, все, что нам нужно, это многократно используемая компоновочная единица кода, которая подчиняется контракту, а функция может быть многократно использована, компонована, и ее подпись действительно контракт.

В чем причина меняться?

Или что такое узкая ответственность?

Теперь, когда у нас есть определение модуля, давайте посмотрим, к чему может относиться причина изменения. Или насколько узкая ответственность считается узкой ответственностью. Интерпретации довольно субъективны, и, как и в большинстве случаев в программном обеспечении, обычно ответ зависит от обстоятельств.

Но так проще думать об этом. Если описание того, что делает модуль, можно разбить на части, так и должно быть. Это довольно абстрактно, так что позвольте мне попробовать. Что ж, на самом деле это становится сложно объяснить без кода, поэтому давайте перейдем к коду.

Как провести рефакторинг, чтобы подчиняться SRP?

Давайте рассмотрим код, потому что слов уже недостаточно.

Позвольте мне взять довольно распространенный код:

Это какой-то код в каком-то студенческом репозитории. Остальной код нам неизвестен. Рассматривая метод Create как модуль, попробуем описать словами, что делает метод Create:

int StudentRepository.Create (Студент студент) проверяет, не указано ли имя студента, и выдает ошибку, если да. В противном случае он сохраняет сущность студента в базе данных и возвращает первичный ключ сохраненной сущности.

Обратите внимание, как описание можно разбить на части? Попробуем выделить части:

  1. проверяет, не указано ли имя ученика, и выдает ошибку, если да
  2. сохраняет сущность студента в базе данных и возвращает первичный ключ сохраненной сущности

Попробуем его реорганизовать. Очевидно, что нам нужны 2 метода, давайте разберемся с ними. Назвать их должно быть довольно просто:

Теперь у нас есть два метода, каждый со своей ответственностью. Метод ThrowIfStudentNameIsBlank делает именно это, а метод Create делает именно это.

Если вам интересно, что метод Create добавляет, затем сохраняет, а затем возвращает идентификатор, это не множественные обязанности, функция может выполнять действие и возвращать значение. Фактически, здесь метод Create сохраняет в базу данных и возвращает первичный ключ. Это вполне обоснованная единственная ответственность. Нет возможности снизить его ответственность дальше, потому что мы просто не можем его разложить.

Ради интереса, давайте попробуем сделать именно это, чтобы понять, почему и когда этого делать не следует, по крайней мере. Итак, что теперь делает метод Create?

int StudentRepository.Create (Студент студент) добавляет объект "студент" в таблицу, сохраняет изменения в базе данных и возвращает первичный ключ сохраненного объекта.

Обратите внимание, как мы расширили возможности метода create? Чтобы мы могли быстро разбить его на части:

Теперь, когда у нас есть отдельные методы с единственной ответственностью, что делает Create?

int StudentRepository.Create (Student student) запускает серию методов, которые, в свою очередь, добавляют сущность Student в таблицу, сохраняют изменения в базе данных и возвращают первичный ключ сохраненной сущности.

Теперь единственный шаг в методе Create заключается в том, что он запускает серию методов для выполнения своей работы - сохранения объекта ученика в таблицу базы данных. Давайте рассмотрим методы, чтобы понять, насколько они полезны:

Первый метод AddStudent(Student student) добавляет студента в таблицу базы данных, но не сохраняет изменения. Есть ли у нас вариант использования для автономного вызова этого метода? Есть ли у нас какая-либо бизнес-логика, которая делает это повторно используемым и независимым? Если так, то это хороший рефакторинг. Но здесь все не так. Он не многоразового. Это делает его плохим кандидатом для модуля, только тогда мы можем применить к нему SRP.

То же самое и со вторым методом SaveChangesToDb(). Конечно, метод сохранения изменений можно использовать повторно, но является ли он независимым? Нет, это сильно зависит от действий, которые ему предшествуют. Это делает его плохим кандидатом для модуля, только тогда мы можем применить к нему SRP.

Последний метод возвращает первичный ключ учащегося, если учащийся предоставлен. Но student.Id - это свойство, что означает, что оно само состоит из получателя и установщика (методов). Таким образом, GetStudentId(Student student) является и повторно используемым, и независимым, но он также избыточным - уже существует метод, который делает то же самое. Так что вообще нет смысла иметь этот метод. Уберем ненужное дополнение.

Теперь, когда мы установили, что эти три метода не являются модулями (в нашем конкретном случае использования) и что мы не можем применить к ним SRP, давайте посмотрим на него под другим углом. Реорганизованный метод Create, который вызвал эти 3 метода, в основном делегировал действия другим методам. Если мы еще раз посмотрим на код, разве это не было сделано в предыдущем коде? Хорошо, вы заметили.

Итак, вывод: перед применением SRP нам нужно убедиться, что мы имеем дело с модулем. И единственный способ убедиться, что мы имеем дело с модулем, - это понять, что составляет модуль в соответствии с нашими требованиями. Здесь требуется сохранять информацию о студентах, и, следовательно, метод Create является модулем. Он несет исключительную ответственность за сохранение информации о новых учащихся в базе данных.

Давайте еще раз вернемся к нашему последнему действующему коду:

ThrowIfStudentNameIsBlank было довольно длинным именем, и мы просто значительно уменьшили его длину для краткости в этом посте. Честно говоря, я предпочитаю более информативное имя, мне не нужно переходить к определению, чтобы понять, что оно делает.

Но сейчас мы потеряли довольно важную часть функциональности. Проверка перед созданием записи студента в базе данных. Теперь у нас есть 2 функции с отдельными обязанностями, без сомнения - одна проверяет ученика, а другая сохраняет ученика в базе данных - но как заставить их работать вместе? Что ж, давайте создадим винт, оркестратор или как хотите это называть.

Мы представили учащимся новый метод под названием ValidateAndCreate, который делает именно это. Он просто делегирует свои обязанности меньшим модулям, каждый из которых имеет единственную ответственность. В некотором смысле его единственная обязанность - последовательно запустить эти два метода и вернуть результат. Это следует за SRP.

Мы также сделали метод Validate статическим, поскольку он не зависит от каких-либо частных переменных. Он также был сделан закрытым, потому что по требованию его не нужно было раскрывать отдельно. Имейте в виду, что это все еще модуль, хотя и частный. Сам Validate также следует за SRP.

Мы сделали метод Create закрытым, и он по-прежнему является модулем, хотя он не отображается (потому что наш вариант использования требует, чтобы проверка и создание выполнялись последовательно, и это единственный способ создать - мы не можем предоставить Create как отдельную функциональность без Validate). Метод Create по-прежнему является модулем, который следует за SRP.

Что ж, неплохой рефакторинг. Но, как мы видели, SRP не ограничивается одним уровнем, ValidateAndCreate - это модуль, который сам имеет подмодули, которые следуют за SRP. Имеет смысл только то, что его супер-модули также следуют SRP. Мы рассмотрим эту область в части 2 раздела Расширение принципа единой ответственности в разделе Пишете ли вы код SOLID C #? серии.

Пожалуйста, оставляйте свои предложения, комментарии и отзывы в разделе комментариев на странице. Кроме того, если вам понравилась статья, щелкните значок сердечка, чтобы порекомендовать статью на носителе, а также поделитесь ею любым способом или в любой форме.

Ваше здоровье!

PS: эта статья опаздывает на день, но я не нарушил свое новогоднее решение;)