be pragmatic, be solid (at boiling frogs, wrocław)
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