Interface segregation

The forgotten i in SOLID

@MrDanack

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

Maintainer of Imagick and Gmagick extensions.

PHP RFCs:
Closure from callable for PHP 7.1
Consistent Callables for PHP 8.0
Constructor behaviour of internal classes passed PHP 7.0

Codez:
Auryn - the best D.I. library https://github.com/rdlowrey/auryn
Tier - D.I. based framework https://github.com/danack/tier
Jig - D.I. based templating phpjig.com

What is interface segregation?

 
“many client-specific interfaces are better than one general-purpose interface.”
  • Easier to test
  • Makes you more productive…
  • …after doing some setup work
  • More reasonable code
Reasonable   Understandable
Obvious from reading the code Documentation / colleague can explain how it works

class MySQLi {
  function __construct($host, $username, $password,
        $dbname, $port) {...}
}  


class S3Client { 
    public function factory(array $config) {}
}

Reasonable   Understandable
Errors happen at boundaries of execution Errors can happen during middle of execution

  //oops - forget to set password and dbname
  $db = new MySQLi('localhost', 'db_user');


  $config['passwrod'] = '12345';
  S3Client::factory($config)

Reasonable   Understandable
Simpler parts, more of them Fewer parts, but more complex code

use Psr\Http\Message\ServerRequestInterface 
      as Request;
      
class SearchController
{
    function search(Request $request, ...)
    {
       ...
    }
}


interface ServerRequestInterface extends RequestInterface
{
    public function getServerParams();
    public function getCookieParams();
    public function withCookieParams(array $cookies);
    public function getQueryParams();
    public function withQueryParams(array $query);
    public function getUploadedFiles();
    public function withUploadedFiles(array $uploadedFiles);
    public function getParsedBody();
    public function withParsedBody($data);
    public function getAttributes();
    public function getAttribute($name, $default = null);
    public function withAttribute($name, $value);
    public function withoutAttribute($name);
}


interface RequestInterface extends MessageInterface
{
    public function getRequestTarget();
    public function withRequestTarget($requestTarget);
    public function getMethod();
    public function withMethod($method);
    public function getUri();
    public function withUri(UriInterface $uri, 
        $preserveHost = false);
}


interface MessageInterface
{
    public function getProtocolVersion();
    public function withProtocolVersion($version);
    public function getHeaders();
    public function hasHeader($name);
    public function getHeader($name);
    public function getHeaderLine($name);
    public function withHeader($name, $value);
    public function withAddedHeader($name, $value);
    public function withoutHeader($name);
    public function getBody();
    public function withBody(StreamInterface $body);
}

Creating a mock ...?


function testSearchController()
{
    $request = \Mockery::mock(Request::class)
      ->shouldReceive(...)
      ->andReturn(...)
      ->getMock();
 
   ???
   ???
}

class SearchController
{
   function search(Request $request, DataSource $dataSource) 
   {
      $queryParams = $request->getQueryParams();
      if (!array_key_exists('searchTerms', $queryParams)) {
         throw new ParamsMissingException("...");
      }
      $searchTerms = $queryParams['searchTerms'];

      $searchOptions = [];
      $searchOptions['keywords'] = explode(',', $searchTerms);
      return $dataSource->searchForItems($searchOptions);
   }
}

  $queryParams = $request->getQueryParams();
  if (!array_key_exists('searchTerms', $queryParams)) {
     throw new ParamsMissingException("...");
  }
  $searchTerms = $queryParams['searchTerms'];

Extracting a type


interface VariableMap
{
    /**
     * @throws ParamMissingException
     */
    public function getVariable(string $variableName) : string;
}

    class PSR7VariableMap implements VariableMap {
    /** @var ServerRequestInterface */
    private $serverRequest;

    public function __construct(ServerRequestInterface $serverRequest) {
        $this->serverRequest = $serverRequest;
    }

    public function getVariable(string $variableName) : string {
        $queryParams = $this->serverRequest->getQueryParams();
        if (array_key_exists($variableName, $queryParams) === false) {
            $message = "Parameter [$variableName] is not available";
            throw new ParamMissingException($message);
        }

        return $queryParams[$variableName];
    }
}

Controller is now simpler, and more reasonable


class SearchController
{
   function search(VariableMap $variableMap, DataSource $dataSource)
   {
       $searchTerms = $variableMap->getVariable('searchTerms');
       $searchOptions = [];
       $searchOptions['keywords'] = explode(',', $searchTerms);

       return $dataSource->searchForItems($searchOptions);
   }
}

