20. Gather together those things that
change for the same reason
Separate those things that change
for different reasons
21. 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()
{
// ...
}
}
23. 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()
{
// ...
}
}
24. 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()
{
// ...
}
}
25. 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()
{
// ...
}
}
26.
27. 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
28. 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
37. 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);
}
38. 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)
{
// ...
}
}
39. /** @test */
public function it_shortens_only_urls_with_pl_domains()
{
$urlWithEuDomain = // ...
$this->setExpectedException(InvalidUrl::class, 'Url has no .pl domain');
$this->shortener->shorten($urlWithEuDomain);
}
40. /** @test */
public function it_shortens_only_urls_with_pl_domains()
{
$urlWithEuDomainButWithHttpScheme = // ...
$this->setExpectedException(InvalidUrl::class, 'Url has no .pl domain');
$this->shortener->shorten($urlWithEuDomainButWithHttpScheme);
}
41. /** @test */
public function it_shortens_urls()
{
$validUrl = // make sure the url satisfies all "ifs"
$shortenedUrl = $this->shortener->shorten($validUrl);
// assert
}
43. 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
46. 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
47. class HasHttp implements Rule
{
public function isSatisfiedBy(Url $url)
{
// ...
}
}
class HasPlDomain implements Rule
{
public function isSatisfiedBy(Url $url)
{
// ...
}
}
48. ”There is a deep synergy between
testability and good design”
– Michael Feathers
50. 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];
}
}
51. class BoundedTweets extends Tweets
{
const MAX = 10;
public function add(Tweet $tweet)
{
if (count($this->tweets) > self::MAX) {
throw new OverflowException();
}
parent::add($tweet);
}
}
56. What does it expect?
What does it guarantee?
What does it maintain?
57. 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)
{
// ...
}
}
58. 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
{
// ...
}
60. 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);
}
}
61. 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)
{
// ...
}
// ...
}
64. 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
68. 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());
// ...
}
}
73. 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());
// ...
}
}
74. 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
76. 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
85. 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);
}
86. 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);
// ...
}
// ...
}
87. 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
90. 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