Interface segregation

The forgotten i in SOLID

@MrDanack

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

SOLID principles

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

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

Maintainer of Imagick and Gmagick extensions.

PHP RFCs:
Constructor behaviour of internal classes - PHP 7.0
Closure from callable - PHP 7.1
get_class() disallow null - in voting!
Consistent Callables - PHP 8.0?

Want help drafting an RFC - talk to me!


Designed by Matt McInerney of pixelspread.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 { 
    static function factory(array $config) { ... }
}

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

class MySQLi {
  function __construct($host, $username, $password,
        $dbname, $port) {...}
}  
  
//oops - forget to set password, dbname and port
$db = new MySQLi('localhost', 'db_user');
  
  

class S3Client { 
    static function factory(array $config) { ... }
}
  
// Spelling is hard
$config['passwrod'] = '12345';
S3Client::factory($config);

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

We're going to refactor some code!

Mock objects allow easier testing


function testFoo()
{
   $mock = Mockery::mock(Request::class);
      ->shouldReceive('getUri')
      ->andReturn('/search')
      ->getMock();
  
    foo($mockObject);
    // If foo doesn't call '$mock->getUri()'
    // the test will fail
}

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

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

Controller is now simpler, and more reasonable


class SearchController
{
// function search(Request $request, DataSource $dataSource) 
   function search(VariableMap $variableMap, DataSource $dataSource)
   {
      $searchTerms = $variableMap->getVariable('searchTerms');
      // $queryParams = $request->getQueryParams();
      // if (!array_key_exists('searchTerms', $queryParams)) {
      //    throw new ParamsMissingException("...");
      // }
      // $searchTerms = $queryParams['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...

Stub implementation of VariableMap

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

Fake implementation of DataSource


 interface DataSource {
     function searchForItems(array $searchOptions);
 }

 class EchoDataSource implements DataSource {
     function searchForItems(array $searchOptions) {
         return $searchOptions;
     }
 }

Testing the 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 the 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 application
  • Tests easier to write, as can use array implementation
  • Tests run faster
  • Tests are easier to reason about

Testing code

Front controller / dispatcher SearchController::search DataSource::searchForItems
Front controller / dispatcher interface Request { } getServerParams() getCookieParams() withCookieParams(...) getHeaders() getParsedBody() withQueryParams(...) getUploadedFiles() withUploadedFiles(...) getUri() getQueryParams() 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.”

We want to limit number of items


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

    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');  
        // $searchOptions = [];
        // $searchOptions['keywords'] = explode(',', $searchTerms);
        // $searchOptions['limite'] = 50; // LIMIT added
        $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);
    ...
}

Everything is a trade-off

Writing SOLID code leads to more typing

  • You get used to this.
  • Readability > quickness to type
  • Use a Dependency Injection Container to run your code github.com/rdlowrey/auryn
  • Type extraction done once - reusable across project
  • No one likes puns?

Compare SOLID code


function badCode($userId)
{
    global $session;
    $userId = $session->getUserId();

    return User::find($userId);
}

function solidCode(Session $session, UserRepo $userRepo) 
{
    $userId = $session->getUserId();

    return $userRepo->find($session->getUserId());
}

Compare SOLID code


badCode(); // This is easy to call

$session = new FileSession("/var/php/sessions", $request);
$userRepo = new UserSqlRepo($pdo);

solidCode($logger, $userRepo); // This is harder to call

Dependency injector container to the rescue


// Setup session once
$injector->alias(Session::class, FileSession::class);
$session = new FileSession("/var/log/appname", $request);
$injector->share($session);

// Setup a UserSqlRepo once
function createUserRepo(PDO $pdo) {
  return new UserSqlRepo($dbConnection);
}

$injector->delegate(UserRepo::class, 'createUserRepo');

// This runs the code and passes the 
// correct variables to the function.
$injector->execute('solidCode');

Designing good interfaces is hard

Trivial ones

  • Psr\Log\LoggerInterface
  • VariableMap
  • Reader and Writer

Business (aka domain) interfaces

  • Emerge from your application.
  • Probably not reusable.
  • Provide a large amount of value.

Avoid big interfaces



interface FileUploader {
    function upload($localFilename, $storageName);
}

Avoid big interfaces



interface FileTransfer {
    function upload($localFilename, $storageName);
    function download($localFilename, $storageName);
}
 

Avoid big interfaces


interface FileTransfer {
    function upload($localFilename, $storageName);
    function download($localFilename, $storageName);
    function fileExists($storageName);
}

And so it grows...

Breakup big interfaces


interface FileUploader {
    function upload($localFilename, $storageName);
}

interface FileDownloader {
    function download($localFilename, $storageName);
}

interface FileChecker {
    function fileExists($storageName);
}

Breakup big interfaces


interface FileTransfer extends FileUploader, FileDownloader { }

class S3FileManager implements 
    FileUploader, 
    FileDownloader, 
    FileChecker
{
   ...
}

function foo(FileTransfer $fileTransfer) {
  ...
}
  
foo(new S3FileManager(...));  

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

Simplicity Is Hard

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.

Fin

 
  • Use interfaces/classes with only the required methods
  • Use semantically meaningful types
  • If your code is boring simple, you're doing good
  • Use DIC/Auryn to do the boring wiring up
  • Questions?

Context objects


function backupDB(DB $liveDb, DB $archiveDb) {

}




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

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