104
Be pragmatic, be SOLID

Be pragmatic, be SOLID

Embed Size (px)

Citation preview

Page 1: Be pragmatic, be SOLID

Be pragmatic,be SOLID

Page 2: Be pragmatic, be SOLID

Krzysztof Menżyk

practises TDDbelieves that software is a craft

loves domain modellingobsessed with brewing

plays squash

kmenzyk [email protected]

Page 3: Be pragmatic, be SOLID

Do you consider yourselfa professional software developer?

Page 4: Be pragmatic, be SOLID

New client

Greenfield project

Starting from scratch

Page 5: Be pragmatic, be SOLID
Page 6: Be pragmatic, be SOLID
Page 7: Be pragmatic, be SOLID

What went wrong?

Page 8: Be pragmatic, be SOLID

The code started to rot

Page 9: Be pragmatic, be SOLID

The design is hard to change

Rigidity

Page 10: Be pragmatic, be SOLID

The design is easy to break

Fragility

Page 11: Be pragmatic, be SOLID

Immobility

The design is hard to reuse

Page 12: Be pragmatic, be SOLID

It is easy to do the wrong thing, but hard to do the right thing

Viscosity

Page 13: Be pragmatic, be SOLID

Your software is bound to change

Page 14: Be pragmatic, be SOLID

Design stamina hypothesis

time

cumulativefunctionality

design payoff lineno design

good design

by Martin Fowler

Page 15: Be pragmatic, be SOLID
Page 16: Be pragmatic, be SOLID

What is Object Oriented Design

then?

Page 17: Be pragmatic, be SOLID

Design Principles andDesign Patterns

Robert C. Martin

Page 18: Be pragmatic, be SOLID

Single Responsibility

Open Closed

Liskov Substitution

Interface Segregation

Dependency Inversion

Page 19: Be pragmatic, be SOLID

SingleResponsibilityPrinciple

A class should have only one reason to change

Page 20: Be pragmatic, be SOLID

Gather together those things that change for the same reason

Separate those things that change for different reasons

Page 21: Be pragmatic, be SOLID

class Employee{ public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }

