Interface segregation

The forgotten i in SOLID

@MrDanack

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

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
Errors happen at start of execution Errors can happen during middle of execution
Simpler parts, more of them Fewer parts, but more complex code

Traditional interface segregation example


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);
    }
}

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];
    }
}

Setup in dependency container

Use rdlowrey/auryn...it's awesome. Or use the Symfony one if you have a Symfony app


    // In bootstrap.php
    $injector->alias('VariableMap', 'PSR7VariableMap');

  

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) === false) {
            $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, success case

rdlowrey/auryn makes it easier to write tests.

  
  function testSearchControllerWorks()
  {
      $varMap = new ArrayVariableMap(['searchTerms' => 'foo,bar']);
      $injector = createTestInjector(
          [VariableMap::class => $varMap],
          [DataSource::class => EchoDataSource::class]
      );
      $result = $injector->execute([SearchController::class, 'search']);
      $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);
  }
  
  

Testing this controller, error case


function testSearchControllerException()
{
    $varMap = new ArrayVariableMap([]);
    $injector = createTestInjector(
        [VariableMap::class => $varMap],
        [DataSource::class => EchoDataSource::class]
    );
    $this->setExpectedException(ParamMissingException::class);
    $injector->execute([SearchController::class, 'search']);
}

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 ww
  • 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;
  
        return $dataSource->searchForItems($searchOptions);
    }
}

Extracting a type

  
class SearchOptions
{
    public $keywords;
    public $limit;

    public function __construct(array $keywords, int $limit = 1000)
    {
        foreach ($keywords as $keyword) {
            if (is_string($keyword) === false) {
                $message = "Type ".gettype($keyword)." not allowed";
                throw new \InvalidArgumentException($message);
            }
        }
  
        $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);
    ...
}

Types are easy for DIC's to know about


  // In bootstrap.php
  
  $injector->share(new TmpPath(__DIR__.'/var/temp'));
  $injector->share(new ImageTmpPath(__DIR__.'/var/temp_images'));

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

Fin

 
  • Use interfaces/classes with only the required methods
  • Use semantically meaningful types
  • If your code is boring, you're doing good
  • Questions?

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.”