Marius Balčytis
Breaking OOP principle
for maintainable applications
<?php
echo date('Y-m-d');
echo (new DateTime())->format('Y-m-d');
class DatePrinter
{
public function printDate()
{
echo (new DateTime())->format('Y-m-d');
}
}
(new DatePrinter())->printDate();
class MyObject {
function work($type) {
$method = 'work' . $type;
$this->$method();
}
function workA() {
echo 'A';
}
}
(new MyObject())->work('A');
OOP is a programming paradigm based on the concept of "objects", which may contain data and code.
A feature of objects is that an object's procedures can access and often modify the data fields of the object with which they are associated (objects have a notion of "this" or "self").
The most important distinction is that while procedural programming uses procedures to operate on data structures, object-oriented programming bundles the two together, so an "object", which is an instance of a class, operates on its "own" data structure.
class Square {
public $size;
function area() {
return $this->size ^ 2;
}
}
class Square {
public $size;
}
class AreaCalculator {
function getSquareArea(Square $square) {
return $square->size ^ 2;
}
}
First described by Martin Fowler in 2003 as an anti-pattern
Business logic is implemented in separate classes which transform the state of the domain objects
class Money {
private $amount;
private $currency;
// ...
}
class Money {
// ...
public function setAmount($amount) {
if (!preg_match('/^\d+\.\d{2}$/', $amount))
throw new InvalidAmountException();
// ...
}
}
$m->setAmount($a)->setCurrency($c);
\-?
in beginningclass Money {
// ...
public static function fromArray(array $data) {
$m = new static();
$m->setAmount($data['amount']);
$m->setCurrency($data['currency']);
return $m;
}
}
amount_cents
Money
class
toArray
will work?class Money {
/** @ArrayField("amount") */
private $amount;
/** @ArrayField("currency") */
private $currency;
// ...
}
class Money {
// ...
public static function fromArray(array $data) {
$m = new static();
$m->setAmount($data['amount']);
$m->setCurrency($data['currency']);
return $m;
}
}
$m = Money::fromArray($_GET);
/** @var MoneyFactoryInterface $factory */
$factory = ...; // get from DI(C)
$m = $factory->fromArray($_GET);
class MoneyFactory implements MoneyFactoryInterface {
// ...
public function fromArray(array $data) {
$m = new Money($data['amount'],
$data['currency']);
$this->validator->validate($m);
return $m;
}
}
class MoneyFactory2 implements MoneyFactoryInterface {
// ...
public function fromArray(array $data) {
$m = new Money($data['amount_cents'] / 100,
$data['currency']);
$this->validator->validate($m);
return $m;
}
}
class MoneyFactory3 implements MoneyFactoryInterface {
// ...
public function fromArray(array $data) {
$m = new Money($data['amount'],
$this->map[$data['currency']]);
$this->validator->validate($m);
return $m;
}
}
class MoneyValidator implements MoneyValidatorInterface {
// ...
public function validate(Money $money) {
$regexp = '/^\d+\.\d{2}$/';
if (!preg_match($regexp, $money->getAmount()))
throw new InvalidAmountException();
}
}
class MoneyValidator2 implements MoneyValidatorInterface {
// ...
public function validate(Money $money) {
$regexp = '/^\-?\d+\.\d{2}$/';
if (!preg_match($regexp, $money->getAmount()))
throw new InvalidAmountException();
}
}
class MoneyValidator3 implements MoneyValidatorInterface {
// ...
public function validate(Money $money) {
$p = $this->positions[$money->getCurrency()];
$regexp = '/^\d+\.\d{' . $p . '}$/';
if (!preg_match($regexp, $money->getAmount()))
throw new InvalidAmountException();
}
}
class MoneyValidator implements MoneyValidatorInterface {
public function __construct($negativeAllowed = false) {
// ...
}
public function setAllowedCurrencies($currencies) {
// ...
}
// ...
}
Money
MoneyWithCents
MoneyWithNumericCurrency
CurrencyAwareMoney
CurrencyAwareMoneyWithCents
CurrencyAwareMoneyWithNumericCurrency
PossiblyNegativeMoney
PossiblyNegativeMoneyWithCents
PossiblyNegativeMoneyWithNumericCurrency
Money
MoneyFactoryInterface
new Service(
new CentAwareMoneyFactory(
new NegativeAmountMoneyValidator()
)
)
Service
in different situationsamount_cents
if not needed)class Mailer {
private $toEmail;
public function setTo($toEmail) { ... }
public function send($text) {
mail($this->toEmail, $text);
}
}
$mailer->setTo('[email protected]');
$mailer->send('User registered');
Someplace later in code
$mailer->setTo('[email protected]');
$mailer->send('Welcome!');
class Mailer {
private $toEmail;
private $footer = 'See you soon!'; // default one
public function setTo($toEmail) { ... }
public function setFooter($footer) { ... }
// ...
}
$mailer->setTo('[email protected]');
$mailer->setFooter('<a href=".../admin">Manage users</a>');
$mailer->send('User registered');
Someplace later in code
$mailer->setTo('[email protected]');
// default footer is ok... or is it?
$mailer->send('Welcome!');
$mailer->setTo('[email protected]');
foreach ($users as $user) {
$userManager->deleteUser($user);
$mailer->send("User {$user->id} deleted");
}
$userManager = $this->getMock('...')
->expect($this->exactly(count($users))
->method('deleteUser');
// ...
OK (56 tests, 110 assertions)
Or is it really?
class UserManager {
// ...
public function deleteUser($user) {
//...
$this->mailer->setTo($user->email);
$this->mailer->send('Your account has been deleted');
}
}
function sendMail($text) {
global $toEmail;
mail($toEmail, $text);
}
function myLogic() {
global $toEmail;
$toEmail = '[email protected]';
sendMail('Something happened!');
}
$mailer->send(
(new MailMessage())
->setText('User registered')
->setTo('[email protected]')
->setFooter('...')
);
$mailer->send(
(new MailMessage())
->setText('Welcome!')
->setTo('[email protected]')
);
$message = (new MailMessage())->setTo('[email protected]');
foreach ($users as $user) {
$userManager->deleteUser($user);
$message->setText("User {$user->id} deleted");
$mailer->send($message);
// we're also hiring
}
class
keyword