public function asJson() { // ... }

public function save() { // ... }

public function delete() { // ... }}

Page 22: Be pragmatic, be SOLID

Try to describe what the class does

Page 23: Be pragmatic, be SOLID

class Employee{ public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }

public function asJson() { // ... }

public function save() { // ... }

public function delete() { // ... }}

Page 24: Be pragmatic, be SOLID

class Employee{ public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }

public function asJson() { // ... }

public function save() { // ... }

public function delete() { // ... }}

Page 25: Be pragmatic, be SOLID

class Employee{ public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }

public function asJson() { // ... }

public function save() { // ... }

public function delete() { // ... }}

Page 26: Be pragmatic, be SOLID
Page 27: Be pragmatic, be SOLID

class Employee{ public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }

public function asJson() { // ... }

public function save() { // ... }

public function delete() { // ... }} violation

Page 28: Be pragmatic, be SOLID

class Employee { public static function hire($name, $forPosition, Money $withSalary) { // ... }

public function promote($toNewPosition, Money $withNewSalary) { // ... }}

class EmployeeSerializer{ public function toJson(Employee $employee) { // ... }}

class EmployeeRepository { public function save(Employee $employee) { // ... }

public function delete(Employee $employee) { // ... }}

the right

way

Page 29: Be pragmatic, be SOLID

What about applying SRP to class methods?

Page 30: Be pragmatic, be SOLID

What about applying SRP to test methods?

Page 31: Be pragmatic, be SOLID

/** @test */public function test_employee(){ $employee = Employee::hire('John Doe', 'Junior Developer', $this->fiveHundredEuros);

$this->assertEquals($this->fiveHundredEuros, $employee->getSalary());

$employee->promote('Senior Developer', $this->sixHundredEuros);

$this->assertEquals($this->sixHundredEuros, $employee->getSalary());

$employee->promote('Technical Leader', $this->fiveHundredEuros);

$this->assertEquals($this->sixHundredEuros, $employee->getSalary());}

Page 32: Be pragmatic, be SOLID

/** @test */public function it_hires_with_salary(){ $employee = Employee::hire('John Doe', 'Junior Developer', $this->fiveHundredEuros);

$this->assertEquals($this->fiveHundredEuros, $employee->getSalary());}

/** @test */public function it_promotes_with_new_salary(){ $employee = Employee::hire('John Doe', 'Junior Developer', $this->fiveHundredEuros); $employee->promote('Senior Developer', $this->sixHundredEuros);

$this->assertEquals($this->sixHundredEuros, $employee->getSalary());}

/** @test */public function it_does_not_promote_if_new_salary_is_not_bumped(){ $employee = Employee::hire('John Doe', 'Senior Developer', $this->sixHundredEuros); $employee->promote('Technical Leader', $this->fiveHundredEuros);

$this->assertEquals($this->sixHundredEuros, $employee->getSalary());}

the right

way

Page 33: Be pragmatic, be SOLID

One test covers one behaviour

Page 34: Be pragmatic, be SOLID

OpenClosedPrinciple

Software entities should be open for extension, but closed for modification

Page 35: Be pragmatic, be SOLID

Write once, change never!

Page 36: Be pragmatic, be SOLID

Wait! What?

Page 37: Be pragmatic, be SOLID

class Shortener{ public function shorten(Url $longUrl) { if (!$this->hasHttpScheme($longUrl)) { throw new InvalidUrl('Url has no "http" scheme'); }

// do stuff to shorten valid url

return $shortenedUrl; }

private function hasHttpScheme(Url $longUrl) { // ... }}

/** @test */public function it_does_not_shorten_url_without_http(){ $urlToFtp = // ...

$this->setExpectedException(InvalidUrl::class, 'Url has no "http" scheme');

$this->shortener->shorten($urlToFtp);}

Page 38: Be pragmatic, be SOLID

class Shortener{ public function shorten(Url $longUrl) { if (!$this->hasHttpScheme($longUrl)) { throw new InvalidUrl('Url has no "http" scheme'); }

if (!$this->hasPlDomain($longUrl)) { throw new InvalidUrl('Url has no .pl domain'); }

// do stuff to shorten valid url

return $shortenedUrl; }

private function hasHttpScheme(Url $longUrl) { // ... }

private function hasPlDomain(Url $longUrl) { // ... }}

Page 39: Be pragmatic, be SOLID

/** @test */public function it_shortens_only_urls_with_pl_domains(){ $urlWithEuDomain = // ...

$this->setExpectedException(InvalidUrl::class, 'Url has no .pl domain');

$this->shortener->shorten($urlWithEuDomain);}

Page 40: Be pragmatic, be SOLID

/** @test */public function it_shortens_only_urls_with_pl_domains(){ $urlWithEuDomainButWithHttpScheme = // ...

$this->setExpectedException(InvalidUrl::class, 'Url has no .pl domain');

$this->shortener->shorten($urlWithEuDomainButWithHttpScheme);}

Page 41: Be pragmatic, be SOLID

/** @test */public function it_shortens_urls(){ $validUrl = // make sure the url satisfies all "ifs"

$shortenedUrl = $this->shortener->shorten($validUrl);

// assert}

Page 42: Be pragmatic, be SOLID

Testing seems hard?

Page 43: Be pragmatic, be SOLID

class Shortener{ public function shorten(Url $longUrl) { if (!$this->hasHttpScheme($longUrl)) { throw new InvalidUrl('Url has no "http" scheme'); }

if (!$this->hasPlDomain($longUrl)) { throw new InvalidUrl('Url has no .pl domain'); }

// do stuff to shorten valid url

return $shortenedUrl; }

private function hasHttpScheme(Url $longUrl) { // ... }

private function hasPlDomain(Url $longUrl) { // ... }} violation

Page 44: Be pragmatic, be SOLID

Abstraction is the key

Page 45: Be pragmatic, be SOLID

interface Rule{ /** * @param Url $url * * @return bool */ public function isSatisfiedBy(Url $url);}

Page 46: Be pragmatic, be SOLID

class Shortener{ public function addRule(Rule $rule) { // ... }

public function shorten(Url $longUrl) { if (!$this->satisfiesAllRules($longUrl)) { throw new InvalidUrl(); }

// do stuff to shorten valid url

return $shortenedUrl; }

private function satisfiesAllRules(Url $longUrl) { // ... }}

the right

way

Page 47: Be pragmatic, be SOLID

class HasHttp implements Rule{ public function isSatisfiedBy(Url $url) { // ... }}

class HasPlDomain implements Rule{ public function isSatisfiedBy(Url $url) { // ... }}

Page 48: Be pragmatic, be SOLID

”There is a deep synergy between testability and good design”

– Michael Feathers

Page 49: Be pragmatic, be SOLID

LiskovSubstitutionPrinciple

Subtypes must be substitutable for their base types

Page 50: Be pragmatic, be SOLID

class Tweets{ protected $tweets = [];

public function add(Tweet $tweet) { $this->tweets[$tweet->id()] = $tweet; }

public function get($tweetId) { if (!isset($this->tweets[$tweetId])) { throw new TweetDoesNotExist(); }

return $this->tweets[$tweetId]; }}

Page 51: Be pragmatic, be SOLID

class BoundedTweets extends Tweets{ const MAX = 10;

public function add(Tweet $tweet) { if (count($this->tweets) > self::MAX) { throw new \OverflowException(); }

parent::add($tweet); }}

Page 52: Be pragmatic, be SOLID

function letsTweet(Tweets $tweets){ // ... // $tweets has already 10 tweets

$tweets->add(new Tweet('Ooooops'));}

Page 53: Be pragmatic, be SOLID

function letsTweet(Tweets $tweets){ // ...

try { $tweets->add(new Tweet('Ooooops')); } catch (\OverflowException $e) { // What to do? }}

violation

Page 54: Be pragmatic, be SOLID

Design by contract

Page 55: Be pragmatic, be SOLID

Design by contract(because public API is not enough)

Page 56: Be pragmatic, be SOLID

What does it expect?

What does it guarantee?

What does it maintain?

Page 57: Be pragmatic, be SOLID

class Tweets{ public function add(Tweet $tweet) { // ... }

/** * @param $tweetId * * @return Tweet * * @throws TweetDoesNotExist If a tweet with the given id * has not been added yet */ public function get($tweetId) { // ... }}

Page 58: Be pragmatic, be SOLID

interface Tweets{ public function add(Tweet $tweet);

/** * @param $tweetId * * @return Tweet * * @throws TweetDoesNotExist If a tweet with the given id * has not been added yet */ public function get($tweetId);}

class InMemoryTweets implements Tweets{ // ...}

Page 59: Be pragmatic, be SOLID

public function retweet($tweetId){ try { $tweet = $this->tweets->get($tweetId); } catch (TweetDoesNotExist $e) { return; }

// ...}

Page 60: Be pragmatic, be SOLID

class DoctrineORMTweets extends EntityRepository implements Tweets{ public function add(Tweet $tweet) { $this->_em->persist($tweet); $this->_em->flush(); }

public function get($tweetId) { return $this->find($tweetId); }}

Page 61: Be pragmatic, be SOLID

class EntityRepository implements ObjectRepository, Selectable{ /** * Finds an entity by its primary key / identifier. * * @param mixed $id The identifier. * @param int $lockMode The lock mode. * @param int|null $lockVersion The lock version. * * @return object|null The entity instance * or NULL if the entity can not be found. */ public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) { // ... }

// ...}

Page 62: Be pragmatic, be SOLID

function retweet($tweetId){ if ($this->tweets instanceof DoctrineORMTweets) { $tweet = $this->tweets->get($tweetId);

if (null === $tweet) { return; } } else { try { $tweet = $this->tweets->get($tweetId); } catch (TweetDoesNotExist $e) { return; } }

// ...} violation

Page 63: Be pragmatic, be SOLID

Violations of LSP arelatent violations of OCP

Page 64: Be pragmatic, be SOLID

class DoctrineORMTweets extends EntityRepository implements Tweets{ // ...

public function get($tweetId) { $tweet = $this->find($tweetId);

if (null === $tweet) { throw new TweetDoesNotExist(); }

return $tweet; }}

the right

way

Page 65: Be pragmatic, be SOLID

LSP violations are difficult to detect until it is too late

Page 66: Be pragmatic, be SOLID

Ensure the expected behaviour of your base class is preserved in

derived classes

Page 67: Be pragmatic, be SOLID

DependencyInversionPrinciple

High-level modules should not depend on low-level modules, both should depend on abstractions

Abstractions should not depend on details. Details should depend on abstractions

Page 68: Be pragmatic, be SOLID

class PayForOrder { public function __construct(PayPalApi $payPalApi) { $this->payPalApi = $payPalApi; }

public function function pay(Order $order) { // ...

$token = $this->payPalApi->createMethodToken($order->creditCard()); $this->payPalApi->createTransaction($token, $order->amount());

// ... }}

Page 69: Be pragmatic, be SOLID

PayForOrder

PayPalApi

Business Layer

Integration Layer

Page 70: Be pragmatic, be SOLID

High-level module

Low-level module

PayForOrder

PayPalApi

Page 71: Be pragmatic, be SOLID

Abstraction is the key

Page 72: Be pragmatic, be SOLID

PaymentProvider

PayPalApi

PayForOrder

Page 73: Be pragmatic, be SOLID

class PayForOrder{ public function __construct(PaymentProvider $paymentProvider) { $this->paymentProvider = $paymentProvider; }

public function function pay(Order $order) { // ...

$token = $this->paymentProvider->createMethodToken($order->creditCard()); $this->paymentProvider->createTransaction($token, $order->amount());

// ... }}

Page 74: Be pragmatic, be SOLID

class PayForOrder{ public function __construct(PaymentProvider $paymentProvider) { $this->paymentProvider = $paymentProvider; }

public function function pay(Order $order) { // ...

$token = $this->paymentProvider->createMethodToken($order->creditCard()); $this->paymentProvider->createTransaction($token, $order->amount());

// ... }}

Abstractions should not depend on details. Details should depend on abstractions

Page 75: Be pragmatic, be SOLID

Define the interface from the usage point of view

Page 76: Be pragmatic, be SOLID

class PayForOrder{ public function __construct(PaymentProvider $paymentProvider) { $this->paymentProvider = $paymentProvider; }

public function function pay(Order $order) { // ...

$token = $this->paymentProvider->charge( $order->creditCard(), $order->amount() );

// ... }} the right

way

Page 77: Be pragmatic, be SOLID

Concrete things change alot

Abstract things change muchless frequently

Page 78: Be pragmatic, be SOLID

PaymentProvider

PayPalProvider

PayForOrder

PayPalApi

3rd party

Page 79: Be pragmatic, be SOLID

class PayPalProvider extends PayPalApi implements PaymentProvider{ public function charge(CreditCard $creditCard, Money $forAmount) { $token = $this->createMethodToken($creditCard); $this->createTransaction($token, $forAmount); }}

Page 80: Be pragmatic, be SOLID

PaymentProvider

PayPalProvider

PayForOrder

PayPalApi

3rd party

Page 81: Be pragmatic, be SOLID

class PayPalProvider implements PaymentProvider{ public function __construct(PayPalApi $payPal) { $this->payPal = $payPal; }

public function charge(CreditCard $creditCard, Money $forAmount) { $token = $this->payPal->createMethodToken($creditCard); $this->payPal->createTransaction($token, $forAmount); }}

Page 82: Be pragmatic, be SOLID

class TestPaymentProvider implements PaymentProvider { //... }

Page 83: Be pragmatic, be SOLID

InterfaceSegregationPrinciple

Clients should not be forced to depend on methods they do not use

Page 84: Be pragmatic, be SOLID

Many client specific interfaces are better than one general purpose

interface

Page 85: Be pragmatic, be SOLID

interface EventDispatcherInterface{ public function dispatch($eventName, Event $event = null);

public function addListener($eventName, $listener, $priority = 0);

public function removeListener($eventName, $listener);

public function addSubscriber(EventSubscriberInterface $subscriber);

public function removeSubscriber(EventSubscriberInterface $subscriber);

public function getListeners($eventName = null);

public function hasListeners($eventName = null);}

Page 86: Be pragmatic, be SOLID

class HttpKernel implements HttpKernelInterface, TerminableInterface{ public function terminate(Request $request, Response $response) { $this->dispatcher->dispatch( KernelEvents::TERMINATE, new PostResponseEvent($this, $request, $response) ); }

private function handleRaw(Request $request, $type = self::MASTER_REQUEST) { // ... $this->dispatcher->dispatch(KernelEvents::REQUEST, $event);

// ...

$this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);

// ... }

private function filterResponse(Response $response, Request $request, $type) { // ... $this->dispatcher->dispatch(KernelEvents::RESPONSE, $event); // ... }

// ...}

Page 87: Be pragmatic, be SOLID

class ImmutableEventDispatcher implements EventDispatcherInterface{ public function dispatch($eventName, Event $event = null) { return $this->dispatcher->dispatch($eventName, $event); }

public function addListener($eventName, $listener, $priority = 0) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); }

public function removeListener($eventName, $listener) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); }

public function addSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); }

public function removeSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); }

// ...}

violation

Page 88: Be pragmatic, be SOLID

It serves too many different types of clients

Page 89: Be pragmatic, be SOLID

Design interfaces from clients point of view

Page 90: Be pragmatic, be SOLID

interface EventDispatcherInterface{ public function dispatch($eventName, Event $event = null);}

interface EventListenersInterface{ public function addListener($eventName, $listener, $priority = 0); public function removeListener($eventName, $listener);}

interface EventSubscribersInterface{ public function addSubscriber(EventSubscriberInterface $subscriber); public function removeSubscriber(EventSubscriberInterface $subscriber);}

interface DebugEventListenersInterface{ public function getListeners($eventName = null); public function hasListeners($eventName = null);} the right

way

Page 91: Be pragmatic, be SOLID

class EventDispatcher implements EventDispatcherInterface, EventListenersInterface, EventSubscribersInterface{ // ...}

class DebugEventDispatcher extends EventDispatcher implements DebugEventListenersInterface{ // ...}

Page 92: Be pragmatic, be SOLID

Design is all about dependencies

Page 93: Be pragmatic, be SOLID

Think about the design!

Page 94: Be pragmatic, be SOLID

Listen to your tests

Page 95: Be pragmatic, be SOLID

”Always leave the code a little better than you found it.”

Page 96: Be pragmatic, be SOLID

But...

Page 97: Be pragmatic, be SOLID

Be pragmatic

Page 98: Be pragmatic, be SOLID

"By not considering the future of your code, you make your code

much more likely to be adaptable in the future."

Page 99: Be pragmatic, be SOLID

At the end of the day what matters most is a business value

Page 100: Be pragmatic, be SOLID

Know the rules well, so you can break them effectively

Page 101: Be pragmatic, be SOLID
Page 102: Be pragmatic, be SOLID

Be pragmatic,be SOLID

kmenzyk [email protected]

Thanks!

Page 103: Be pragmatic, be SOLID

Worth reading

http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

https://gilesey.wordpress.com/2013/09/01/single-responsibility-principle/

”Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martin

http://martinfowler.com/bliki/DesignStaminaHypothesis.html

Page 104: Be pragmatic, be SOLID

Photo Creditshttps://flic.kr/p/5bTy6C

http://www.bonkersworld.net/building-software/

https://flic.kr/p/jzCox

https://flic.kr/p/n37EXH

https://flic.kr/p/9mcfh9

https://flic.kr/p/7XmGXp

http://my.csdn.net/uploads/201205/13/1336911356_6234.jpg

http://bit.ly/1cMgkPA

https://flic.kr/p/qQTMa

http://bit.ly/1EhyGEc

https://flic.kr/p/5PyErP

http://fc08.deviantart.net/fs49/i/2009/173/c/7/The_Best_Life_Style_by_Alteran_X.jpg

https://flic.kr/p/4Sw9pP

https://flic.kr/p/8RjbTS