4Developers 2015: Be pragmatic, be SOLID - Krzysztof Menżyk

Preview:

Citation preview

Be pragmatic,be SOLID

Krzysztof Menżyk

practises TDDbelieves that software is a craft

loves domain modellingobsessed with brewing

plays squash

kmenzyk krzysztof@menzyk.net

Do you consider yourselfa professional software developer?

New client

Greenfield project

Starting from scratch

What went wrong?

The code started to rot

The design is hard to change

Rigidity

The design is easy to break

Fragility

Immobility

The design is hard to reuse

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

Viscosity

Your software is bound to change

Design stamina hypothesis

time

cumulativefunctionality

design payoff lineno design

good design

by Martin Fowler

What is Object Oriented Design

then?

Design Principles andDesign Patterns

Robert C. Martin

Single Responsibility

Open Closed

Liskov Substitution

Interface Segregation

Dependency Inversion

SingleResponsibilityPrinciple

A class should have only one reason to change

Gather together those things that change for the same reason

Separate those things that change for different reasons

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() { // ... }}

Try to describe what the class does

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() { // ... }}

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() { // ... }}

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() { // ... }}

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

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

What about applying SRP to class methods?

What about applying SRP to test methods?

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

/** @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

One test covers one behaviour

OpenClosedPrinciple

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

Write once, change never!

Wait! What?

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

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) { // ... }}

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

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

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

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

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

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

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

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

// assert}

Testing seems hard?

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

Abstraction is the key

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

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

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

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

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

– Michael Feathers

LiskovSubstitutionPrinciple

Subtypes must be substitutable for their base types

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]; }}

class BoundedTweets extends Tweets{ const MAX = 10;

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

parent::add($tweet); }}

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

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

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

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

violation

Design by contract

Design by contract(because public API is not enough)

What does it expect?

What does it guarantee?

What does it maintain?

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) { // ... }}

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{ // ...}

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

// ...}

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

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) { // ... }

// ...}

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

Violations of LSP arelatent violations of OCP

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

LSP violations are difficult to detect until it is too late

Ensure the expected behaviour of your base class is preserved in

derived classes

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

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

// ... }}

PayForOrder

PayPalApi

Business Layer

Integration Layer

High-level module

Low-level module

PayForOrder

PayPalApi

Abstraction is the key

PaymentProvider

PayPalApi

PayForOrder

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

// ... }}

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

Define the interface from the usage point of view

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

Concrete things change alot

Abstract things change muchless frequently

PaymentProvider

PayPalProvider

PayForOrder

PayPalApi

3rd party

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

PaymentProvider

PayPalProvider

PayForOrder

PayPalApi

3rd party

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

class TestPaymentProvider implements PaymentProvider { //... }

InterfaceSegregationPrinciple

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

Many client specific interfaces are better than one general purpose

interface

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

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); // ... }

// ...}

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

It serves too many different types of clients

Design interfaces from clients point of view

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

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

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

Design is all about dependencies

Think about the design!

Listen to your tests

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

But...

Be pragmatic

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

much more likely to be adaptable in the future."

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

Know the rules well, so you can break them effectively

Be pragmatic,be SOLID

kmenzyk krzysztof@menzyk.net

Thanks!

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

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