The quest for global design principles - PHP Benelux 2016

Preview:

Citation preview

Global Design

Principles

The Quest for

Matthias Noback

Stability

Change

Stability versus change

• Backwards compatibility of APIs

• Semantic versioning

• HTTP status codes

• XML schemas

For internal APIs

only!

The key is in the concept of communication

ReceiverSender

What's being communicated?

Message

Type ("BookHotel")

Value (“Ter Elst", “29-01-2016“, “30-01-2016”)

Message flavours

Command Event

Imperative "BookHotel"

Informational "HotelWasBooked"

Message flavours

Query Document

Interrogatory "GetRecentBookings"

Neutral "BookingsCollection"

Sender

Construct the message

Message

Translate

Construct

(Prepare fortransport)

Receiver

Deconstruct the message

(Unwrap)

Translate

Construct

Global design principles can be

discovered

if we recognise the fact that communication between objects and applications are (more or less) equal

Communication between objects

• Calling a function is like sending a message

• The function and its parameters are the message type

• The arguments constitute the value of the message

class AccountService{

public function deposit($accountId, Money $amount) {

...}

}Opportunity for

sending a message

Inter-object communication

$money = new Money(20000, 'EUR');$userId = 123;

$accountService->deposit($userId, $money

);

Translation

The sender prepares the message for the receiver

Prepare the message

Send the message

Communication between applications

• An HTTP request is a message

• The HTTP method and URI are the type of the message

• The HTTP request body constitutes the value of the message

Or AMQP, Stomp, Gearman, ...

PUT /accounts/deposit/ HTTP/1.1Host: localhost

{"accountId": "123","currency": "EUR","amount": 20000

}

Inter-application communication

Opportunity for sending a message

$uri ='/accounts/deposit/';

$data = json_encode(['accountId' => 123'currency' => 'EUR','amount' => 20000

]);

$httpClient->createRequest('PUT', $uri, $data)->send();

Translation

Prepare the message

Send the message

/** @Route("/accounts/deposit") */function depositAction(Request $request){

$request = json_decode($request->getContent());$money = new Money(

$request['amount'],$request['currency']

);

$this->get('account_service')->deposit($request['accountId'],$money

);

return new Response(null, 200);}

Translation

Prepare the message

Send the message

Global design principles

Should be applicable to both inter-object and inter-application communication

Part IStability

III Implementation

II Information

Sender Receiver

Communication

Dependency relation!

Stable Unstable

What is safer?

Unstable Stable

OR

To be stable"Steady in position or balance;

firm"

Irresponsible

What makes something unstable?

1. Nothing depends on it

It doesn't need to stay the same for anyone

Dependent

What makes something unstable?

2. It depends on many things

Any of the dependencies could change at any time

Stability is not a quality of one thing

It emerges from the environment of that thing

Responsible

What makes something stable?

1. Many things depend on it

It has to stay the same, because change would break dependents

Independent

What makes something stable?

2. It depends on nothing

It is not affected by changes in anything

–The Stable dependencies principle

Depend in the direction of stability

Stable package Unstable package

What to do when this happens?

Stable package

Depend on something that you own

Unstable packageStable package

–The Dependency inversion principle

Always depend on abstractions, not on concretions

Dependency inversion for objects

• Depend on an interface instead of a class

• Separate a task from its implementation

Stable thing

Communication between systems

External application (unstable)

Mediator

Stable thing

Slightly better

External application (unstable)

ConsumerApplication

Communication between systems

Message queue

External application (unstable)

Dependency inversion for systems

• Depend on your own (messaging) system

• Communicate with other applications through a message queue

Part IIInformation

I Stability

III Implementation

Knowledge, data, duplication

Nameplate order system

Nameplate machine

Business automation for creating nameplates

<h1>You are about to order a nameplate!</h1>

<p>Name on the plate: {{ name }}<br/>Width of the plate: {{ 12*(name|length) }} cm.</p>

Calculating the width of a nameplate

Knowledge

class Nameplate{

private $name;

function __construct($name) {$this->name = $name;

}

function widthInCm() {return strlen($this->name) * 12;

}}

Data object

Knowledge is close to the subject

<h1>You are about to order a nameplate!</h1>

<p>Name on the plate: {{ nameplate.name }}<br/>Width of the plate: {{ nameplate.widthInCm) }} cm.</p>

