Дивні умови та Domain Driven Development

Якщо Ви бажаєте працювати із зрозумілим кодом, що має сенс та який можливо легко підтримувати – в ньому мають бути відсутні дивні умови. Що я маю на увазі, вживаючи словосполучення “дивні умови”? Дивна умова – це умова без назви. Приведу декілька прикладів дивних умов:

(1):
  if deliverable.height < container.height &&
     deliverable.width < container.width &&
     deliverable.depth < container.depth

(2):
  if order.items.all? { |i| i.quantity >= cfg.order.min_order_items_qnt(i) } 

(3):
  if order.items.quantity >= cfg.discount.min_order_items_qnt &&
     order.items.sum >= cfg.discount.min_order_items_total_sum

Що всі ці умови значать?

З точки зору Domain Driven Development кожна умова повинна мати зрозумілу назву в термінах предметної області (domain’у). Давайте перепишемо згадані вище умови аби побачити як код можно зробити значно зрозуміліше:

(1):
  if deliverable.fits container

(2):
  if processable order

(3):
  if discountable order

Справа не у тому, що коду стало менше. Насправді його стало більше за рахунок того, що код умов було обрамлено деклараціями функцій/методів. Також код став повільнішим за рахунок додаткового виклику функцій (якщо тільки немає т.з. інлайнінгу). Справа у тому, що код став зрозумілішим. Розробник пише код один раз, а потім сам та інші розробники на проєкті читають його сотні разів. Коли умова має зрозумілу назву, то читання такого коду викликає значно менше когнітивне навантаження, що пришвидшує розробку та зменшує кількість дефектів.

Бонус 1

Код, що виконується за певної умови також повинен бути інкапсульований у термін з предметної області. Ми розуміємо, що значить умова, якщо вона записана так, як було описано вище, але що виконується за цієї умови – це також дуже важливо. Код, що виконується за певної умови вже поєднаний цією умовою, він вже є одним і це одне повинне мати назву. Це значить, що увесь код з умовами має виглядати якось так:

(1):
  container.put(deliverable) if deliverable.fits(container)

(2):
  process(order) if processable order

(3):
  discount(order) if discountable order

Бонус 2

Оскільки умова, та код, що за неї виконується поєдані, то чому вони роз’єднані? process(order) if processable(order) має бути одним цілим, іншими словами, виглядати якось так: process(order) та інкапсулювати умову у самому собі, бо в іншому випадку легко забути перевірити умову та опрацювати замовлення, яке не має бути опрацьоване.

Бонус 3

По-перше, розгалуження у коді – це погано, адже ускладнює його для читання та уповільнює його виконання.

По-друге, як я вже писав у Відповідність функцій принципу єдиної відповідальності функція має виконувати лише одну дію, а перевірка, чи ця дія може бути виконана – це вже друга дія. Функції, окрім тих, що отримують дані від джерел, яким ми не довіряємо (наприклад, від користувачів) мають отримувати лише коректні дані для своєї роботи та не перевіряти їх.

По-трете, навіщо нам необхідні замовлення, які не можуть бути опрацьовані?! Умови навколо значень певних типів створюють неявну підтипізацію (sub-typing), що лише ускладнюють код.

Як ми вирішимо цю проблему?

(1):
  Container.put(deliverable) # знаходимо відповідний контейнер, а не перевіряємо, чи якийсь контейнер підходить та одразу кладемо замовлене у нього. Саме тому ми кладемо (put) замовлення не у конкретний контейнер, а у клас контейнерів.

(2):
  order.process # будь-який екземпляр <#Order> повинен бути таким, що може бути опрацьованим, а інакше це не замовлення (Order.create створює лише валідні замовлення, а не замовлення, які не замовлення)

(3):
  order.discount # якщо неможливо отримати знижку на замовлення, то знижка становить 0

Те, що я переписав приклади з використанням об’єктної семантики, не має великого значення. Головне криється у коментарях.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *