If you’re a programmer you make design decisions every second. Statements, functions, classes, packages, applications, even entire systems: you need to think, and often think hard, about everything. Luckily there are many useful design principles, patterns and best practices that you can apply. But some of them merely expose code smells. Others only help you design your classes. And some are applicable to packages only. Wouldn’t it be nice to have some more general, always useful, invariably applicable, foundational design principles?
In this talk we’ll look at software from many different perspectives, and while we’re zooming in and out, we’ll discover some of the deeper principles that lie beneath proper object-oriented design. They are the foundation of many of the well-known design patterns and they may even serve as an explanation for code smells.
12. Global design
principles can be
discovered
if we recognise the fact that
communication between objects and
applications are (more or less) equal
13. Communication between
objects
• Calling a function is like sending a message
• The function and its parameters are the message
type
• The arguments constitute the value of the message
15. $money = new Money(20000, 'EUR');
$userId = 123;
$accountService->deposit(
$userId,
$money
);
Translation
The sender prepares the message for the receiver
Prepare the
message
Send the message
16. Communication between
applications
• An HTTP request is a message
• The HTTP method and URI are the type of the
message
• The HTTP request body constitutes the value of the
message
Or AMQP, Stomp, Gearman, ...
17. PUT /accounts/deposit/ HTTP/1.1
Host: localhost
{
"accountId": "123",
"currency": "EUR",
"amount": 20000
}
Inter-application
communication
Opportunity for
sending a
message
42. <h1>You are about to order a nameplate!</h1>
<p>Name on the plate: {{ name }}<br/>
Width of the plate: {{ 12*(name|length) }} cm.</p>
Calculating the width of a
nameplate
Knowledge
43. class Nameplate
{
private $name;
function __construct($name) {
$this->name = $name;
}
function widthInCm() {
return strlen($this->name) * 12;
}
}
Data object
Knowledge is close to
the subject
44. <h1>You are about to order a nameplate!</h1>
<p>Name on the plate: {{ nameplate.name }}<br/>
Width of the plate: {{ nameplate.widthInCm) }}
cm.</p>
No knowledge in the template
49. –The Don't repeat yourself principle
“Every piece of knowledge must
have a single, unambiguous,
authoritative representation
within a system.”
Even across applications!
50. "Don't repeat yourself"
• Doesn't mean you can't repeat data
• It means you can't have knowledge in multiple
locations
52. What's the difference
between...
class Money
{
private $amount;
private $currency;
public function setAmount($amount) {
$this->amount = $amount;
}
public function getAmount() {
return $this->amount;
}
...
}
54. Inconsistent data
$savings = new Money();
$savings->setAmount(1000);
// what's the currency at this point?
$savings->setCurrency('USD');
// only now do we have consistent data
$savings->setCurrency('EUR');
// we have a lot more money now!
$savings->setAmount('Amsterdam');
55. Making something better of this
class Money
{
private $amount;
private $currency;
public function __construct($amount, $currency) {
$this->setAmount($amount);
}
private function setAmount($amount) {
if (!is_int($amount) || $amount < 0) {
throw new InvalidArgumentException();
}
$this->amount = $amount;
}
}
Private
Required
59. What about API messages?
<money>
<amount>1000</amount>
<currency>USD</currency>
</money>
PUT /savings/
<money>
<currency>EUR</currency>
</money>
POST /savings/
61. Forms & Doctrine ORM
$form = $this->createForm(new MoneyType());
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($form->getData());
// calculates change set and executes queries
$em->flush();
}
62. If you allow your users to
change every field at any time
• You end up with inconsistent data
• You loose the why of a change
• You end up with the what of only the last change
• You ignore the underlying real-world scenario
70. class Person
{
/**
* @return PhoneNumber[]
*/
public function getPhoneNumbers() {
return $this->phoneNumbers;
}
}
Initial implementation
71. class Person
{
/**
* @return ArrayCollection
*/
public function getPhoneNumbers() {
return $this->phoneNumbers;
}
}
Implementation leakage
(Doctrine)
72. class Person
{
/**
* @return PhoneNumber[]
*/
public function getPhoneNumbers() {
return $this->phoneNumbers->toArray();
}
}
Hiding implementation
73. class NameplateController
{
function getAction($id) {
$nameplate = $this
->getDoctrine()
->getManager()
->getRepository(Nameplate::class)
->findOneBy(['id' => $id]);
if ($nameplate === null) {
throw new NotFoundHttpException();
}
...
}
}
More implementation hiding
Actual field names!
null or false?
"find"?
74. class NameplateRepository
{
function byId($id) {
$nameplate = $this
->findOneBy(['id' => $id]);
if ($nameplate === null) {
throw new NameplateNotFound($id);
}
return $nameplate;
}
}
Push it out of sight
Domain-specific exception
Hide specific return value
No "find"
75. class NameplateController
{
function getAction($id) {
try {
$nameplate = $this
->nameplateRepository
->byId($id);
} catch (NameplateNotFound $exception) {
throw new NotFoundHttpException();
}
...
}
}
Respect layers
Convert domain
exception to web
specific exception
82. Design your messages in such
a way that
• You hide your implementation
• Clients won't need to reimplement your logic
• Clients get the information they need
89. Discoverability of an API
• Full Reflection capabilities :)
• Basic knowledge of English
90. // I've got this password I want to hash…
$password = ...;
// Look, I found a function for this: password_hash()
password_hash($password, HashingStrategy $hashingStrategy);
// It requires an argument: a password (string)
// I already got a password right here:
password_hash($password);
// Wait, it requires a hashing strategy (a HashingStrategy object)
// I just found a class implementing that interface:
$hashingStrategy = new BcryptStrategy();
// That doesn't work, BcryptStrategy needs a cost
$hashingStrategy = new BcryptStrategy(10);
password_hash($password, $hashingStrategy);
Example of API discovery
Who is talking?
How stupid are they?
How do you find out a valid range?
91. /**
* @param array $options
*/
function some_function(array $options);
/**
* @param integer $type
*/
function some_other_function($type);
/**
* @param object $command
*/
function handle($command);
Undiscoverable APIs
92. Any kind of API should be
maximally discoverable
97. Think about
• Stable dependencies
• Duplication of facts, not knowledge
• Immutability over mutability
• No leakage of implementation details
• Everything should be maximally discoverable
98. –Chris Hadfield
“… I should do things that keep
me moving in the right direction,
just in case — and I should be sure
those things interest me, so
whatever happens, I’m happy.”