Be pragmatic,be SOLID
Krzysztof Menżyk
practises TDDbelieves that software is a craft
loves domain modellingobsessed with brewing
plays squash
kmenzyk [email protected]
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
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