Статьи

SOLID Ruby: принцип единой ответственности

Мы используем методы 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Основная обязанность этого класса — заботиться о бизнес-логике, связанной с состоянием игры — проверки и отношения в этом случае.

Но, как мы видим, он также отвечает за консультации с веб-сервисом, чтобы узнать цену игры и за вывод информации об игре в заранее заданном формате. Эта реализация имеет низкую когезию, у нее есть несколько причин для изменения:
— Проверка и отношения (мы поговорим больше о 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Теперь у нас есть класс, сфокусированный на каждой ранее определенной ответственности.

Чтобы установить цену игры, вы можете использовать объект «оркестровки», который получает игру и применяет цену, полученную от веб-службы. Другой вариант — сделать это прозрачным через класс 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

view raw
gistfile1.rb
hosted with ❤ by GitHub

Я не люблю вызывать внешние сервисы из моделей ActiveRecord, но это способ сделать это.

В коде все еще есть некоторые недостатки, и мы улучшим его в следующих статьях (совет: мы связываем наши объекты с конкретными реализациями вместо ролей).

Реальный пример, который я всегда люблю приводить, это класс Creditcard от Spree.

Вы можете видеть, что, помимо поведения ActiveRecord «по умолчанию» (проверка и отношения), этот класс отвечает за авторизацию карты, обработку платежа за покупку и множество связанных действий. Это явно плохо. У нас есть класс с более чем одной ответственностью с другими обязанностями, частично выполненными им. Ему не хватает сплоченности и он сильно связан (это зависит от других частей, чтобы полностью выполнить свою ответственность).

Если вы посмотрите на текущий класс (главную ветвь), вы заметите, что он был реорганизован и из него извлечено много поведения. Это отличный пример того, как можно улучшить ситуацию с помощью SRP.

А как насчет ActiveRecord? Это нарушает SRP? Да, это так, и это по дизайну. Если вы прочитаете описание шаблона Active Record (не путайте его с ActiveRecord, фреймворком), вы заметите, как это сознательный компромисс: мы обмениваемся на более высокий уровень изоляции для более прямого и быстрого подходить. Я в порядке с этим, пока модели ActiveRecord не делают больше, чем поиск и логические проверки (методы предикатов).

Имейте в виду, что быть экстремистским (с обеих сторон) плохо. Не задумывайтесь над принципами, иначе у вас будет слишком много уровней косвенности и кода, которые так же сложно поддерживать, как и «собранные вместе» версии.

Вот список советов, чтобы не нарушать SRP:

  • В вашем классе слишком много комментариев, объясняющих, почему что-то есть;
  • Вы используете слишком много переменных экземпляра и часто используете их как отдельные группы во всех методах, как будто каждая из них была структурой данных (каждая группа, скорее всего, принадлежит отдельному классу);
  • Вы передаете слишком много параметров для вызовов методов или конструкторов. Здесь нет «магического числа», но я начинаю искать лучшую абстракцию после четырех параметров;
  • Использование частных / защищенных методов часто (но не всегда) означает, что кусок кода не принадлежит и должен быть извлечен;
  • Большинство методов вашего класса интенсивно используют условные выражения.

Вот суть с дополнительными ссылками на SOLID и в целом хороший код ООП.

Вот и все, что касается принципа единой ответственности. Следующая остановка — принцип обращения зависимостей.