Marius Balčytis
Breaking OOP principle
for maintainable applications
<?phpecho 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_centsMoney 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) {// ...}// ...}
MoneyMoneyWithCentsMoneyWithNumericCurrencyCurrencyAwareMoneyCurrencyAwareMoneyWithCentsCurrencyAwareMoneyWithNumericCurrencyPossiblyNegativeMoneyPossiblyNegativeMoneyWithCentsPossiblyNegativeMoneyWithNumericCurrency
MoneyMoneyFactoryInterface
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 onepublic 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