Цикл статей о SOLID принципах
- S – Single Responsibility Principle
- D – Dependency Inversion Principle
- О принципах проектирования
--------------------------------------------------
Принцип единственной обязанности (Single-Responsibility Principle, SRP): У класса должна быть только одна причина для изменения.
Роберт Мартин. Принципы, паттерны и практики гибкой разработки
Существует ряд патологических случаев нарушения принципа единственной обязанности, которые будут очевидны сегодня практически любому. Классы бизнес-логики, которые знают о пользовательском интерфейсе или о базе данных; класс windows-сервиса c кучей бизнес-логики; статические утилитные классы, изменяющие глобальное состояние и т.п.
При этом нарушение SRP бывают как на уровне классов/модулей, так и на уровне подсистем и приложений. Классическим примером из моей практики являются два вопиющих нарушения SRP на уровне приложений: использование Windows Forms приложения в качестве WCF сервиса, и необходимость взаимодействия виндового сервиса с пользователем через Message Box-ы.
Приведенные выше случаи не столько нарушают SRP, сколько противоречат здравому смыслу. А как быть с более тонкими проблемами, когда один разработчик говорит, что дизайн хорош, а для другого он серьезно «попахивает» и вам хочется найти весомые аргументы против текущего решения?
Но прежде чем ответить на этот вопрос, нужно понять, а для чего вообще существует принцип SRP? В чем проблема, когда у класса есть более одной обязанности или причины для изменения? С какими проблемами он призван бороться?
Для чего нужен SRP?
SRP предназначен для борьбы со сложностью. На самом деле, любой принцип или паттерн проектирования предназначен для этого. Когда в нашем приложении всего 200 строк, то дизайн как таковой вообще не нужен. Достаточно аккуратно написать 5-7 методов и все будет хорошо. Проблемы возникают, когда система растет и увеличивается в масштабах. Поскольку сложность растет не линейно при увеличении размера приложения приходится подходить к дизайну более осмысленно, вспоминать всякие страшные слова, типа абстракции и сокрытия информации, лучше продумывать обязанности каждого класса, чтобы у разработчика сегодня и через год была возможность сосредоточиться на главной проблеме класса/модуля и проигнорировать второстепенные детали.
ПРИМЕЧАНИЕ
Подробнее о дизайне и критериях его качества читайте в заметках: Критерии плохого дизайна и О дизайне.
SRP и борьба со сложностью
Давайте рассмотрим такой пример. R# API предоставляет абстракцию под названием Context Actions (кстати, нечто подобное появится и в API Visual Studio 2015!). Это такая штука, которая появляется при нажатии Alt + Enter и с ее помощью можно делать много всего интересного: удалять ненужные выражения, преобразовывать LINQ-запросы и т.п.
Контекстные действия представлены базовым классом ContextActionBase, который содержит следующие ключевые члены:
public abstract class ContextActionBase
{
public abstract void Execute();
public abstract bool IsAvailable();
public abstract string Text { get; }
}
(На самом деле интерфейс R# API несколько отличается от этого, но суть именно такая.)
Когда я начинал разрабатывать плагин для упрощения контрактного программирования, то я начал с реализации контекстных действий по добавлению предусловий для проверки аргументов метода на null: с класса AddRequiresContextAction.
Поскольку изначально в голову приходят лишь небольшое число граничных случаев, то вначале класс AddRequiresContextAction содержал всю логику: логику по определению доступности добавления предусловия (например, что аргумент метода является ссылочного типа, он еще не проверен на null и т.п.), а также логику по добавлению предусловия для текущего класса.
Нарушал ли данный класс SRP? Сложно сказать. И да, и нет. С одной стороны, сам интерфейс базового класса говорит о необходимости выполнения нескольких действий, с другой стороны у класса на тот момент было всего три метода, каждый из которых отвечал за определенный аспект поведения, а их реализация была довольно простой. Можно сказать, что данный класс был «крепко связанный» (highly cohesive), а значит нарушать SRP не мог.
Но по мере того, как росло мое понимание предметной области добавлялось все больше и больше граничных условий. Со временем я понял, что если аргумент уже проверяется с помощью if-throw, то контекстное действие по добавлению Contract.Requires должно отсутствовать; что действие также должно быть недоступным, если значение аргумента по умолчанию равно null, что нужно учитывать еще и абстрактные классы и т.п. В результате мой класс AddRequiresContextAction начал изменяться и граф вызовов его внутренних методов стал выглядеть следующим образом:
Теперь даже по рисунку видно, что в классе две группы обязанностей, которые тесно взаимодействуют внутри, но никак не взаимодействуют между собой. Очевидно, что у данного класса появилось две ярко выраженные ответственности: проверка допустимости действия и его исполнение, которые не были столь очевидны до этого. В результате, я выделил два дополнительных класса: AddRequiesAvailability и AddRequiresExecutor, которые отвечают за «доступность» действия и за его «выполнение».
Более того, со временем даже эти два класса были разбиты на более мелкие составляющие, так что один класс в результате «вырос» в целый граф взаимодействующих объектов:
(Текущая версия класса AddRequiresContextAction выглядит так).
А в чем проблема в исходном решении? Зачем нужно было выделять два дополнительных класса? Чтобы классы соответствовали принципу, описанному дядюшкой Бобом?
Когда класс разрастается, он просто перестает помещаться в голове. Навигация затрудняется, на глаза попадаются ненужные детали, связанные с другим аспектом, в результате, количество понятий начинают превышать священные 7 ± 2 и мы начинаем терять контроль над кодом.
Я хочу иметь возможность сосредоточиться на сложных аспектах системы по отдельности, поэтому когда мне становится сложно это делать, я начинаю разбивать классы и выделять новые.
SRP в реальной жизни
Я честно признаюсь, что не понимаю принцип SRP в формулировке от «дядюшки» Боба и его метафору с «осью изменения» (мне понятна эта метафора в контексте Open-Closed Principle, но об этом мы поговорим в следующий раз.) Определение довольно запутанное, да и пояснение лишь сбивает с толку:
«В контексте принципа SRP мы будем называть обязанностью причину изменения. Если вы можете найти несколько причин для изменения класса, то у такого класса более одной обязанности.
…
Следует ли разделить эти обязанности? Все зависит от того, как именно изменяется приложение.
С другой стороны, если приложение не модифицируют таким образом, что эти обязанности изменяются порознь, то и разделять их нет необходимости. Более того, разделение в этом случае попахивало бы ненужной сложностью.
Отсюда вытекает следствие. Ось изменения становится таковой, только если изменение имеет место. Неразумно применять SRP – как и любой другой принцип, – если для того нет причин.»
На практике я довольно часто говорю о том, что класс нарушает SRP, но при этом я никогда не апеллирую к «осям изменений». Я говорю о тяжеловесности интерфейса и реализации, о решении классом не связанных друг с другом задач, о том, что здесь много кода и мало классов, о том, что в коде много лишнего шума, который нужно перенести в отдельные классы, чтобы было легче увидеть его основную суть, о том что высокоуровневая логика перемешана с низкоуровневыми деталями и т.п. Иногда я готовлю классы к будущим изменениям, но в плане сопровождаемости, а не в плане нарушения SRP.
Для меня SRP – это способ поиска скрытых абстракций, достаточно сложных, чтобы им отвели отдельную именованную сущность и спрятали в их недрах все детали. Разделение классов на составляющие диктуются не «осями изменений», а здравым смыслом и попыткой справиться с нарастающей сложностью системы.
Дякую, пишіть ще
ОтветитьУдалитьСпасибо! Буду продолжать:)
Удалить>>R# API предоставляет абстракцию под названием Context Actions (кстати, нечто подобное появится и в API Visual Studio 2015!). Это такая штука, которая появляется при нажатии Alt + Enter и с ее помощью можно делать много всего интересного: удалять ненужные выражения, преобразовывать LINQ-запросы и т.п.
ОтветитьУдалить--А не подскажете инфу, с какой вы получили эти сведения о Visual Studio 2015? " Сейчас доступна Visual Studio "14" CTP которая благодаря Roslyn умеет уже частично делать те действия которые вы описали. Странно выглядит что Microsoft презентует новый продукт с линейки Visual Studio, не выпустив до этого окончательный вариант Visual Studio "14".
Есть официальные заявления, что VIsual Studio "14" выйдет в 15-м году, а исходя из их маркировки она будет называться Visual Studio 2015. А исходя из того, что в 14-й студии эти действия есть уже в CTP, то можно сделать выводы, что они полноценно появятся именно в Visual Studio 2015.
УдалитьСпасибо. Очень круто
ОтветитьУдалить> использование Windows Forms приложения в качестве WCF сервиса
ОтветитьУдалитьЗабавно, но на такой пример я наткнулся в "Programming WCF Services by Juval Lowy". Причем обсуждается хостинг в потоке формы, гибридный класс FormHost и прочие малоприятные вещи.
Это годится для "коленочного решения", но никак не для учебной (относительно) литературы.
Александр, Juval приводит эти примеры, поскольку нужно покрыть эти темы. Хотя при этом и правда нужно предупреждать, что делать так в продакшн коде нельзя не в коем случае.
УдалитьА в чем причина? Скажем, если мне нужно контролировать WCF сервис и получать от него информацию по тому, какие функции и с каким параметрами вызывали (в моем случае, имеется сервис заявок, клиенты могут ставить заявки, нужно выводить список текущих, и иметь возможность отменить их вручную на стороне сервиса, и настраивать доступ к бирже). Я так понимаю, в таком случае надо чтобы сервис реализовывал два контракта: для выставления заявок и для управлением доступа к бирже (для клиента на сервере), но в тоже время получается нужен какой-то callback на выставление сделки чтобы клиент на сервере мог получать информацию о сделках. Звучит как-то монструозно.
УдалитьИ как тогда правильно хостить WCF службы? Путем создания windows services и логированием в файл или windows event viewer?
Сергей, спасибо за этот цикл статей. Можно попросить добавить к этой статье тег SOLID, как на следующих двух? Я бы себе закладочку сделал :) Спасибо
ОтветитьУдалитьОбновил ссылки во всех постах и всем добавил тег SOLID.
УдалитьОтлично! Спасибо
Удалить