Things I like about encapsulation: levels of understanding
Sometimes it is said that programming is more about reading code than it is about writing code. And that reading is harder than writing. If we consider these true, it makes sense to put some effort on optimizing our code for reading. Encapsulation is a very useful tool for that.
To improve or fix a system we need to find the existing code we have to work with, determine the boundaries in which we need to work, and then we need to read and understand the code inside that boundary so we understand all the consequences the changes can cause.
Usually, the boundaries we are going to work within are defined by a function, or a class method. Sometimes we find some long spaghetti code and we have to spend some time analyzing it to find some arbitrary boundaries we determine are safe enough. I find it useful to consider all the code I find in such boundaries to be in one level of understanding.
Here is an example of some code in the same level, contained in the boundaries of a class method.
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;
}
}
This code is meant to calculate the price of a product given a quantity of items and an optional promo code. The requirements to calculate that are the following:
- Fetch the product price
- Get the total price by multiplying the product price by the quantity
- Find if the promo code applies to the product
- If it applies, discard the discount if the quantity is lower than the minimum to qualify for the discount
- If not discarded, apply the discount for each product up to the maximum
- Find if the product includes a discount for a bulk purchase
- Discard quantity discount if the quantity is lower than the minimum to qualify for the discount
- If not discarded, apply the percentage discount rounding down
This is a lot to process before you can start working with that code. If the job to be done is an update in the promo code logic, there is also a good amount of code that is not relevant, but you still need to consider to make sure it does not break.
We can make this code easier to read and work with by using encapsulation.
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;
}
}
This code encapsulates the two different discount calculations and pushes them to another level. With this code you can read what is the price calculation about in a high level.
- Fetch the product price
- Get the total price by multiplying the product price by the quantity
- Apply promo code discount
- Apply quantity discount
These two last parts are encapsulating the code in the example before and pushing it to a lower level, so it does not disappear, it is just moved to its own service. Now if you need to work with the promo code discount calculation you don’t need to read and understand the whole price calculation service, you can navigate to the CalculatePromoDiscountService
, which could look like this.
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;
}
}
Testing is better too
Imagine trying to write unit tests for the first price calculator service. You would need to write cases for combinations of different products with different amounts, promo codes and bulk prices. Covering all combinations would make the test quite large.
If we apply the encapsulation we would end with more tests, but each of them would be smaller and more focused. That can be a win too.
Conclusion
With this kind of techniques you need to have an eye on the number of classes you are creating, because encapsulating too much will probably produce a big number of files that can create other kinds of problems. But used in the right places it can make complex code more manageable.
If you like my content, you can buy me a coffee to support it.