Using Interfaces Effectively
Why do Interfaces exist?
To make our jobs easier
Less of this
More of this
@barryosull
Who am I
- Lead Developer, Solutions Architect, Trainer
- Architect of many, many apps
- Recovered Architecture Astronaut
- A little DDD and EventSourcing obsessed
(I will talk for hours)
@barryosull
https://barryosull.com
contact@barryosull.com
@barryosull
Encapsulation
Separate the details from the behaviour
What you want to do
How you want to do it
Send a letter to an address
Use the postal service
Remove noise to makes things clearer
(See the forest for the trees)
@barryosull
Interfaces
Same behaviour, different implementations
What you want to do
How you want to do it
Send a letter to an address
Use the postal service
Deliver via Jetpack
@barryosull
The Benefit of Interfaces
Less Mess
More Clarity
Interfaces Example
<?php
namespace App\Services;
use App\ValueObjects\ {Message, Address, MessageCollection};
interface MessageDelivery
{
public function deliver(Message $message, Address $address);
public function fetch(Address $address): MessageCollection;
}
?>
<?php
namespace App\Services;
class MessageDeliveryException extends \Exception {}
?>
<?php
namespace App\Services;
class MessageDeliveryException extends \Exception {}
?>
@barryosull
Interface Implementation
<?php
namespace App\Services;
use App\Service\ {MessageDelivery, MessageDeliveryException};
use App\ValueObjects\ {Message, Address};
use PostOffice\ {Letter, PostBox, FullException};
class MessageDeliveryPostOffice implements MessageDelivery
{
private $post_box;
public function __construct(PostBox $post_box)
{
$this->post_box = $post_box;
}
public function deliver(Message $message, Address $address)
{
try {
$letter = new Letter($address->toArray(), $message->value());
$this->post_box->post($letter);
} catch (FullException $e) {
throw new MessageDeliveryException($e->getMessage(), $e->getCode(), $e);
}
}
//... fetch implementation omitted
}
?>
@barryosull
Binding the Interface
<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Env;
use App\Service\MessageDelivery;
use App\Service\ {MessageDeliveryPostBox, MessageDeliveryFake, MessageDeliveryTimer};
class AppServiceProvider extends ServiceProvider
{
public function register()
{
if ($this->app->environment() == Env::TESTING) {
$this->app->singleton(MessageDelivery::class, MessageDeliveryFake::class);
} else {
$this->app->singleton(MessageDelivery::class, function(){
return new MessageDeliveryTimer(
new MessageDeliveryPostBox()
);
});
}
}
}
@barryosull
Decisions, decisions
Implementation B
Implementation A
Choosing your implementation
Using the interface
<?php
namespace App\Controllers\Http;
use App\Service\MessageDelivery;
use App\ValueObjects\ {Message, Address};
class MessageDeliveryController
{
private $message_delivery;
public function __construct(MessageDelivery $message_delivery)
{
$this->message_delivery = $message_delivery;
}
public function handle(Request $request)
{
$message = new Message($request->input('message'));
$address = new Address($request->input('address'));
$this->message_delivery->deliver($message, $address);
}
}
?>
@barryosull
Testing interfaces
Integration tests
<?php
namespace Tests\Integration\App\Service\MessageDelivery;
use App\Service\MessageDelivery;
abstract class MessageDeliveryTest extends \PHPUnit\Framework\TestCase
{
abstract protected function makeMessageDelivery(): MessageDelivery;
public function test_delivering_a_message()
{
$message_delivery = $this->makeMessageDelivery();
$message = new Message("Hi there");
$address = new Address(["line 1", "line 2", "Wexford", "IE"]);
$message_delivery->deliver($message, $address);
$delivered_message = $message_delivery->fetch($address);
$this->assertEquals($message, $delivered_message, "Messages should match");
}
}
@barryosull
When to use Interfaces
There is more that one way to do something in the codebase
Examples:
- You want to change behaviour, based on a value
- You want to change technologies/libraries
- Your acceptance tests are incredibly slow
Do not add interfaces for the sake of interfaces
@barryosull
Seeing the forest for the trees
Extracting interfaces from existing code
- Categorise code as either intent or implementation
- Move implementation details into intent named methods
- Extract these methods into a class and inject into object
- Look for implementation changes based on input
- If they exist, extract an interface
- Extract implementations into appropriate classes
- Bind at runtime, or create depending on input
@barryosull
Real World Interfaces
@barryosull
PSR3
Captain's Log
<?php namespace Psr\Log;
interface LoggerInterface
{
public function emergency($message, array $context = array());
public function alert($message, array $context = array());
public function critical($message, array $context = array());
public function error($message, array $context = array());
public function warning($message, array $context = array());
public function notice($message, array $context = array());
public function info($message, array $context = array());
public function debug($message, array $context = array());
public function log($level, $message, array $context = array());
}
@barryosull
Flysystem
Let's file this away for later
<?php namespace League\Flysystem;
interface AdapterInterface extends ReadInterface
{
const VISIBILITY_PUBLIC = 'public';
const VISIBILITY_PRIVATE = 'private';
public function write($path, $contents, Config $config);
public function writeStream($path, $resource, Config $config);
public function update($path, $contents, Config $config);
public function updateStream($path, $resource, Config $config);
public function rename($path, $newpath);
public function copy($path, $newpath);
public function delete($path);
public function deleteDir($dirname);
public function createDir($dirname, Config $config);
public function setVisibility($path, $visibility);
}
@barryosull
The Workshop
Process:
- Split into teams of 4 - 5 people
- 15 mins to get to know each other
- Setup the workshop codebase
- Choose a challenge to complete together
Take an existing Laravel 5.5 App and solve problems using interfaces
@barryosull
Challenge 1
PSR3 (Beginner)
We want to switch from our own naive implementation of a logger to the PSR3 standard
Notes:
- Open up the `RequestLogger` middleware
- Extract the logger logic and wrap in a PSR3 interface
- Bind your implementation to the interface and inject it into the middleware
- Swap the implementation with Monolog
@barryosull
Challenge 2
Slow API (Intermediate)
A slow API is slowing down our acceptance tests. We want to use a fake one during those tests.
Notes:
- Code lives in `Controllers\Front\PostController`
- When testing use the fake
- When in local/staging/production use the real one
- Make the caching easy to enable/disable
@barryosull
Challenge 3
Caching and timing (Hardmode)
The contacts list in admin is constantly getting hit with requests and it's impacting the DB. We want to add a cache, but we don't know which one is best.
Notes:
- Write a cache in both Redis and the FileSystem
- Make it easy to switch one version for another
- Time how fast each cache is
- Make it easy to enable or disable the timer
- * Cache must be cleared when a user is stored (expect Eloquent to get in the way here)
@barryosull
Getting started
- Get your computer
- Go to https://github.com/barryosull/interfaces-workshop
- Follow the install instructions
- Choose a challenge and get to it!
@barryosull
Q&A
@barryosull
https://barryosull.com
contact@barryosull.com
https://slides.com/barryosull/workshop-using-interfaces-effectively
Workshop URL: https://github.com/barryosull/interfaces-workshop
Workshop: Using Interfaces Effectively
By Barry O' Sullivan
Workshop: Using Interfaces Effectively
Slides to introduce the concepts of interfaces and the following workshop challenges.
- 1,245