Речі, які мені подобаються в інкапсуляції: рівні розуміння

pic

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

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

Зазвичай межі, в яких ми будемо працювати, визначаються функцією або методом класу. Іноді ми стикаємося з довгим спагеті-кодом і нам потрібно витратити деякий час, щоб проаналізувати його і знайти довільні межі, які ми вважаємо достатньо безпечними. Я вважаю корисним розглядати весь код, який я знаходжу в таких межах, як код одного рівня розуміння.

Ось приклад деякого коду в тому ж рівні, що міститься в межах методу класу.

final readonly class CalculateProductPriceService  
{  
 public function __construct(  
 private ProductPriceRepositoryInterface $productPriceRepository,  
 private PromoCodeRepositoryInterface $promoCodeRepository,  
 private BulkDiscountRepositoryInterface $bulkDiscountRepository,  
 ) {}  

 public function calculate(string $productId, int $quantity, ?string $promoCode): int  
 {  
 $price = $this->productPriceRepository->fetch($productId) * $quantity;  

 if (!$promoCode) {  
 $promoDiscount = 0;  
 } else {  
 $promo = $this->promoCodeRepository->fetch($productId, $promoCode);  

 if ($quantity > $promo->minQuantity) {  
 $promoDiscount = $promo->discount * min($quantity, $promo->maxQuantity);  
 }  
 }  

 $promoPrice = $price - $promoDiscount;  

 $bulkDiscount = $this->bulkDiscountRepository->fetch($productId);  

 if ($quantity < $bulkDiscount->minQuantity) {  
 $bulkDiscount = 0;  
 } else {  
 $bulkDiscount = floor($promoPrice * $bulkDiscount->percentageDiscount / 100);  
 }  

 $bulkPrice = $promoPrice - $bulkDiscount;  

 return $bulkPrice;  
 }  
}

Цей код призначений для обчислення ціни продукту на основі кількості одиниць та необов'язкового промокоду. Для цього необхідно виконати наступні вимоги:

  • Отримати ціну продукту
  • Обчислити загальну ціну, помноживши ціну продукту на кількість
  • Перевірити, чи застосовується промокод до продукту
  • Якщо він застосовується, скасувати знижку, якщо кількість менша за мінімальну для отримання знижки
  • Якщо знижка не скасована, застосувати її для кожного продукту до максимального ліміту
  • Перевірити, чи є у продукту знижка за покупку оптом
  • Скасувати знижку за кількість, якщо кількість менша за мінімальну для отримання знижки
  • Якщо знижка не скасована, застосувати відсоткову знижку, округлюючи в менший бік

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

Ми можемо зробити цей код більш зрозумілим і зручним для роботи, застосовуючи інкапсуляцію.

final readonly class CalculateProductPriceService  
{  
 public function __construct(  
 private ProductPriceRepositoryInterface $productPriceRepository,  
 private CalculatePromoDiscountService $calculatePromoDiscountService,  
 private CalculateBulkDiscountService $calculateBulkDiscountService,  
 ) {}  

 public function calculate(string $productId, int $quantity, ?string $promoCode): int  
 {  
 $price = $this->productPriceRepository->fetch($productId) * $quantity;  

 $promoDiscount = $this->calculatePromoDiscountService->calculate($productId, $quantity, $promoCode);  
 $promoPrice = $price - $promoDiscount;  

 $quantityDiscount = $this->calculateBulkDiscountService->calculate($productId, $quantity, $promoCode);  
 $bulkPrice = $promoPrice - $quantityDiscount;  

 return $bulkPrice;  
 }  
}

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

  • Отримати ціну продукту
  • Обчислити загальну ціну, помноживши ціну продукту на кількість
  • Застосувати знижку за промокодом
  • Застосувати знижку за кількість

Ці дві останні частини інкапсулюють код з попереднього прикладу та переміщають його на нижчий рівень, щоб він не зник, а був переміщений до власного сервісу. Тепер, якщо вам потрібно працювати з обчисленням знижки за промокодом, вам не потрібно читати і розуміти весь сервіс обчислення ціни, ви можете перейти до CalculatePromoDiscountService, який може виглядати так.

final readonly class CalculatePromoDiscountService  
{  
 public function __construct(  
 private PromoCodeRepositoryInterface $promoCodeRepository,  
 ) {}  

 public function calculate(string $productId, int $quantity, ?string $promoCode): int  
 {  
 if (!$promoCode) {  
 return 0;  
 }  

 $promo = $this->promoCodeRepository->fetch($productId, $promoCode);  

 if ($quantity > $promo->minQuantity) {  
 return $promo->discount * min($quantity, $promo->maxQuantity);  
 }  

 return 0;  
 }  
}

Тестування також стає простішим

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

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

Висновок

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

Якщо вам подобається мій контент, ви можете підтримати мене кавою.

Перекладено з: Things I like about encapsulation: levels of understanding

Leave a Reply

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