Не намагайся повторити це вдома.

І велика частина нашої повсякденної роботи полягає в тому, щоб переглядати код інших людей. Це дуже важливо з кількох причин: для того, щоб направляти менш досвідчених розробників і навчати їх правильному підходу, для того, щоб вчитися від інших розробників (кожен має чому навчити і кожен може чогось навчитися), і найголовніше — для того, щоб доставити якісний код.

Але іноді, переглядаючи роботу інших, можна натрапити на шматки коду, які, можливо, працюють і виконують свою задачу, але їх реалізація є поганою. У цьому випадку я натрапив на щось, що варто поділитися, щоб інші могли навчитися на цих помилках.

Нижче наведено витяг з 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

Leave a Reply

Your email address will not be published. Required fields are marked *