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

  1. Categorise code as either intent or implementation
  2. Move implementation details into intent named methods
  3. Extract these methods into a class and inject into object
  4. Look for implementation changes based on input
  5. If they exist, extract an interface
  6. Extract implementations into appropriate classes
  7. 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

  1. Get your computer
  2. Go to https://github.com/barryosull/interfaces-workshop
  3. Follow the install instructions
  4. 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,056