I wrote Why is Collector Pattern so Awesome a while ago, but I got feeling and feedback that it's way too complicated.
The pattern itself is simple, but put in framework context, it might be too confusing to understand.
That's why we look on collector pattern in minimalistic plain PHP way today.
Let's say you have a simple PriceCalculator
class that calculates a price for a product with a VAT:
class PriceCalculator
{
public function calculate(Product $product): float
{
// compute vat
$price = $product->getPrice() * 1.15;
return $price;
}
}
Then we decide to have 50 % discount for admins:
class PriceCalculator
{
public function calculate(Product $product): float
{
// compute vat
$price = $product->getPrice() * 1.15;
+ // discount for admin
+ if ($this->currentUser->getRole() === 'admin') {
+ $price *= 0.5;
+ }
+
return $price;
}
}
And another 20 % discount for students:
class PriceCalculator
{
public function calculate(Product $product): float
{
// compute vat
$price = $product->getPrice() * 1.15;
// discount for admin
if ($this->currentUser->getRole() === 'admin') {
$price *= 0.5;
}
+ // discount for students
+ if ($this->currentUser->getOccupation() === 'student') {
+ $price *= 0.8;
+ }
+
return $price;
}
}
Our PriceCalculator
grows and grows, our e-commerce platform expands all over Europe and we found out they have a different strategy to calculate price with VAT. How do we solve it?
"Override the whole class and implements calculate()
method for yourself."
class UnitedKingdomPriceCalculator extends PriceCalculator
{
public function calculate(Product $product): float
{
// compute vat
$price = $product->getPrice() * 1.15;
return $price;
}
}
That's an easy solution for the end-user. But it also means zero reusable code that leads to duplicated work. Imagine there will be 20 websites in the UK and each of them will have their own code to calculate price with VAT. 100 % similar code (if written correctly), because it applies to the whole country.
Instead, such UK solution can be one of many, that is openly shared.
No need to write it more than once for all of the e-commerce sites.
class PriceCalculatorCollector
{
/**
* @var PriceCalculatorInterface[]
*/
private $priceCalculators = [];
/**
* @param PriceCalculatorInterface[] $priceCalculators
*/
public function __construct(array $priceCalculators)
{
$this->priceCalculators = $priceCalculators;
}
public function calculate(Product $product): float
{
$price = $product->getPrice();
foreach ($this->priceCalculators as $priceCalculator) {
$price = $priceCalculator->calculate($price);
}
return $price;
}
}
with interface decoupling:
interface PriceCalculatorInterface
{
public function calculate(float $price): float;
}
final class CzechVatPriceCalculator implements PriceCalculatorInterface
{
public function calculate(float $price): float
{
return $price * 1.21;
}
}
final class AdminDiscountPriceCalculator implements PriceCalculatorInterface
{
public function calculate(float $price): float
{
if (! $this->currentUser->getRole() === 'admin') {
return $price;
}
return $price *= 0.5;
}
}
final class UnitedKingdomPriceCalculator implements PriceCalculatorInterface
{
public function calculate(float $price): float
{
return $price * 1.15;
}
}
Based on your needs, collect this or that service.
$priceCalculatorCollector = new PriceCalculatorCollector([
new AdminDiscountPriceCalculator(),
new UnitedKingdomPriceCalculator(),
]);
$price = $priceCalculatorCollector->calculatePrice($product);
✅ single entry point for Collector
✅ each solution that implements PriceCalculatorInterface
is reusable
✅ to extend PriceCalculatorCollector
with another feature, e.g. have a discount for Lenovo laptops from now till the end of June 2018, we don't have to modify it - just register a new PriceCalculator
✅ to reflect 1 change in reality, e.g. from 15 % to 20 % VAT, all we need to do it change 1 class for everyone
Win for the end-user, win for your project and win for the code.
And that's all there is. Just kidding, there is much more, but that's out of the scope of this simple tutorial. Why the collector class doesn't implement the interface and other questions will be answered in following posts.
Happy collecting!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!