Іноді говорять, що програмування — це більше про читання коду, ніж про його написання. І що читання важче за написання. Якщо ми приймемо це як істину, тоді логічно буде приділити деякі зусилля на оптимізацію нашого коду для читання. Інкапсуляція є дуже корисним інструментом для цього.
Щоб покращити або виправити систему, нам потрібно знайти існуючий код, з яким ми маємо працювати, визначити межі, в яких потрібно працювати, а потім прочитати і зрозуміти код всередині цих меж, щоб ми розуміли всі наслідки, які зміни можуть спричинити.
Зазвичай межі, в яких ми будемо працювати, визначаються функцією або методом класу. Іноді ми стикаємося з довгим спагеті-кодом і нам потрібно витратити деякий час, щоб проаналізувати його і знайти довільні межі, які ми вважаємо достатньо безпечними. Я вважаю корисним розглядати весь код, який я знаходжу в таких межах, як код одного рівня розуміння.
Ось приклад деякого коду в тому ж рівні, що міститься в межах методу класу.
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