No knowledge in the template

<nameplate><name>Ibuildings</name>

</nameplate>

Serialized Nameplate object

// calculate the width of the nameplate

$nameplate = deserialize($xml);$name = $nameplate['name'];

$width = strlen($name);

$widthPerCharacterInCm = 12;

$widthInCm = $width * $widthPerCharacterInCm;

// configure the nameplate machine ;)

Accepting messages

Duplication of knowledge

<nameplate><name>Ibuildings</name><width>120</width>

</nameplate>

Deduplication

Knowledge is in one place, facts can be everywhere

$nameplate = deserialize($xml);

$width = $nameplate['width'];

Accepting messages

No duplicate knowledge anymore

–The Don't repeat yourself principle

“Every piece of knowledge must have a single, unambiguous, authoritative representation

within a system.”

Even across applications!

"Don't repeat yourself"

• Doesn't mean you can't repeat data

• It means you can't have knowledge in multiple locations

Mutability

What's the difference between...

class Money{

private $amount;private $currency;

public function setAmount($amount) {$this->amount = $amount;

}

public function getAmount() {return $this->amount;

}

...}

... and this

class Money{

public $amount;public $currency;

}

Inconsistent data$savings = new Money();$savings->setAmount(1000);

// what's the currency at this point?

$savings->setCurrency('USD');

// only now do we have consistent data

$savings->setCurrency('EUR');

// we have a lot more money now!

$savings->setAmount('Amsterdam');

Making something better of this

class Money{

private $amount;private $currency;

public function __construct($amount, $currency) {$this->setAmount($amount);

}

private function setAmount($amount) {if (!is_int($amount) || $amount < 0) {

throw new \InvalidArgumentException();}

$this->amount = $amount;}

}

Private

Required

Immutability

• Once created, can not be modified

• Can only be replaced

Consistent data!

$savings = new Money(1000, 'USD');// we already have consistent data

// we can't change anything anymore

Immutable data!

Using immutable values

• Prevents bugs

• Prevents invalid state

What about API messages?

<money><amount>1000</amount><currency>USD</currency>

</money>

PUT /savings/<money>

<currency>EUR</currency></money>

POST /savings/

Large object graphs<user>

<first-name/><last-name/> <mobile-phone-number/> <email-address/> <homepage/> <orders>

<order/>...

</orders><tickets>

<ticket/>...

</tickets><payments>

<payment/>...

</payments></user>

Forms & Doctrine ORM

$form = $this->createForm(new MoneyType());$form->handleRequest($request);

if ($form->isValid()) {$em = $this->getDoctrine()->getManager();$em->persist($form->getData());

// calculates change set and executes queries

$em->flush();}

If you allow your users to change every field at any time

• You end up with inconsistent data

• You loose the why of a change

• You end up with the what of only the last change

• You ignore the underlying real-world scenario

Commands and events

Register ConfirmRegistration

SendMessage

RegistrationConfirmed

MessageSent

UserRegisteredApplication

True

• Messages should serve actual use cases instead of patch operations

• After processing them, data should be in a valid state

Part IIIImplementation

I StabilityII Information

IV Conclusion

Implementation

Leaking implementation details

class Person{

/** * @return PhoneNumber[] */public function getPhoneNumbers() {

return $this->phoneNumbers;}

}

Initial implementation

class Person{

/** * @return ArrayCollection */public function getPhoneNumbers() {

return $this->phoneNumbers;}

}

Implementation leakage (Doctrine)

