Beyond Design Principles & Patterns
Writing good OO code
Matthias Noback
@matthiasnoback
Design patterns
●
Abstract factory
● Mediator
● Proxy
● Builder
● Composite
● Chain of responsibility
● Adapter
● Strategy
●
Façade
● Bridge
● Observer
● Singleton
● Factory method
● Command
Design principles, and more patterns
● Class principles (SOLID)
● Package design principles
●
Tactical DDD patterns
● Testing patterns
● Architectural patterns
Education
● Refer to books (but nobody reads them)
● In interviews: do you know SOLID?
●
Knowing is one thing, applying is something else.
Back to basics
Any class can serve as the blueprint of an object,
but not every object will be a Good Objecttm
Objects introduce meaning
By wrapping primitive values
Class name = Type
Strings
Email
addresses
Floats
Latitude
If not every string counts as an email address,
introduce a dedicated type: EmailAddress.
If not every integer counts as an age,introducea
dedicated type: Age.
And so on!
Objectskeeptogetherwhatbelongs
together
Latitude and Longitude
X and Y
Amount and Currency
Distance and Unit
...
Cohesion
What is together belongs together.
Cohesion
What belongs together, gets together.
Objects attract related data and behavior.
Objectsbringtogethermeaningful
behaviors
Coordinates.distanceTo(Coordinates other): Distance
Money.convertTo(Currency other): Money
Map.set(String key, T item): void
Map.get(String key): T
Array.push(T item): void
Array.pop(): T
Objects
State & Behavior
final class Coordinates
{
private Latitude $latitude;
private Longitude $longitude;
public function halfwayTo(
Coordinates $other): Coordinates {
// ...
}
}
State
Behavior
Primitive
values Entities
Value
objects
Services
Anemic
domain
objects
For all objects
Make sure they can't exist in an incoherent or
inconsistent state
At construction time
● Provide the right data (values, value objects)
● Provide the right services (collaborating objects)
// no setter injection
$service = new Service();
$service->setLogger(...);
// no setter party
$object = new Object();
$object.setFoo(...);
$object.setBar(...);
At modification time
● Provide the right data (values, value objects)
What's the “right” data?
● Valid (correct types, correct number of things,
etc.)
● Meaningful (within an allowed range, pattern,
etc.)
What's the “right” data?
Only allow transitions that make sense
final class Order
{
public function cancel(...): void
{
if (wasShipped) {
throw new LogicException(...);
}
}
}
Whenimplementingbehavior:follow
this recipe
public function someMethod(int $value)
{
// check pre-conditions
Assertion::greaterThan($value, 0);
// fail early
if (...) {
throw new RuntimeException(...);
}
// happy path: at "0" indent
// check post-conditions
return ...;
}
Command/Query Separation
Every method is either
a command or a query method.
public function commandMethod(...): void
{
/*
* Changes observable state of the system
*
* May have side effects:
* - network calls
* - filesystem changes
*
* Returns nothing.
* May throw an exception.
*/
}
public function queryMethod(...): [specific type]
{
/*
* Returns something, doesn't change
* anything.
*
* May throw an exception.
*/
}
CQS principle
Asking for information doesn’t
change observable state.
Return single-type values
● Object of specific type, or an exception
● List with values of specific type, or an empty list
public function queryMethod(...): [specific type]
{
if (...) {
throw new RuntimeException(...);
}
return objectOfSpecificType;
}
public function queryMethod(...): List
{
if (...) {
return List::empty();
}
return List::of(...);
}
At failure time
Throw useful and detailed exceptions
Defensive programming
Recover from failure?
No, usually: scream about it!
Offensive programming
Add lots of sanity checks and throw exceptions.
Assert::greaterThan(...);
Assert::count(...);
...
Offensive programming
Run static analysis tools, like PHPStan.
DateTime::createFromFormat() returns bool|DateTime
Offensive programming
Introduce strictly typed alternatives to PHP's
weakly typed functions.
public static function createFromFormat(
string $format,
string $date
): DateTimeImmutable {
$result = DateTimeImmutable::createFromFormat(
$format,
$date
);
if ($result === false) {
throw new RuntimeException(...);
}
return $result;
}
State versus behavior, revisited
Objects have state and behavior
But they hide data and implementation details
State versus behavior, revisited
Expose more behavior, less state
Treat your objects as black boxes
When writing tests for them
Don'ttestconstructorsbycalling
getters
Give the object reason to remember something,
and describe that reason in a test.
public function it_can_be_constructed(): void
{
$money = new Money(1000, new Currency('EUR'));
assertEquals(
1000,
$money->getAmount()
);
assertEquals(
'EUR',
$money->getCurrency()->asString()
);
}
Getters just
for testing!
public function it_can_be_converted(): void
{
$money = new Money(1000, new Currency('EUR'));
$rate = new ExchangeRate(
new Currency('EUR'),
new Currency('USD'),
1.23456
);
$converted = $money->convert($rate);
assertEquals(
new Money(
1235, // rounded to the second digit
new Currency('USD')
)
$converted
);
}
No getters!
Nor testing them!
Guiding experiment
Only add a getter if
something other than a test needs it.
Changing the behavior of an object
Always aim to do it without touching the code of its class
(let it remain a black box!)
class Game
{
public function move(): void
{
$steps = $this->roll();
}
private function roll(): int
{
return mt_rand(1, 6);
}
}
How to use a
hard-coded value
for testing?
class Game
{
public function move(): void
{
$steps = $this->roll();
}
protected function roll(): int
{
return mt_rand(1, 6);
}
}
class GameWithFixedRoll extends Game
{
protected function roll(): int
{
return 6;
}
}
final class Game
{
private Die $die;
public function __construct(Die $die)
{
$this->die = $die;
}
public function move(): void
{
$steps = $this->die->roll();
}
}
interface Die
{
public function roll(): int;
}
Dependency injection!
Composition instead of
inheritance
Final classes!
final RandomDie implements Die
{
public function roll(): int
{
return mt_rand(1, 6);
}
}
final FixedDie implements Die
{
public function __construct($value)
{
$this->value = $value;
}
public function roll(): int
{
return $this->value;
}
}
Technically a test double
called “stub”
Behavior can be modified
without touching the code
“Composition over inheritance”
Changing the behavior of objects by composing
them in different ways
(as opposed to using inheritancetooverride
behavior)
Create better objects
● Introduce more types.
● Be more strict about them.
●
Design objects that only accept valid data.
● Design objects that can only be used in valid
ways.
● Use composition instead of inheritance to
change an object's behavior.
Well designed objects lead to an application that
almost clicks together
Yeah, like Lego...
https://joind.in/talk/c2deb
Matthias Noback
@matthiasnoback

Beyond Design Principles and Patterns