Solid structure

Building decoupled applications with an injector.

@MrDanack

Press 'c' to toggle code style
Press 's' for speaker notes

https://joind.in/talk/04aa0

SOLID principles

Single responsibility ★★★★★
Open/closed wat
Liskov substitution ★★★★
Interface segregation
Dependency inversion ★★★

If you don't recognise these.....then reading up on them can be your homework.

Apologies in advance

  • I normally try to make talks start off easy, before reaching the difficult stuff.
  • I think I may have failed.
  • J.B. Rainsberger - Integrated Tests Are A Scam
    https://vimeo.com/80533536

We'll totally get onto Solid structure....

....but first something a bit different.

The true nature of reality

Understanding the most fundamental parts of programming.

@MrDanack

So my education background is physics/chemistry

  • Solids, liquids, gases
  • Molecules and atoms
  • Fundamental particles, quarks and leptons
  • Fundamental forces, gravitational, electromagnetic, strong nuclear, and weak nuclear

So my education background is physics/chemistry

  • Solids, liquids, gases
  • Molecules and atoms
  • Fundamental particles, quarks and leptons
  • Fundamental forces, gravitational, electromagnetic, strong nuclear, and weak nuclear

Normal ways of thinking about code

  • What language to use
  • Frameworks vs Individual libraries
  • Procedural vs Object oriented
  • Functional vs Stateful

Those aren't the fundamental aspects of programming

Those aren't the fundamental things.

They're nice, and most of the time they are what you should be thinking about.

Fundamental programming forces

Can be unit tested vs Can't be unit tested
Semantically meaningful types vs Basic types e.g. strings, ints

Left - bootstrap layer


function getTwitterApiKey()
{
    return getenv('twitter_api_key');
}


// Hard-coded to specific class.
// and so not testable
$app = new App(); 

Right - external services

  • DB
  • External api
  • File system

Right side - is typeless


function sendMessageToUser(
    TwitterAPI $twitterApi,
    Message $message,
    User $user
) {
    $text = "@".$user->getName()." ".$message->getText()
    $twitterApi->sendMessage($text);
}
  
class TwitterAPI
{
    function sendMessage(string $text);
    ...
}
  

Middle - your lovely app code


class SendMessageController
{
    function __construct(TwitterApi $twitter, VariableMap $varMap)
    {
        $message = $varMap->getMessage();
        $twitter->send($message);
    }
}

Having two different modes is a problem

  • Unstructured - testable + un-testable code gets mixed up, leading to a messy application
  • Encapsulating external services with interfaces can be a bit of pain

Dependency injection to the rescue

https://github.com/rdlowrey/auryn

Auryn is an auto-wiring recursive dependency injector.

aka it does DI really well.

Auryn runs code for you


function foo()
{
  echo 'Hello world';
}

$injector->execute('foo');
// Output is 'Hello world'


Can pass it any callable.

Defining raw params


      
class FileWriter implements Writer {
    function __construct($path ) { ... }
    function write($string) { ... }
}

$fileWriterParams = array(
    ':path'    => '/var/coolapp',
);

$injector->define('FileWriter', $fileWriterParams);
// Auryn can also just make objects
$fileWriter = $injector->make(FileWriter::class);


Auto-wiring means it looks at param types


function writeHelloWorld(FileWriter $fileWriter)
{
     $fileWriter->write("Hello world");
}

$fileWriterParams = array(
    ':path'    => '/var/coolapp',
);

$injector->define('FileWriter', $fileWriterParams);
$injector->execute('writeHelloWorld');

I <3 types. And you should too.

Sharing is caring - aka


function writeHelloWorld(FileWriter $fileWriter)
{
     $fileWriter->write("Hello world");
}

$fileWriterParams = array(
    ':path'    => '/var/coolapp',
);

$injector->define('FileWriter', $fileWriterParams);

$injector->share(FileWriter::class); // This line is new
$injector->execute('writeHelloWorld'); // FileWriter is created
$injector->execute('writeHelloWorld'); // FileWriter is reused

You can also share objects