I hate strongly dislike mocks

  • Mocks should only be used when you have to test behaviour
  • Stubs/fakes are much easier to use
  • Stubs/fakes suffer from poor marketing...

Other implementations are equally valid

class ArrayVariableMap implements VariableMap {
  public function __construct(array $variables) {
    $this->variables = $variables;
  }

  public function getVariable(string $variableName) : string {
    if (!array_key_exists($variableName, $this->variables)) {
        $message = "Parameter [$variableName] is not available";
        throw new ParamMissingException($message);
    }

    return $this->variables[$variableName];
  }
}

Testing this controller, success case

  function testSearchControllerWorks()
{
    $varMap = new ArrayVariableMap(
        ['searchTerms' => 'foo,bar']
    );
    // EchoDataSource just returns the keywords searched
    $dataSource = new EchoDataSource(); 

    $controller = new SearchController();
    $result = $controller->search($varMap, $dataSource);
    $this->assertEquals(['foo', 'bar'], $result);
} 

Testing this controller, error case

  
function testSearchControllerException()
{
    $varMap = new ArrayVariableMap([]);
    $dataSource = new EchoDataSource();

    $controller = new SearchController();
    $this->setExpectedException('ParamMissingException');
    $controller->search($varMap, $dataSource);
}
  
  

Benefits review

  • Controller re-usable with different HTTP library
  • Actually no longer tied to HTTP request.
    Can be used by CLI scripts
  • Tests easier to write, as can use array implementation
  • Tests run faster
  • Tests are easier to reason about

Testing code

  • J.B. Rainsberger - Integrated Tests Are A Scam
    https://vimeo.com/80533536
  • Unit tests aren't what you think they are either
Front controller / dispatcher SearchController::search DataSource::searchForItems
Front controller / dispatcher interface Request { } getServerParams() getCookieParams() withCookieParams(...) getQueryParams() getUri() withQueryParams(...) getUploadedFiles() withUploadedFiles(...) getParsedBody() getHeaders() withParsedBody(...) getAttributes() getAttribute(...) withAttribute(...) withoutAttribute(...) getRequestTarget() withRequestTarget(...) getMethod() withMethod(...) getBody() withUri(...) getProtocolVersion() withProtocolVersion(...) withHeader(...) withoutHeader(...) withAddedHeader(...) hasHeader(...) getHeader(...) getHeaderLine(...) withBody(...) interface VariableMap { function getVariable($variableName); } SearchController::search DataSource::searchForItems

What is interface segregation type specialization?

 
“many client-specific interfaces types are better than one general-purpose interface type.”

class SearchController
{
  function search(DataSource $dataSource, VariableMap $variableMap)
  {
    $searchTerms = $variableMap->getVariable('searchTerms');
    $searchOptions = [];
    $searchOptions['keywords'] = explode(',', $searchTerms);
    $searchOptions['limite'] = 50; // LIMIT added
  
    return $dataSource->searchForItems($searchOptions);
  }
}

Extracting a type

  
class SearchOptions
{
    public $keywords;
    public $limit;

    public function __construct(array $keywords, int $limit = 1000)
    {
        //@TODO - check $keywords are all strings
        $this->keywords = $keywords;
        $this->limit = $limit;
    }
}

class SearchController
{
    function search(VariableMap $variableMap, DataSource $dataSource)
    {
        $searchTermsString = $variableMap->getVariable('searchTerms');
        $searchTermsArray = explode(',', $searchTermsString);
        $searchOptions = new SearchOptions($searchTermsArray, 50);

        return $dataSource->searchForItems($searchOptions);
    }
}

Use semantically meaningful types

function writeTempFile(string $tmpPath) {
    ...
    file_put_contents($tmpPath.'/foo.txt', $data);
    ...
}
function writeImageTempFile(string $tmpPath) {
    ...
    file_put_contents($tmpPath.'/foo.png', $imageData);
    ...
}


class TmpPath
{
    private $path;

    public function __construct(string $tmpPath)
    {
        $this->path = $tmpPath;
    }

    public function getPath()
    {
        return $this->path;
    }
}

Use semantically meaningful types


function writeTempFile(TmpPath $tmpPath)
{
    ...
    file_put_contents($tmpPath->getPath().'/foo.txt', $data);
    ...
}

