Procedural Programming For The Win

Marius Balčytis

Procedural Programming For The Win

Breaking OOP principle
for maintainable applications

Hello, I'm Marius Balčytis

What's OOP?

Definatelly not this

			<?php
			echo date('Y-m-d');
		

Maybe this?

			echo (new DateTime())->format('Y-m-d');
		

Or this?

			class DatePrinter
			{
			    public function printDate()
			    {
			        echo (new DateTime())->format('Y-m-d');
			    }
			}
			(new DatePrinter())->printDate();
		

Probably Some Design Patterns Missing?

			class MyObject {
			    function work($type) {
			        $method = 'work' . $type;
			        $this->$method();
			    }
			    function workA() {
			        echo 'A';
			    }
			}
			(new MyObject())->work('A');
		

Definition

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").

How's Procedural Programming Different?

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.

Objective

			class Square {
			    public $size;
			    function area() {
			        return $this->size ^ 2;
			    }
			}
		

Procedural

			class Square {
			    public $size;
			}
			class AreaCalculator {
			    function getSquareArea(Square $square) {
			        return $square->size ^ 2;
			    }
			}
		

Anemic Domain Model

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

2 basic rules

  1. Logic-free domain objects
  2. No state in service layer

Lets break them!

Logic in Domain Objects

An example - Money

			class Money {
			    private $amount;
			    private $currency;
			        // ...
			}
		
			class Money {
			    // ...
			    public function setAmount($amount) {
			        if (!preg_match('/^\d+\.\d{2}$/', $amount))
			            throw new InvalidAmountException();
			        // ...
			    }
			}
		

So Far So Good... Until

			class Money {
			    // ...
			    public static function fromArray(array $data) {
			        $m = new static();
			        $m->setAmount($data['amount']);
			        $m->setCurrency($data['currency']);
			        return $m;
			    }
			}
		

So Far So Good... Lets open-source it!

@annotations are no logic!
...or are they?

Configuration

			class Money {
			    /** @ArrayField("amount") */
			    private $amount;
			    /** @ArrayField("currency") */
			    private $currency;
			    // ...
			}
		

Code

			class Money {
			    // ...
			    public static function fromArray(array $data) {
			        $m = new static();
			        $m->setAmount($data['amount']);
			        $m->setCurrency($data['currency']);
			        return $m;
			    }
			}
		

What's the difference?

What's the alternative?

Instead of...

			$m = Money::fromArray($_GET);
		

...Lets Do

			/** @var MoneyFactoryInterface $factory */
			$factory = ...; // get from DI(C)
			$m = $factory->fromArray($_GET);
		

Factory

			class MoneyFactory implements MoneyFactoryInterface {
			    // ...
			    public function fromArray(array $data) {
			        $m = new Money($data['amount'],
			                           $data['currency']);
			        $this->validator->validate($m);
			        return $m;
			    }
			}
		

Factory 2

			class MoneyFactory2 implements MoneyFactoryInterface {
			    // ...
			    public function fromArray(array $data) {
			        $m = new Money($data['amount_cents'] / 100,
			                           $data['currency']);
			        $this->validator->validate($m);
			        return $m;
			    }
			}
		

Factory 3

			class MoneyFactory3 implements MoneyFactoryInterface {
			    // ...
			    public function fromArray(array $data) {
			        $m = new Money($data['amount'],
			                           $this->map[$data['currency']]);
			        $this->validator->validate($m);
			        return $m;
			    }
			}
		

Validator

			class MoneyValidator implements MoneyValidatorInterface {
			    // ...
			    public function validate(Money $money) {
			        $regexp = '/^\d+\.\d{2}$/';
			        if (!preg_match($regexp, $money->getAmount()))
			            throw new InvalidAmountException();
			    }
			}
		

Validator 2

			class MoneyValidator2 implements MoneyValidatorInterface {
			    // ...
			    public function validate(Money $money) {
			        $regexp = '/^\-?\d+\.\d{2}$/';
			        if (!preg_match($regexp, $money->getAmount()))
			            throw new InvalidAmountException();
			    }
			}
		

Validator 3

			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();
			    }
			}
		

Even Better

			class MoneyValidator implements MoneyValidatorInterface {
			    public function __construct($negativeAllowed = false) {
			        // ...
			    }
			    public function setAllowedCurrencies($currencies) {
			        // ...
			    }
			    // ...
			}
		

So, from Client's Point of View...

Instead of

			Money
			MoneyWithCents
			MoneyWithNumericCurrency
			CurrencyAwareMoney
			CurrencyAwareMoneyWithCents
			CurrencyAwareMoneyWithNumericCurrency
			PossiblyNegativeMoney
			PossiblyNegativeMoneyWithCents
			PossiblyNegativeMoneyWithNumericCurrency
        

We Have

			Money
			MoneyFactoryInterface
        
			new Service(
			    new CentAwareMoneyFactory(
			        new NegativeAmountMoneyValidator()
			    )
			)
        

What's the Profit?

Lets add state to service layer!

			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!');
        

Lets Add Custom Footer

			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!');
        

Another example

			$mailer->setTo('[email protected]');
			foreach ($users as $user) {
			    $userManager->deleteUser($user);
			    $mailer->send("User {$user->id} deleted");
			}
        

Unit-Test

			$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');
			    }
			}
        

Same as Good Old Globals

			function sendMail($text) {
			    global $toEmail;
			    mail($toEmail, $text);
			}
        
			function myLogic() {
			    global $toEmail;
			    $toEmail = '[email protected]';
			    sendMail('Something happened!');
			}
        

What's the alternative?

			$mailer->send(
			    (new MailMessage())
			        ->setText('User registered')
			        ->setTo('[email protected]')
			        ->setFooter('...')
			);
        
			$mailer->send(
			    (new MailMessage())
			        ->setText('Welcome!')
			        ->setTo('[email protected]')
			);
        

No Way to Change Behaviour from Unexpected Places

			$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
			}
        

Summary

Anemic Domain Model

Furthermore

Keep in Mind

  1. https://en.wikipedia.org/wiki/Object-oriented_programming
  2. https://en.wikipedia.org/wiki/Procedural_programming
  3. http://www.martinfowler.com/bliki/AnemicDomainModel.html
  4. https://en.wikipedia.org/wiki/Anemic_domain_model