$object = new Foo();
      
// Fiddle with $object state here

$injector->share($object); // This line is new


Aliasing interfaces to classes


class StubWriter implements Writer {
    function write(string $string) {  ... } 
}
  
function foo(Writer $fw)
{
    $fw->write("foo did something");
}

$injector->alias('Writer', 'StubWriter');
$injector->execute('foo');

So far, so 'meh'

Most DICs can do that sort of thing.

The key thing that makes Auryn be incredibly powerful is how it allows you to set delegate functions for creating objects, and all of the object instantiation is done recursively.

Delegation is awesome



function createTwitterApiConfig()
{
    return new TwitterApiConfig(
        getenv('twitter_api_key'),
        getenv('twitter_timout'),
    );
}

$injector->delegate('TwitterApiConfig', 'createTwitterApiConfig');

Recursion is awesome



class TwitterApi {
    function __construct(TwitterApiConfig $tac)
    {
       ...
    }
}

function sendMessage(TwitterApi $twitterApi)
{
    ...
}
      
$injector->delegate('TwitterApiConfig', 'createTwitterApiConfig');
$injector->execute('sendMessage');

End result of using DI

  • Testable + typed that makes up most of your app, can be written cleanly.
  • Non-testable + untyped bootstrap code, completely separated away.
  • Injector provides the binding between these two different regions.

Limitations of using an injector

There's two problems that don't occur in 'normal' programming, when using an injector:

  • More than one of a type.
  • Unknowable dependencies.

More than one of a type


// Move data that is more than 24 months old to archive
function archiveData(PDO $live, PDO $archive) 
{
    // ...
}

  

// Don't execute this directly
function archiveData(PDO $live, PDO $archive) 
{
    // ...
}

// Instead excute a wrapped version.
function archiveDataWrapped(PDOFactory $pdoFactory) 
{
    $live = $pdoFactory->create('live');
    $archive = $pdoFactory->create('archive');
    
    archiveData($live, $archive);
}
  

Context objects



class ArchiveContext {
    public $current;
    public $archive;
}

  

http://accu.org/index.php/journals/246


function createArchiveContext(DataStorageFactory $dsFactory) {
    return new ArchiveContext(
        $dsFactory->create('current');
        $dsFactory->create('archive');
    );
}

$injector->delegate('ArchiveContext', 'createArchiveContext');

// Want to move data from current to archive
function archiveData(ArchiveContext $archiveContext)
{
   $current = $archiveContext->current;
   $archive = $archiveContext->archive;
   ...
}


Sometimes can't know dependencies



function sendMessage(UserPrefs $userPrefs, ...) 
{
    $service = $userPrefs->getMessageService();
    if ($service == 'twitter') {
        // Need a twitter Api
    }
    if ($service == 'pager_duty') {
        // Need a pager duty Api
    }
}

  


function sendTwitterMessage(TwitterApi $twitterApi) {
   ...
}
      
function sendPagerDutyMessage(PagerDuty $pagerDuty) {
      
}
      
function sendMessage(UserPrefs $userPrefs) 
{
    $service = $userPrefs->getMessageService();
    if ($service == 'twitter') {
        return new Exec('sendTwitterMessage');
    }
    if ($service == 'pager_duty') {
        return new Exec('sendPagerDutyMessage');
    }
}

  

Sometimes you do want to use a service locator



$injector->share($injector);

function createFoo(Injector $injector, Config $config)
{
    $apiVersion = $config->getApiVersion();

    if ($apiVersion == 1) {
        return $injector->make('ApiV1');
    }

    return $injector->make('ApiV2');
}

Fin

 
  • Push untestable code to the edges of your application.
  • Push untyped code to the edges of your application.
  • Use Auryn it is awesome.
  • CLI app - https://github.com/Danack/console
  • HTTP app - https://github.com/Danack/tier
  • Watch https://vimeo.com/80533536
  • https://joind.in/talk/88a21
“For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.”

Object-oriented programming is an exceptionally bad idea which could only have originated in California — Edsger W. Dijkstra