Hexagonal architecture - message-oriented software design (PHP Barcelona 2015)

77

Transcript of Hexagonal architecture - message-oriented software design (PHP Barcelona 2015)

HEXAGONAL ARCHITECTURE Message oriented software design

By Matthias Noback

ARCHITECTUREWhat's the problem?

Nice app

Sad app

Your brain can't handle it

M V C ?

Coupling to frameworks and libraries

How do you start a new project?

Pick a framework

Install a skeleton project

Remove demo stuff

Auto-generate entities

Auto-generate CRUD controllers

Done

"It's a Symfony project!"

That's actually outside inThe boring stuff

The interesting stuff

Symfony

Doctrine

RabbitMQ

Redis

Angular

Slow tests

DB

Browser

Message queue

Key-value

Filesystem

Why do frameworks not solve this for us?

Because they can't ;)

Frameworks are about encapsulation

Low-level API

$requestContent = file_get_contents('php://input'); $contentType = $_SERVER['CONTENT_TYPE']; if ($contentType === 'application/json') { $data = json_decode($requestContent, true); } elseif ($contentType === 'application/xml') { $xml = simplexml_load_string($requestContent); ... }

Nicely hides the details

$data = $serializer->deserialize( $request->getContent(), $request->getContentType() );

Low-level API

$stmt = $db->prepare( 'SELECT * FROM Patient p WHERE p.anonymous = ?' ); $stmt->bindValue(1, true); $stmt->execute(); $result = $stmt->fetch(\PDO::FETCH_ASSOC); $patient = Patient::reconstituteFromArray($result);

Hides a lot of details

$patient = $repository->createQueryBuilder('p') ->where('p.anonymous = true') ->getQuery() ->getResult();

What about abstraction?

$patient = $repository->createQueryBuilder('p') ->where('p.anonymous = true') ->getQuery() ->getResult();

Concrete

Concrete

Concrete

$patients = $repository->anonymousPatients();

Abstract

Nice

DIY

Coupling to the delivery mechanism

public function registerPatientAction(Request $request) { $patient = new Patient();

$form = $this->createForm(new RegisterPatientForm(), $patient);

$form->handleRequest($request);

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

return $this->redirect($this->generateUrl('patient_list')); }

return array( 'form' => $form->createView() ); }

Request and Form are web-specific

EntityManager is ORM, i.e. relational DB-specific

Reusability: impossible

Some functionality

The web

The CLI

Some functionality

Run it

Lack of intention-revealing code

data

data

data

public function updateAction(Request $request) { $patient = new Patient();

$form = $this->createForm(new PatientType(), $patient);

$form->handleRequest($request);

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

return $this->redirect($this->generateUrl('patient_list')); }

return array( 'form' => $form->createView() ); }

from the HTTP request

copied into an entity

then stored in the database

What exactly changed?!

And... why?

R.A.D.

Rapid Application Development

B.A.D.

B.A.D. Application Development

In summary

Coupling to a framework

Coupling to a delivery mechanism (e.g. the web)

Slow tests

Lack of intention in the code

THE ESSENCEof your application

The essence

Other things

The "heart"?

"The heart of software is its ability to solve domain-related problems for its users.

–Eric Evans, Domain Driven Design

All other features, vital though they may be, support this basic purpose."

What's essential?

Domain model

Interaction with it

Use cases

What's not essential?

“The database is an implementation detail”

–Cool software architect

The core doesn't need to know about it

What about interaction?

The core doesn't need to know about it

Infrastructure

The world outside

Web browser

TerminalDatabase

Messaging

Filesystem(E)mail

Mmm... layers Layers allow you to separate stuff

Layers allow you to

allocate things

Layers have boundaries

Rules for crossing them

Rules about communication

Actually: rules about dependencies

The dependency rule

–Robert Martin, Screaming Architecture

What crosses layer boundaries?

Message

MessagessomeFunctionCall( $arguments, $prepared, $for, $the, $receiver );

$message = new TypeOfMessage( $some, $relevant, $arguments ); handle($message);

What about the application boundary?

The app

Messag

e

The world outside

How does an app allow incoming messages at all?

By exposing input ports

Routes Console commands

A WSDL file for a SOAP API

Ports use protocols for communication

Each port has a language of its own

Web (HTTP)

Messaging (AMQP)

HTTP Request

Form

Request

Controller

Entity

Value object

Web p

ort

Tran

slate

the re

quest

Repository

Adapters

The translators are called: adapters

An example

Plain HTTP message

$_POST, $_GET,

$_SERVER, Request

POST /patients/ HTTP/1.1 Host: hospital.com

name=Matthias&[email protected]

Command

$command = new RegisterPatient( $request->get('name'), $request->get('email') );

Command

$command = new RegisterPatient( $request->get('name'), $request->get('email') );

Expresses intention

Implies changeIndependent of delivery mechanism

Only the message

class RegisterPatientHandler { public function handle(RegisterPatient $command) { $patient = Patient::register( $command->name(), $command->email() ); $this->patientRepository->add($patient); } }

CommandCommand handler

CommandCommand handler A

Command bus

Command handler B

Command handler C

HTTP Request

Form

Request

Controller

Patient (entity)

Web p

ort

PatientRepository

RegisterPatient- Handler

RegisterPatient (command)

Infras

truc

ture

Application

Domain

Change

New entity (Patient)

Entity-Manager UnitOf-Work

$patient = Patient::register( $command->name(), $command->email() ); $this->patientRepository ->add($patient);

Insert query (SQL)

INSERT INTO patients SET name='Matthias', email='[email protected]';

SQL query

EntityManager

UnitOfWork

QueryBuilder

Persi

stenc

e port

Prepare

for p

ersist

ence

PatientRepository

Core

Infrastruc

ture

Messaging (AMQP)

Persistence (MySQL)

What often goes wrong: we violate boundary rules...

EntityManager

UnitOfWork

QueryBuilder

PatientRepository

Core

Infrastruc

ture

RegisterPatient- Handler

Domain

Infrastr

ucture

Applicatio

n

Domain

PatientRepository (uses MySQL)

EntityManager

UnitOfWork

QueryBuilder

PatientRepository (uses MySQL)

Domain

Infrastr

ucture

Applicatio

n

Domain

RegisterPatient- Handler

Domain

PatientRepository (interface) Dependency

inversion

PatientRepository (uses MySQL)

RegisterPatient- Handler

Domain

InMemory- PatientRepository

Speedy alternative

RegisterPatient- Handler

PatientRepository (uses MySQL)

PatientRepository (interface)

"A good software architecture allows decisions [...] to be deferred and delayed."

–Robert Martin, Screaming Architecture

IN CONCLUSIONwhat did we get from all of this?

Separation of concerns

Core

Infrastructure

CommandCommand

Command handler

Command handler

Stand-alone use cases

Command

Command handler

Intention-revealing

Reusable

Infrastructure stand-ins

Regular implementation

Interface

Stand-in, fast implementation

This is all very much supportive of...

See also: Modelling by Example

DDD

TDD

BDD

CQRS

QUESTIONS?joind.in/15863FEEDBACK?