І велика частина нашої повсякденної роботи полягає в тому, щоб переглядати код інших людей. Це дуже важливо з кількох причин: для того, щоб направляти менш досвідчених розробників і навчати їх правильному підходу, для того, щоб вчитися від інших розробників (кожен має чому навчити і кожен може чогось навчитися), і найголовніше — для того, щоб доставити якісний код.
Але іноді, переглядаючи роботу інших, можна натрапити на шматки коду, які, можливо, працюють і виконують свою задачу, але їх реалізація є поганою. У цьому випадку я натрапив на щось, що варто поділитися, щоб інші могли навчитися на цих помилках.
Нижче наведено витяг з Ruby on Rails додатку. У цьому випадку це дія контролера, яка при виклику повинна звертатися до об'єкта сервісу і, якщо успішно, перенаправляти на інший маршрут. Якщо ж не успішно — знову показувати форму і відображати повідомлення про помилку.
def create
result = SomeService.new(service_params).call
if result == true
redirect_to root_url, notice: t(".success")
else
render :new, alert: t(".error", message: result)
end
end
Звернули увагу на щось?
Правильно, результат виклику SomeService
— це або Boolean
, або String
. Це є проблемою з кількох причин. Перша причина — семантика. Хоча семантика значення, яке повертається сервісом, має вказувати на те, чи було воно успішним, на практиці воно також говорить про те, що сталося не так і чому сервіс не був успішним. Друга причина — ви не знайдете умов типу result == true
в коді Ruby загалом. Це, на мою думку, є запахом, вказівкою на те, що щось не так.
Які ж можливі рішення? Є кілька альтернатив.
Перше, інтуїтивно зрозуміле рішення — це повернення Hash
, що містить результат виклику SomeService
. Наприклад:
class SomeService
def call
# тут відбувається магія
if service_was_successful?
{result: :success}
else
{result: :error, message: "Щось пішло не так :("}
end
end
end
Контролер у свою чергу виглядатиме значно виразніше:
def create
some_service = SomeService.new(service_params).call
if some_service[:result] == :success
redirect_to root_url, notice: t(".success")
else
render :new, alert: t(".error", message: some_service[:message])
end
end
Покращенням цього є повернення об'єкта ServiceResult
замість Hash
. Цей об'єкт буде мати методи #success?
та #errors
, і завдяки цьому ми отримуємо не тільки більш виразний контролер, але й визначаємо контракт, інтерфейс, з яким повинні погоджуватися всі інші сервіси в нашому додатку.
Інший підхід — використання вбудованого механізму, який уже є доступним і який можна застосувати в таких ситуаціях: виключення. Давайте подумаємо про це. Нормальним результатом роботи сервісу має бути успіх, і лише в окремих випадках має бути помилка. Таким чином, немає необхідності пам'ятати, яке значення має повернути сервіс. Замість цього, ми повинні зрозуміти, що він робить і які помилки можуть виникнути. Давайте переімплементуємо цей сервіс знову:
class SomeService
class ThirdPartyServiceUnavailableError < ::Error
end
def call
# тут відбувається магія # сервіс може продовжити (або не продовжити) після цього рядка
raise ThirdPartyServiceUnavailableError if third_party_service_is_unavaiable? # ...
end
end
Контролер також виглядатиме значно виразніше і пряміше:
def create
SomeService.new(service_params).call
redirect_to root_url, notice: t(".success")
rescue ThirdPartyServiceUnavailableError => e
render :new, alert: t(".error", message: e.message)
end
Однак, цей підхід не рекомендується у більшості випадків, оскільки виключення є дорогими операціями і повинні використовуватися тільки в особливих випадках, як це і вказує їх назва.
Велика частина нашої повсякденної роботи полягає в тому, щоб переглядати код інших людей. Це дуже важливо з кількох причин: для того, щоб направляти менш досвідчених розробників і навчати їх правильному підходу, для того, щоб вчитися від інших розробників (кожен має чому навчити і кожен може чогось навчитися), і найголовніше — для того, щоб доставити якісний код.
Але іноді, переглядаючи роботу інших, можна натрапити на шматки коду, які, можливо, працюють і виконують свою задачу, але їх реалізація є поганою. У цьому випадку я натрапив на щось, що варто поділитися, щоб інші могли навчитися на цих помилках.
Нижче наведено витяг з Ruby on Rails додатку. У цьому випадку це дія контролера, яка при виклику повинна звертатися до об'єкта сервісу і, якщо успішно, перенаправляти на інший маршрут. Якщо ж не успішно — знову показувати форму і відображати повідомлення про помилку.
def create
result = SomeService.new(service_params).call
if result == true
redirect_to root_url, notice: t(".success")
else
render :new, alert: t(".error", message: result)
end
end
Звернули увагу на щось?
Правильно, результат виклику SomeService
— це або Boolean
, або String
. Це є проблемою з кількох причин. Перша причина — семантика. Хоча семантика значення, яке повертається сервісом, має вказувати на те, чи було воно успішним, на практиці воно також говорить про те, що сталося не так і чому сервіс не був успішним. Друга причина — ви не знайдете умов типу result == true
в коді Ruby загалом. Це, на мою думку, є запахом, вказівкою на те, що щось не так.
Які ж можливі рішення? Є кілька альтернатив.
Перше, інтуїтивно зрозуміле рішення — це повернення Hash
, що містить результат виклику SomeService
. Наприклад:
class SomeService
def call
# тут відбувається магія
if service_was_successful?
{result: :success}
else
{result: :error, message: "Щось пішло не так :("}
end
end
end
Контролер у свою чергу виглядатиме значно виразніше:
def create
some_service = SomeService.new(service_params).call
if some_service[:result] == :success
redirect_to root_url, notice: t(".success")
else
render :new, alert: t(".error", message: some_service[:message])
end
end
Покращенням цього є повернення об'єкта ServiceResult
замість Hash
. Цей об'єкт буде мати методи #success?
та #errors
, і завдяки цьому ми отримуємо не тільки більш виразний контролер, але й визначаємо контракт, інтерфейс, з яким повинні погоджуватися всі інші сервіси в нашому додатку.
Інший підхід — використання вбудованого механізму, який уже є доступним і який можна застосувати в таких ситуаціях: виключення. Давайте подумаємо про це. Нормальним результатом роботи сервісу має бути успіх, і лише в окремих випадках має бути помилка. Таким чином, немає необхідності пам'ятати, яке значення має повернути сервіс. Замість цього, ми повинні зрозуміти, що він робить і які помилки можуть виникнути. Давайте переімплементуємо цей сервіс знову:
class SomeService
class ThirdPartyServiceUnavailableError < ::Error
end
def call
# тут відбувається магія # сервіс може продовжити (або не продовжити) після цього рядка
raise ThirdPartyServiceUnavailableError if third_party_service_is_unavaiable? # ...
end
end
Контролер також виглядатиме значно виразніше і пряміше:
def create
SomeService.new(service_params).call
redirect_to root_url, notice: t(".success")
rescue ThirdPartyServiceUnavailableError => e
render :new, alert: t(".error", message: e.message)
end
Однак, цей підхід не рекомендується у більшості випадків, оскільки виключення є дорогими операціями і повинні використовуватися тільки в особливих випадках, як це і вказує їх назва.
Вони цілком підходять для обробки наведеного вище прикладу (наприклад, недоступність стороннього сервісу), але можуть стати дуже проблемними, якщо вся бізнес-логіка залежить від них.
Як завжди, пам'ятайте, що немає універсального правила щодо того, який підхід слід обрати. Кожен з цих варіантів має свої переваги та недоліки, і зазвичай немає єдиного правильного рішення для проблеми проєктування. Ми використовуємо всі ці варіанти в наших продуктивних кодових базах. Хоча ми надаємо перевагу потужній послідовності об'єктів результату, вони не завжди підходять для кожного застосунку (наприклад, додавання цієї концепції в застосунки, які вже мають 7–10 років в експлуатації і набір дуже різних підходів до проєктування, може призвести до більшого шуму, ніж користі, і ще однієї концепції для навантаження та обробки в головах членів команди).
Підсумовуючи, всі ці варіанти є дійсними, завжди оцінюйте і обирайте інструменти, які найкраще підходять для поточної ситуації в бізнесі.
Вони чудово підходять для обробки наведеного вище прикладу (тобто, недоступність стороннього сервісу), але можуть стати дуже проблемними, якщо вся бізнес-логіка залежатиме від них.
Як завжди, пам'ятайте, що немає єдиного універсального підходу, який слід застосовувати. Кожен з цих варіантів має свої переваги та недоліки, і зазвичай немає одного правильного рішення для проблеми проєктування. Ми використовуємо всі ці варіанти в наших продуктивних кодових базах. Хоча ми надаємо перевагу потужній послідовності об'єктів результату, вони не завжди підходять для кожного застосунку (наприклад, додавання цієї концепції в застосунки, що вже мають 7–10 років в експлуатації і набір дуже різних підходів до проєктування, може створити більше шуму, ніж користі, і ще одну концепцію для обробки в головах кожного члена команди).
Підсумовуючи, всі ці варіанти є дійсними, завжди оцінюйте ситуацію та обирайте інструменти, які найкраще підходять для поточної ситуації в бізнесі.
Перекладено з: Don’t do this at home