Мы используем методы BDD и объектно-ориентированного программирования для получения ясного и элегантного кода, верно? Фактически, это результаты основной цели: создать код с низкими затратами на обслуживание, код, который не требует много времени и людей для исправлений и улучшений.
Существует группа руководств и принципов, которые помогут нам достичь этой цели. Он называется SOLID и был впервые описан Робертом Мартином (он же Дядя Боб) более десяти лет назад.
SOLID не является набором жестких правил и не является единственной группой рекомендаций, которую вы должны использовать, но это очень хорошая отправная точка и она должна сразу изменить ваш дизайн и код программного обеспечения. Вот принципы, описанные SOLID:
- Принцип единой ответственности
- Открыто-Закрытый Принцип
- Принцип замещения Лискова
- Принцип разделения интерфейса
- Принцип обращения зависимостей
Цель этих принципов — сделать изменения кода как можно более свободными от побочных эффектов. Это означает, что исправления или улучшения должны привести к изменению кода в как можно меньшем количестве мест. Мы эффективно снижаем затраты на техническое обслуживание благодаря дизайну, сделанному с учетом изменений.
Я призываю вас не становиться жертвой легкого мышления «это Java BS, нам здесь это не нужно». Хороший объектно-ориентированный дизайн применим к любому объектно-ориентированному языку. Форма может измениться (совет: Ruby до смешного легко реализует эти принципы), но идея и основы совпадают.
Еще один обязательный отказ от ответственности: нет единого способа реализовать все эти принципы. Попробуйте некоторые методы и выберите то, что имеет смысл для вас, основываясь на вашем стиле и образе мышления. Я обычно показываю один вариант, но имейте в виду, что есть гораздо больше. Если вы не согласны, пожалуйста, не стесняйтесь высказать свою точку зрения в комментариях.
В этой статье я расскажу о принципе единой ответственности (SRP). Это самый простой для понимания и основа для всего хорошего объектно-ориентированного кода.
Примечание: я часто буду использовать термин «класс» в этой статье, но в большинстве случаев принципы могут применяться к любому «объекту», будь то класс, модуль, метод и так далее.
SRP заявляет, что класс должен иметь только одну ответственность и должен полностью ее выполнять — эта ответственность также не должна распределяться между несколькими классами. Это способ достижения высокой когезии, весьма желательная черта в ОО-программном обеспечении. Связанный класс полностью выполняет свою ответственность, защищая его от фрагментации и скрывая детали реализации.
Один из способов думать о том, что является обязанностью, — это думать о ней как о причине изменения класса. Таким образом, мы можем определить этот принцип следующим образом: у класса должна быть только одна причина для изменения.
Хорошо, время кодировать! Вот гипотетический класс ActiveRecord, представляющий видеоигру:
class Game < ActiveRecord::Base | |
belongs_to :category | |
validates_presence_of :title, :category_id, :description, | |
:price, :platform, :year | |
def get_official_price | |
open(«http://thegamedatabase.com/api/game/#{name}/price?api_key=ek2o1je») | |
end | |
def print | |
<<-EOF | |
#{name}, #{platform}, #{year} | |
current value is #{get_official_price} | |
EOF | |
end | |
end |
Основная обязанность этого класса — заботиться о бизнес-логике, связанной с состоянием игры — проверки и отношения в этом случае.
Но, как мы видим, он также отвечает за консультации с веб-сервисом, чтобы узнать цену игры и за вывод информации об игре в заранее заданном формате. Эта реализация имеет низкую когезию, у нее есть несколько причин для изменения:
— Проверка и отношения (мы поговорим больше о ActiveRecord, нарушающем SRP позже)
— Информация о цене
— Форматирование вывода
Хороший способ решить эту проблему — разделить этот класс и написать два дополнительных класса:
class Game < ActiveRecord::Base | |
belongs_to :category | |
validates_presence_of :title, :category_id, :description, | |
:price, :platform, :year | |
end | |
class GamePriceService | |
attr_accessor :game | |
# we could use a config file | |
BASE_URL = «http://thegamedatabase.com/api/game» | |
API_KEY = «ek2o1je» | |
def initialize(game) | |
self.game = game | |
end | |
def get_price | |
data = open(«#{BASE_URL}/#{game.name}/price?api_key=#{API_KEY}«) | |
JsonParserLib.parse(data) | |
end | |
end | |
class GamePresenter | |
attr_accessor :game | |
def initialize(game) | |
self.game = game | |
end | |
def print | |
price_service = GamePriceService.new(game) | |
<<-EOF | |
#{game.name}, #{game.platform}, #{game.year} | |
current value is #{price_service.get_price[:value]} | |
EOF | |
end | |
end |
Теперь у нас есть класс, сфокусированный на каждой ранее определенной ответственности.
Чтобы установить цену игры, вы можете использовать объект «оркестровки», который получает игру и применяет цену, полученную от веб-службы. Другой вариант — сделать это прозрачным через класс Game:
class Game < ActiveRecord::Base | |
belongs_to :category | |
validates_presence_of :title, :category_id, :description, | |
:price, :platform, :year | |
def price | |
price = read_attribute(:price) | |
unless price.present? | |
update_attribute(:price, GamePriceService.new(self).get_price) | |
read_attribute(:price) | |
end | |
end | |
end |
Я не люблю вызывать внешние сервисы из моделей ActiveRecord, но это способ сделать это.
В коде все еще есть некоторые недостатки, и мы улучшим его в следующих статьях (совет: мы связываем наши объекты с конкретными реализациями вместо ролей).
Реальный пример, который я всегда люблю приводить, это класс Creditcard от Spree.
Вы можете видеть, что, помимо поведения ActiveRecord «по умолчанию» (проверка и отношения), этот класс отвечает за авторизацию карты, обработку платежа за покупку и множество связанных действий. Это явно плохо. У нас есть класс с более чем одной ответственностью с другими обязанностями, частично выполненными им. Ему не хватает сплоченности и он сильно связан (это зависит от других частей, чтобы полностью выполнить свою ответственность).
Если вы посмотрите на текущий класс (главную ветвь), вы заметите, что он был реорганизован и из него извлечено много поведения. Это отличный пример того, как можно улучшить ситуацию с помощью SRP.
А как насчет ActiveRecord? Это нарушает SRP? Да, это так, и это по дизайну. Если вы прочитаете описание шаблона Active Record (не путайте его с ActiveRecord, фреймворком), вы заметите, как это сознательный компромисс: мы обмениваемся на более высокий уровень изоляции для более прямого и быстрого подходить. Я в порядке с этим, пока модели ActiveRecord не делают больше, чем поиск и логические проверки (методы предикатов).
Имейте в виду, что быть экстремистским (с обеих сторон) плохо. Не задумывайтесь над принципами, иначе у вас будет слишком много уровней косвенности и кода, которые так же сложно поддерживать, как и «собранные вместе» версии.
Вот список советов, чтобы не нарушать SRP:
- В вашем классе слишком много комментариев, объясняющих, почему что-то есть;
- Вы используете слишком много переменных экземпляра и часто используете их как отдельные группы во всех методах, как будто каждая из них была структурой данных (каждая группа, скорее всего, принадлежит отдельному классу);
- Вы передаете слишком много параметров для вызовов методов или конструкторов. Здесь нет «магического числа», но я начинаю искать лучшую абстракцию после четырех параметров;
- Использование частных / защищенных методов часто (но не всегда) означает, что кусок кода не принадлежит и должен быть извлечен;
- Большинство методов вашего класса интенсивно используют условные выражения.
Вот суть с дополнительными ссылками на SOLID и в целом хороший код ООП.
Вот и все, что касается принципа единой ответственности. Следующая остановка — принцип обращения зависимостей.