Одной из концепций, с которой мы добились большого успеха в команде Tuts +, являются объекты обслуживания . Мы использовали сервисные объекты, чтобы уменьшить связь в наших системах, сделать их более тестируемыми и сделать важную бизнес-логику более очевидной для всех разработчиков в команде.
Поэтому, когда мы решили кодифицировать некоторые из концепций, которые мы использовали в нашей разработке на Rails, в Ruby-гем (называемый Aldous ), сервисные объекты были в начале списка.
Сегодня я хотел бы дать краткое изложение сервисных объектов, поскольку мы реализовали их в Aldous . Надеюсь, это расскажет вам большинство вещей, которые вам нужно знать, чтобы использовать сервисные объекты Aldous в ваших собственных проектах.
Анатомия базового объекта обслуживания
Сервисный объект — это в основном метод, который обернут в объект. Иногда объект службы может содержать несколько методов, но самая простая версия — это просто класс с одним методом, например:
1
2
3
4
5
|
class DoSomething
def perform
# do stuff
end
end
|
Мы все привыкли использовать существительные для именования наших объектов, но иногда бывает трудно найти хорошее существительное для представления концепции, тогда как говорить об этом в терминах действия (или глагола) просто и естественно. Сервисный объект — это то, что мы получаем, когда мы «плывем по течению» и просто превращаем глагол в объект.
Конечно, с учетом приведенного выше определения мы можем превратить любое действие / метод в объект службы, если мы того пожелаем. Следующее…
1
2
3
4
5
|
class Customer
def createPurchase(order)
# do stuff
end
end
|
… можно превратить в:
1
2
3
4
5
6
7
8
|
class CreateCustomerPurchase
def initialize(customer, order)
end
def perform
# do stuff
end
end
|
Мы могли бы написать несколько других постов о влиянии сервисных объектов на дизайн вашей системы, о различных компромиссах, которые вы сделаете, и т. Д. А пока давайте просто осознаем их как концепцию и рассмотрим их как еще один инструмент у нас в арсенале.
Зачем использовать сервисные объекты в Rails
По мере того, как приложения Rails становятся больше, наши модели имеют тенденцию становиться достаточно большими, и поэтому мы ищем способы перенести из них некоторую функциональность в «вспомогательные» объекты. Но это часто легче сказать, чем сделать. У Rails нет концепции на уровне модели, которая более гранулярна, чем модель. Таким образом, вам в конечном итоге придется сделать много суждений:
- Вы создаете модель PORO или создаете класс в папке
lib
? - Какие методы вы перемещаете в этот класс?
- Как вы разумно называете этот класс, учитывая методы, которые мы в него переместили?
Теперь вам нужно сообщить, что вы сделали другим разработчикам в вашей команде и всем новым людям, которые присоединятся позже. И, конечно же, столкнувшись с аналогичной ситуацией, другие разработчики могут сделать другие суждения, что приведет к несоответствиям.
Сервисные объекты дают нам концепцию, которая более гранулирована, чем модель. У нас может быть единое местоположение для всех наших услуг, и вы можете переместить только один метод в службу. Вы называете этот класс в честь действия / метода, который он будет представлять. Мы можем извлекать функциональные возможности в более детализированные объекты без слишком большого количества суждений, что позволяет всей команде работать на одной странице, что позволяет нам приступить к созданию великолепного приложения.
Использование сервисных объектов уменьшает связь между вашими моделями Rails, и получающиеся сервисы можно многократно использовать из-за их небольшого размера / небольшой площади.
Служебные объекты также хорошо тестируются, так как они обычно не требуют столько тестовых шаблонов, сколько более тяжелые объекты, и вам нужно беспокоиться только о тестировании одного метода, который содержит объект.
И сервисные объекты, и их тесты легко читаются / понимаются, поскольку они очень сплочены (также является побочным эффектом их небольшого размера). Вы также можете отбрасывать и перезаписывать как сервисные объекты, так и их тесты практически по желанию, так как затраты на это относительно низкие, и поддерживать их интерфейс очень легко.
У сервисных объектов определенно много интересного, особенно когда вы вводите их в свои Rails-приложения.
Сервисные объекты с Aldous
Учитывая, что сервисные объекты настолько просты, зачем нам вообще драгоценный камень? Почему бы просто не создать PORO, и тогда вам не нужно беспокоиться о другой зависимости?
Вы могли бы определенно сделать это, и на самом деле мы делали это довольно долго в Tuts +, но благодаря широкому использованию мы закончили тем, что разработали несколько шаблонов для сервисов, которые сделали нашу жизнь немного проще, и это именно то, что мы сделали толкнул в Олдос . Эти образцы легки и не включают много волшебства. Они делают нашу жизнь немного проще, но мы сохраняем весь контроль, если нам это нужно.
Где они должны жить
Перво-наперво, где должны жить ваши услуги? Мы склонны помещать их в app/services
, поэтому вам нужно следующее в вашем app/config/application.rb
:
1
2
3
4
5
6
7
|
config.autoload_paths += %W(
#{config.root}/app/services
)
config.eager_load_paths += %W(
#{config.root}/app/services
)
|
Что они должны называться
Как я упоминал выше, мы склонны называть объекты службы после действий / глаголов (например, CreateUser
, RefundPurchase
), но мы также склонны добавлять ‘service’ ко всем именам классов (например, CreateUserService
, RefundPurchaseService
). Таким образом, независимо от того, в каком контексте вы находитесь (просматривая файлы в файловой системе, просматривая класс службы в любом месте кодовой базы), вы всегда будете знать, что имеете дело с объектом службы.
Это не навязывается драгоценным камнем, но стоит учесть урок.
Объекты обслуживания неизменны
Когда мы говорим неизменяемый, мы имеем в виду, что после инициализации объекта его внутреннее состояние больше не изменится. Это действительно здорово, так как позволяет гораздо проще рассуждать о состоянии каждого объекта, а также системы в целом.
Для того чтобы вышеприведенное было истинным, метод сервисного объекта не может изменить состояние объекта, поэтому любые данные должны быть возвращены как выходные данные метода. Это трудно осуществить напрямую, поскольку объект всегда будет иметь доступ к своему внутреннему состоянию. С Aldous мы пытаемся обеспечить его соблюдение с помощью конвенций и образования, и следующие два раздела покажут вам, как это сделать.
Представление успеха и неудачи
Сервисный объект Aldous всегда должен возвращать один из двух типов объектов:
-
Aldous::Service::Result::Success
-
Aldous::Service::Result::Failure
Вот пример:
01
02
03
04
05
06
07
08
09
10
11
|
class CreateUserService < Aldous::Service
def perform
user = User.new(user_data_hash)
if user.save
Result::Success.new
else
Result::Failure.new
end
end
end
|
Поскольку мы наследуем от Aldous::Service
, мы можем построить наши возвращаемые объекты как Result::Success
. Использование этих объектов в качестве возвращаемых значений позволяет нам делать такие вещи, как:
1
2
3
4
5
6
7
|
hash = {}
result = CreateUserService.perform(hash)
if result.success?
# do success stuff
else #result.failure?
# do failure stuff
end
|
Теоретически мы могли бы просто вернуть true или false и получить то же поведение, что и выше, но если бы мы это сделали, мы не смогли бы переносить дополнительные данные с нашим возвращаемым значением, и мы часто хотим переносить данные.
Использование DTO
Успех или неудача операции / услуги — это только часть истории. Часто мы создаем некоторый объект, который мы хотим вернуть, или генерируем некоторые ошибки, о которых мы хотим уведомить вызывающий код. Вот почему возврат объектов, как мы показали выше, полезен. Эти объекты используются не только для обозначения успеха или неудачи, они также являются объектами передачи данных .
Aldous позволяет вам переопределить метод в базовом классе обслуживания, чтобы указать набор значений по умолчанию, которые будут содержать объекты, возвращаемые службой, например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
class CreateUserService < Aldous::Service
attr_reader :user_data_hash
def initialize(user_data_hash)
@user_data_hash = user_data_hash
end
def default_result_data
{user: nil}
end
def perform
user = User.new(user_data_hash)
if user.save
Result::Success.new(user: user)
else
Result::Failure.new
end
end
end
|
Хеш-ключи, содержащиеся в default_result_data
, автоматически становятся методами для объектов Result::Success
и Result::Failure
возвращаемых службой. И если вы предоставите другое значение для одного из ключей в этом методе, он переопределит значение по умолчанию. Итак, в случае вышеупомянутого класса:
1
2
3
4
5
6
7
8
9
|
hash = {}
result = CreateUserService.perform(hash)
if result.success?
result.user # will be an instance of User
result.blah # would raise an error
else #result.failure?
result.user # will be nil
result.blah # would raise an error
end
|
По сути, хэш-ключи в методе default_result_data
являются контрактом для пользователей объекта службы. Мы гарантируем, что вы сможете вызывать любой ключ в этом хэше в качестве метода для любого объекта результата, который выходит из службы.
Безошибочные API
Когда мы говорим об API без ошибок, мы имеем в виду методы, которые никогда не вызывают ошибок, но всегда возвращают значение, указывающее на успех или неудачу. Я писал об API без ошибок раньше. Сервисы Aldous безошибочны в зависимости от того, как вы их называете. В приведенном выше примере:
1
|
result = CreateUserService.perform(hash)
|
Это никогда не вызовет ошибки. Внутренне Aldous помещает ваш метод rescue
блок восстановления, и если ваш код вызывает ошибку, он возвращает Result::Failure
с данными default_result_data
качестве данных.
Это очень освобождает, потому что вам больше не нужно думать о том, что может пойти не так с написанным вами кодом. Вас интересует только успех или неудача вашего обслуживания, и любая ошибка приведет к сбою.
Это отлично подходит для большинства ситуаций. Но иногда вы хотите сгенерированную ошибку. Лучшим примером этого является случай, когда вы используете служебный объект в фоновом работнике, и ошибка может привести к повторной попытке фонового работника. Вот почему служба Aldous также волшебным образом получает представление perform!
метод и позволяет переопределить другой метод из базового класса. Вот снова наш пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class CreateUserService < Aldous::Service
attr_reader :user_data_hash
def initialize(user_data_hash)
@user_data_hash = user_data_hash
end
def raisable_error
MyApplication::Errors::UserError
end
def default_result_data
{user: nil}
end
def perform
user = User.new(user_data_hash)
if user.save
Result::Success.new(user: user)
else
Result::Failure.new
end
end
end
|
Как видите, теперь мы переопределили метод raisable_error
. Иногда мы хотим, чтобы произошла ошибка, но мы также не хотим, чтобы это была какая-либо ошибка. В противном случае наш код вызова должен был бы знать о каждой возможной ошибке, которую может вызвать служба, или вынужден перехватывать один из базовых типов ошибок. Вот почему, когда вы используете perform!
метод, Aldous по-прежнему будет отлавливать все ошибки для вас, но затем повторно вызовет raisable_error
вами raisable_error
и установит исходную ошибку в качестве причины. Теперь вы можете иметь это:
1
2
3
4
5
6
7
|
hash = {}
begin
service = CreateUserService.build(hash)
result = service.perform!
rescue service.raisable_error => e
# error stuff
end
|
Тестирование старых сервисных объектов
Возможно, вы заметили использование фабричного метода:
1
2
|
CreateUserService.build(hash)
CreateUserService.perform(hash)
|
Вы должны всегда использовать их, и никогда не создавать сервисные объекты напрямую. Фабричные методы — это то, что позволяет нам аккуратно подключить такие приятные функции, как автоматическое восстановление и добавление default_result_data
.
Однако, когда дело доходит до тестов, вам не нужно беспокоиться о том, как Aldous расширяет функциональность ваших сервисных объектов. Поэтому при тестировании просто создайте объекты напрямую с помощью конструктора, а затем протестируйте свою функциональность. Вы получите спецификации для написанной вами логики и будете уверены, что Aldous будет делать то, что должен (для этого у Aldous есть собственные тесты), когда дело доходит до производства.
Вывод
Надеюсь, это дало вам представление о том, как сервисные объекты (и особенно сервисные объекты Aldous) могут быть хорошим инструментом в вашем арсенале при работе с Ruby / Rails. Попробуйте Aldous и дайте нам знать, что вы думаете. Также не стесняйтесь взглянуть на код Aldous. Мы не просто написали это, чтобы быть полезным, но также чтобы быть читаемым и простым для понимания / изменения.