Yes you read that right, class functions instead of class methods. This is not a naming mistake.
The OOP way
class Product
{
public function __construct(
public string $name = '',
public string $description = '',
public Price $price = new Price(),
)
{}
public function priceDisplay(ConcatenateString $string = ConcatenateString::SingleSpace): string
{
return $this->price->amount . $string->value . $this->price->currency->value;
}
}
class Price
{
public function __construct(
public float $amount = 0,
public Currency $currency = Currency::EUR,
)
{}
}
enum ConcatenateString: string
{
case SingleSpace = ' ';
case Comma = ',';
}
enum Currency: string
{
case EUR = '€';
case USD = '$';
}
The focus is on displaying the product price.
If I take a very strict view of the single responsibility principle the priceDisplay
method already violates it, because Product
is a data object.
The procedural way
I can create a function to concatenate all strings.
function concatenate(ConcatenateString $string, string ...$values): string
{
return implode($string->value, $values);
}
The main problem is that it requires a lot more typing.
$product = new Product();
// OOP
echo $product->priceDisplay();
// procedural
echo concatenate(ConcatenateString::SingleSpace, $product->price->amount, $product->price->currency->value);
Class functions
I'm not strictly breaking the one file one class OOP rule, so lets add a function to the class file.
// product.php
class Product
{
public function __construct(
public string $name = '',
public string $description = '',
public Price $price = new Price(),
)
{}
}
function productPriceDisplay(Product $product): string
{
return concatenate(ConcatenateString::SingleSpace, $product->price->amount, $product->price->currency->value);
}
This reduces the typing and uses the concatenate
function.
$product = new Product();
echo productPriceDisplay($product);
If I can use the pipe operator, it will even be shorter.
echo new Product() |> productPriceDisplay(...);
The only methods in Product
would do data manipulation, like
class product
{
// ...
public function setUSDPrice(float $amount) : void
{
$this->price = new Price($amount, Currency::USD);
}
}
// in application
$product = new Product();
$product->price = new Price(0, Currency::USD);
$product->setUSDPrice(0);
Conclusion
Class functions will look strange coming from OOP, but the more I think about composition the more it makes sense.
It has been on my mind for a while to give functions a bigger role than just helpers (like concatenate
).
And after seeing a Brandon Rhodes talk I feel that I'm going in the right direction.
The rules I would set for the class functions are:
- They should be prefixed with the class name
- They should have the class instance as the first argument
The two things I'm missing from OOP in class functions are method visibility and method enforcement by contract.
I need a bigger project to experience if those things are going to be missed or not.
Top comments (1)
Interesting. Thanks for sharing!