be pragmatic, be solid (at boiling frogs, wrocław)

Post on 13-Apr-2017

538 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Be pragmatic,be SOLID

Krzysztof Menżyk @kmenzyk

Krzysztof Menżyk

Technical Leader at practises TDDbelieves that software is a craftloves domain modellingobsessed with homebrewingplays squash

Do you consider yourselfa professional software developer?

New client

Greenfield project

Starting from scratch

What went wrong?

Your software is bound to change

What are the symptomsof bad design?

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

Design stamina hypothesis

time

cumulativefunctionality

design payoff lineno design

good design

by Martin Fowler

Design will pay offif you plan your product to succeed.

What is Object Oriented Design

then?

Design Principles andDesign Patterns

Robert C. Martin

Single Responsibility Principle

Open Closed Principle

Liskov Substitution Principle

Interface Segregation Principle

Dependency Inversion Principle

Single Responsibility Principle

Open Closed Principle

Liskov Substitution Principle

Interface Segregation Principle

Dependency Inversion Principle

SOLID

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.

High cohesion between things that change for the same reason.

Loose coupling between things that change for different reasons.

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

Hiring policies have changed

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

The design of our REST API

has changed

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

DB table holdingemployee datahas changed

What responsibilities the class has?

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

HR business logic

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

Data presentation(via REST API)

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

Persistence

final 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

final 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

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

High cohesion

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

Loose coupling

Try to describe the responsibility in a single sentence.

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', Money::EUR(2000));

$this->assertEquals(Money::EUR(2000), $employee->getSalary());

$employee->promote('Senior Developer', Money::EUR(3000));

$this->assertEquals(Money::EUR(3000), $employee->getSalary());

$employee->promote('Technical Leader', Money::EUR(2000));

$this->assertEquals(Money::EUR(3000), $employee->getSalary());}

/** @test */public function it_hires_with_salary(){ $employee = Employee::hire('John Doe', 'Junior Developer', Money::EUR(2000));

$this->assertEquals(Money::EUR(2000), $employee->getSalary());}

/** @test */public function it_promotes_with_new_salary(){ $employee = Employee::hire('John Doe', 'Junior Developer', Money::EUR(2000)); $employee->promote('Senior Developer', Money::EUR(3000));

$this->assertEquals(Money::EUR(3000), $employee->getSalary());}

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

$this->assertEquals(Money::EUR(3000), $employee->getSalary());}

the right

way

One test covers one behaviour.

SRP violation usually indicates other violations.

SOLID

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

// do stuff to shorten valid url

return $shortenedUrl; }

// ...}

Shorten only PL links

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 hasPlDomain(Url $longUrl) { // ... }

// ...}

Shorten only PL links

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

SOLID

LiskovSubstitutionPrinciple

Subtypes must be substitutable for their base types.

class InMemoryTweets implements 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]; }}

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

public function get($tweetId);}

Public API is not enough.

Design by contract

PreconditionsWhat does it expect?

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

public function get($tweetId);}

interface Tweets{ /** * @param Tweet $tweet */ public function add(Tweet $tweet);

/** * @param int $tweetId */ public function get($tweetId);}

PostconditionsWhat does it guarantee?

interface Tweets{ /** * @param Tweet $tweet */ public function add(Tweet $tweet);

/** * @param int $tweetId */ public function get($tweetId);}

interface Tweets{ /** * @param Tweet $tweet */ public function add(Tweet $tweet);

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

InvariantsWhat does it maintain?

interface Tweets{ /** * @param Tweet $tweet */ public function add(Tweet $tweet);

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

interface Tweets{ /** * Adds a tweet to the collection * which can be obtained by „get” * @param Tweet $tweet */ public function add(Tweet $tweet);

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

LSP in terms of the contract

Preconditions are no stronger than the base class method.

class InMemoryTweets implements Tweets{ // ...

public function get($tweetId) { if ($tweet < 10) { throw new \RuntimeException(); }

// ... }}

violation

Postconditions are no weaker than the base class method.

class InMemoryTweets implements Tweets{ // ...

public function get($tweetId) { // ...

if ($tweet->isArchived()) { return null; } // ... }}

violation

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

// ...}

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

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

class EntityRepository{ /** * 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 PersistentTweets) { $tweet = $this->tweets->get($tweetId);

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

// ...} violation

Violations of LSP arelatent violations of OCP.

class PersistentTweets 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.

SOLID

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 addSubscriber(EventSubscriberInterface $subscriber) { 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 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{ // ...}

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.

class PayForOrder{ private $payPalApi;

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

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

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

// ... }}

PayForOrder

PayPalApi

Business Layer

Integration Layer

High-level module

Low-level module

PayForOrder

PayPalApi

violation

Abstraction is the key.

PaymentApi

PayPalApi

PayForOrder

class PayForOrder{ public function __construct(PaymentApi $paymentApi) { $this->paymentApi = $paymentApi; }

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

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

// ... }}

class PayForOrder{ public function __construct(PaymentApi $paymentApi) { $this->paymentApi = $paymentApi; }

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

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

// ... }}

Abstractions should not depend on details.

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

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

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

// ... }}

the right

way

Details should depend on abstractions.

interface PaymentProvider{ public function charge(CreditCard $creditCard, Money $forAmount);}

class PayPalApi implements PaymentProvider{ public function charge(CreditCard $creditCard, Money $forAmount) { // ... }}

Define the interface from the usage point of view.

Interface should communicate responsibilities rather than

implementation details.

Concrete things change alot.

Abstract things change muchless frequently.

PaymentApi

PayPalApiexternal library

PayForOrder

PaymentApi

PayPalProvider

PayForOrder

PayPalApiexternal library

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

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

Design is all about dependencies.

Think about the design!

Learn your code.

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

Krzysztof Menżyk @kmenzyk

Thanks!

Worth reading

http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdfhttp://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOodhttps://gilesey.wordpress.com/2013/09/01/single-responsibility-principle/”Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martinhttp://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

https://flic.kr/p/pMFZ9n

https://flic.kr/p/zrbMdo

top related