function writeImageTempFile(ImageTmpPath $tmpPath)
{
    ...
    file_put_contents($tmpPath->getPath().'/foo.png', $imageData);
    ...
}

Psychological aspects of programming

The chase for Endorphins

@MrDanack

How does this code make you feel?


class ArrayVariableMap implements VariableMap
{
    public function __construct(array $variables)
    {
        $this->variables = $variables;
    }

    public function getVariable(string $variableName) : string
    {
        if (array_key_exists($variableName, $this->variables) === false) {
            $message = "Parameter [$variableName] is not available";
            throw new ParamMissingException($message);
        }

        return $this->variables[$variableName];
    }
}

Terminology sculpts cognition

“The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise. The intellectual effort needed to ... understand a program need not grow more than proportional to program length.” - Edsger W. Dijkstra

a.k.a. think of your code being better because it is simpler rather than being boring.

Coping with many more components

  • Dependency injector container to the rescue.
  • Solves explosion of dependencies - each dependency only has to be setup once.
  • You focus on writing code that consumes dependencies.
  • Use rdlowrey/auryn it's great! Or use php-di/php-di it's kind of ok.

Injector runs code for you

    $foo = new Foo();
    $foo->bar();
    $injector->make('Foo');
    $foo->bar();
    $injector->execute('Foo::bar');

Dependencies built recursively

class Zot {
    function __construct(Fot $fot) { ... }
    function bar() {...}
}
class Fot {
    function __construct(Pik $pik) { ... }
}

class Pik {
    function __construct() { ... }
}
$injector->execute('Zot::bar');

Aliasing a type to an implementation

  
function foo(DataSource $dataSource) {
    return $dataSource->search('bar');
}

$injector->alias('DataSource', 'EchoDataSource');
$result = $injector->execute('foo'); 

    

Sharing aka singleton


  class Foo {
      public function bar(TmpPath $tmpPath) {
          ...
      }
  }

  $injector->share(new TmpPath(__DIR__.'/var/temp'));  
  $injector->execute('Foo::bar');


function testSearchControllerWorks()
{
    $varMap = new ArrayVariableMap(['searchTerms' => 'foo,bar']);

    $injector->alias('VariableMap', 'ArrayVariableMap');
    $injector->share($varMap);

    $injector->alias('DataSource', 'EchoDataSource');
    $injector->share(new EchoDataSource);

    $result = $injector->execute('SearchController::search');
    $this->assertEquals(['foo', 'bar'], $result);
}  

    

Delegation is key

  

function createESCredentials() {
    $username = getenv('elasticsearch_username');
    $password = getenv('elasticsearch_password');

    return new ESCredentials($username, $password);
}

$injector->delegate('ESCredentials', 'createESCredentials');
$injector->make('ESCredentials');
  class ElasticSearchDataSource implements DataSource {
  public function __construct(ElasticSearchClient $esClient) {
    ...
  }

  ...
}
class ElasticSearchClient {
    public function __construct(ESCredentials $credentials) {
      ...
    }
}   
class ESCredentials {
    public function __construct($username, $password) {
      ...
    }
}
  $injector->alias('DataSource', 'ElasticSearchDataSource');
  $injector->delegate(
      'ElasticSearchCredentials',
      'createESCredentials'
  );
  // This needs a data-source
  $injector->execute('SearchController::search');
  • Not as scary as it might seem at first.
  • In practice you get used to it really quickly.
  • I find it to be such a productivity gain, that it's worth doing - for tests if nothing else.

Fin

 
  • Use interfaces/classes with only the required methods
  • Use semantically meaningful types
  • If your code is boring, you're doing good
  • Use Auryn to do the boring wiring up
  • Questions?
  • https://joind.in/talk/325e5

Using this stuff in production?

Tier - app 'framework' based around DI

'Framework' - https://github.com/danack/tier

Example app - https://github.com/danack/tierjigskeleton

When people give talks on the "S.O.L.I.D." design principles one of the letters that doesn't get enough attention is the "i" - the "interface segregation principle". This talk seeks to redress that imbalance by going into a bit more in-depth into:

  • An introduction to interface segregation and an explanation of how it make your code easier to test.
  • Why in PHP we need to apply the principle more broadly, to make types be more specific, so that code is more reasonable.
  • Me babbling on about emotions, and how good code is boring. Which is good!
“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.”