Single Responsibility Principe достаточно прост для понимания и его не сложно придерживаться.
Но в работе я достаточно часто сталкиваюсь нарушением этого принципа. В этой статье я собрал самые больные из способов нарушить SPR из тех, что я встречал.
Первый способ: Singleton
Singleton — подразумевает, что помимо своих основных обязанностей класс занимается еще и контролированием количества своих экземпляров, чем нарушает Single Responsibility Principle.
Очень просто создать Singleton. Достаточно скопировать из википедии пару строк кода в вашу любимую IDE. Singleton есть везде, не нужно заморачиваться с передачей объекта в нужное место и управлять памятью тоже не обязательно, экземпляр один, к тому же он вечный. Эта легкость возможно и является причиной применения Singleton-а не по назначению.
Singleton повышает связность кода
Зависимость от Singleton-а не видна в публичном интерфейсе. Достать и использовать экземпляр можно в любом месте, независимо ни от чего.
Сложные зависимости, а тем более скрытые (те, что не видно в публичном интерфейсе) приводят к появлению неожиданных эффектов в частях приложения, на первый взгляд не связанных с теми частями, где производится изменение кода. Это приводит к неожиданным багам. К тому же такие баги могут быть не найдены сразу после внесения изменений, из-за не очевидной области функциональности, на которое повлияло изменение.
Экземпляр Singleton-а невозможно подменить без танца с бубном
Singleton делает невозможным полиморфизм в отношении себя. А значить невозможны автотесты. Изменение поведения Singleton во время исполнение затрудняется. При необходимости перевести компонент в особый режим, требующий другого поведения Singleton-а вызывает боль и страдания.
Singleton может хранить свое состояние
Это приводит к изменению поведения при изменении порядка вызовов методов класса. Так как Singleton есть везде, то контролировать порядок вызовов практически невозможно. Это может приводить к разнообразным артефактам. Эта проблема усугубляет негативные эффекты увеличения связности кода и порождает еще более изощренные баги.
Есть более безопасные порождающие паттерны
Повышение связности кода и невозможность подмены его экземпляра успешно решает IoC. Реализация этого принципа например с помощью Dependency Injection забирает обязанность контролирования количества экземпляров класса и делают зависимости более явными.
Несмотря на все описанные выше ужасы есть места где уместно использовать Singleton
Singleton уместен в тех случаях, когда не может логически существовать более одного экземпляра объекта. Например NSApplication.
В готовых библиотеках использовать Singleton оправдано в ситуациях, когда незачем или дорого что-то кастомить или подменять.
Второй способ: смешение архитектурных слоев
Есть огромное количество способов смешать архитектурные слои. Каждый из способов раскладывает свои грабли.
Бизнес-логика в модели.
Помещая бизнес-логику в объект модели мы добавляем в него, помимо основной ответственности — хранения данных, дополнительные ответственности связанные с обработкой этих данных. Как правило при таком подходе в объекте модели скапливается большая часть операций, которые можно выполнить с этим объектом. Что является многократным нарушением SRP.
Наличие бизнес-логики в объектах модели приводит к невозможности переиспользовать часть бизнес-логики для другого объекта модели, затрудняет расширение модели, предрасполагает к реализации полиморфизма через copy-paste. При создании похожего объекта копируется с небольшими изменениям часть кода с бизнес логикой. Такой подход делает практически невозможным безопасное изменение той части логики, которая находится в модели.
Обнаружить такое нарушение достаточно просто, по наличию любых методов в объекте модели.
Часто встречаются методы по сохранению объекта модели в память устройства или методы для обновления данных из удаленного ресурса.
Неправильное использование паттернов MVC, MVP, MVVM
Все эти паттерны достаточно схожи, это видно даже по аббревиатурам. У каждого есть View и Model. Различаются они способом взаимодействия View с Model-ю через промежуточный слой.
Но у каждого из этих паттернов один общий недостаток — разрастание Controller-а, Presenter-а или View-Model-и. Суть проблемы кроется в неправильном понимании компонента View и компонента хранящего бизнес-логику (Controller, Presenter или View-Model).
View должна содержать логику, для отображения пользовательского интерфейса, Controller, Presenter или View-Model должны содержать бизнес-логику. Отдельных слов заслуживает MVC iOS SDK навязывает использование MVC. Но UIViewController не является MVC-шным Controller-ом, так как в большинстве случаев содержит логику для отображения пользовательского интерфейса.
Этот факт делает практически невозможным реализовать правильный MVC. В лучшем случаев UIViewController становится частью View, бизнес логика выноситься в другие слои.
С другой стороны, после отделения логики, необходимой для отображения интерфейса, в Controller-е, Presenter-е или View-Model-и все-равно может остаться слишком много кода. Это очень часто связано с тем, что для одного экрана создается один объект содержащий бизнес-логику, при этом он может реализовывать несколько пользовательских сценариев, что опять нарушает SRP.
Каждый объект содержащий бизнес логику должен реализовывать не больше одного пользовательского сценария.
Как правило множество пользовательских сценариев могут состоять из одних и тех же операций, сохранение и получение данных из локального хранилища, запросы к удаленному серверу итд.
В этом случае, всю логику, которая не относится к пользовательскому сценарию необходимо вынести в инфраструктурный слой.
Решением проблемы “разрастающегося контроллера” будет грамотное использование архитектурных паттернов. Если придерживаться SRP при написании кода и выносить из Controller-а, Presenter-а или View-Model-и весь код не относящийся к его основной ответственности, логика станет прозрачнее, пользовательские сценарии понятнее и их будет проще читать.
Третий способ: NSNotificationCenter
Нотификации в iOS — отличный способ связать все со всем!
NSNotificationCenter является частным, но достаточно ярким представителем Singleton-а. Не смотря на то, что нотификации — паттерн взаимодействия (Communication Pattern), а Singleton — порождающий паттерн (Creational Pattern), нотификации сохраняет все недостатки Singleton-а.
Начиная обсервить нотификацию не связанную с основными обязанностями классы мы нарушаем SRP.
Основные проблемы возникающие при использовании нотификаций:
Повышение связности кода достигается за счет того, что отправить нотификацию и получить ее может абсолютно любой класс в абсолютно любой части приложения.
Если нотификация отправлена, мы уже не можем не перехватить, не подменить ее.
Если несколько объектов подписаны на одну нотификацию, то невозможно управлять порядком получения нотификации.
Решение описанных проблем — отказаться от использования нотификаций в пользу более удобных механизмов.
Исключением может являться использовании нотификаций для оповещения о изменении глобального состояния приложения, например изменение состояния сетевого подключения или уход приложения в фон и выход из фона.
Формальные признаки нарушения SRP:
- Одним из самых заметных признаков является разрастание размера класса или метода.
- Наследование класса от большего количества протоколов (при условии соблюдения ISP).
- Использование Singleton-ов может свидетельствовать о нарушении SRP.
- Передача информации о изменении состояния конкретного объекта с использованием NSNotificationCenter (можно нотификациями сообщать об изменении глобального состояния)
- Скапливание в объектах утилитных методов.
- Большее количество публичных методов класса, также может свидетельствовать о нарушении SRP.
- Большее количество приватных методов, которые можно разбить на группы. В этом случае каждая группа скорее всего имеет свою зону ответственности.
Автор: spolispastom