class Person{

/** * @return PhoneNumber[] */public function getPhoneNumbers() {

return $this->phoneNumbers->toArray();}

}

Hiding implementation

class NameplateController{

function getAction($id) {$nameplate = $this

->getDoctrine()->getManager()->getRepository(Nameplate::class)->findOneBy(['id' => $id]);

if ($nameplate === null) {throw new NotFoundHttpException();

}...

}}

More implementation hiding

Actual field names!

null or false?

"find"?

class NameplateRepository{

function byId($id) {$nameplate = $this

->findOneBy(['id' => $id]);

if ($nameplate === null) {throw new NameplateNotFound($id);

}

return $nameplate;}

}

Push it out of sight

Domain-specific exception

Hide specific return value

No "find"

class NameplateController{

function getAction($id) {try {

$nameplate = $this->nameplateRepository->byId($id);

} catch (NameplateNotFound $exception) {throw new NotFoundHttpException();

}

...}

}

Respect layers

Convert domain exception to web specific exception

Basically just plain old OOP

• Encapsulation

• Abstraction

Implementation details and usability

<ticket> ... <priority type="integer">1</priority> ...

Assembla API

<ticket> ... <priority key="highest"> <label>Highest</label> </priority> ...

Why not...

<ticket> ... <is-story type="boolean">false</is-story> <total-estimate type="float">0.0</total-estimate> ...

Assembla API

<story> ... <total-estimate type="float">0.0</total-estimate> ...

Why not...

Design your messages in such a way that

• You hide your implementation

• Clients won't need to reimplement your logic

• Clients get the information they need

API discovery

/** * @param string $password * @param integer $algo * @param array $options */function password_hash($password, $algo, array $options = array());

Undiscoverable API

What are my options here?

And here?

class Algorithm extends \SplEnum{

const __default = self::BCRYPT;

const BCRYPT = 1;}

Allow no mistakes

['salt' => '...''cost' => 10

]

Options for bcrypt hashing

class Bcrypt implements HashingStrategy{

/** * @param integer $cost * @param string|null $salt */

public function __construct($cost, $salt = null) {...

}}

Inject a strategy

function password_hash($password, HashingStrategy $hashingStrategy

);

Inject a strategy

More explicit and... discoverable!

Discoverability of an API

• Full Reflection capabilities :)

• Basic knowledge of English

// I've got this password I want to hash…$password = ...;

// Look, I found a function for this: password_hash()password_hash($password, HashingStrategy $hashingStrategy);

// It requires an argument: a password (string)// I already got a password right here:password_hash($password);

// Wait, it requires a hashing strategy (a HashingStrategy object)// I just found a class implementing that interface:$hashingStrategy = new BcryptStrategy();

// That doesn't work, BcryptStrategy needs a cost$hashingStrategy = new BcryptStrategy(10);password_hash($password, $hashingStrategy);

Example of API discoveryWho is talking?

How stupid are they?

How do you find out a valid range?

/** * @param array $options */function some_function(array $options);

/** * @param integer $type */function some_other_function($type);

/** * @param object $command */function handle($command);

Undiscoverable APIs

Any kind of API should be maximally discoverable

Everything should be an object

A class is a type

Define lots of interfaces

An interface defines the public API of functions

<?xml version="1.0" encoding="UTF-8"?><ticket> <reporter> <id>43</id> <link rel="self" href="/api/reporters/43" /> <link rel="index" href="/api/reporters/" /> </reporter> ...

HATEOAS

Links are a way to explain an "object"

Summary/Hypothesis

• Objects are just like applications

• Try to apply the same rules to them

Think about

• Stable dependencies

• Duplication of facts, not knowledge

• Immutability over mutability

• No leakage of implementation details

• Everything should be maximally discoverable

–Chris Hadfield

“… I should do things that keep me moving in the right direction, just in case — and I should be sure

those things interest me, so whatever happens, I’m happy.”

Questions? Feedback?

joind.in/talk/f3b34 Thanks!