Interface segregation

The forgotten i in SOLID

@MrDanack

Slides are at docs.basereality.com

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

What is interface segregation type specialization?

 
“many small client-specific interfaces types are better than one big 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)
  {
     //@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);
    }
}

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


// This is easy to call
badCode();

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

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

Insert one hour talk about dependency injection here

Don't have time to do this properly

https://github.com/danack/example

https://github.com/Danack/example/blob/master/readme_auryn_slim.md

Designing good interfaces is hard

What is type specialization?

 
“many small types are better than one big type, and naming your types makes them easier to reason about

Trivial ones

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

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

Boundary crossing types



$emailSender->send(
    $user->getName(),
    $user->getEmailAddress()
);

$emailSender->send(
    null,
    $signupFormData->getEmailAddress()
);

Extract a type



interface EmailRecipient {
  public function getName(): ?string;
  public function getEmailAddress(): string;
}

Extract a type



class UserEmailRecipient implements EmailRecipient {

  private $user;

  function __construct(User $user) {
    $this->user = $user;
  }

  public function getName(): ?string {
    return $this->user->getName();
  }

  public function getEmailAddress(): string {
    return $this->user->getEmailAddress();
  }
}

Extract a type



class SignupEmailRecipient implements EmailRecipient {

  private $formData;

  function __construct(SignupFormData $signupFormData) {
   $this->formData = $signupFormData;
  }

  public function getName(): ?string {
    return null;
  }

  public function getEmailAddress(): string {
    return $this->formData->getEmailAddress();
  }
};

Boundary crossing types



$emailSender->send(
  new UserEmailRecipient($user)
);


$emailSender->send(
  new SignupEmailRecipient($signupFormData)
);

Extract and name input parameters type


// Gets a list of articles
function getArticles(
  VarMap $varMap,
  ArticlesRepo $articlesRepo
) {

  $limit = v::intVal()->max(20)
    ->validate($varMap->get('limit'));

  $after = v::oneOf(
    v::nullType(),
    v::intVal()->max(10000)
  )->validate($varMap->get('after'));

  return $articlesRepo->getArticles($limit, $after);
}

Extract and name input parameters type


// Gets a list of articles
function getArticles(
  VarMap $varMap,
  ArticlesRepo $articlesRepo
) {

  $getArticlesParams = GetArticlesParams::create($varMap);

  return $articlesRepo->getArticles($getArticlesParams);
}

danack/params on packagist and github.

Avoid big interfaces


interface Cache {
  function get($key);
  function set($key, $value);
  function setTtl($key, $value, $ttl);

  function delete($key);
  function deleteItems(array $keys);

  function clear();
  function clearKeys(array $keys);
  function clearPattern($regex);

  function setNx($key, callable $fn);
  function setNxTtl($key, $ttl, $value);
}

Breakup big interfaces


interface CacheBasic {
    function get($key);
    function set($key, $value);
}

interface CacheDelete {
    function delete($key);
}

interface CacheClear {
    function clear();
}

Magic to make it work


interface BasicAndDeleteCache extends CacheBasic, CacheDelete { }

class RedisCache implements
    CacheBasic,
    CacheDelete
{ ... }

function foo(BasicAndDeleteCache $fileTransfer) {...}

class RedisCache extends RedisCache
    implements BasicAndDeleteCache {
  // empty - just used for declaring interface
}
  
foo(new RedisCache(...));

Union types RFC if/when passed will make this easier to do.

Don't wrap other services


interface DB {

  function autocommit(bool $mode) : bool;
  function change_user($user, $password, $database) : bool;
  function character_set_name() : string;
  function close(void) : bool;
  function commit(int $flags = 0 , string $name = null ) : bool;
  // and all the others from MySQLi
}

class MySQLDB extends MySQLi implements DB {
  ...
}


Don't wrap other services

Business / domain types

  • Get discovered when making an application.
  • Probably not reusable.
  • Provide a large amount of value.
  • Cover just a small concept.

Abstraction layer for database access


interface PurchaseFeeRepo
{
  public function findPurchaseThatNeedsFeeFetching(): ?Purchase;

  public function saveFee(Purchase $purchase, FeeData $feeData);

  public function saveFeeProcessingFailed(Purchase $purchase);
}

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

“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

 

https://joind.in/talk/729af

Twitter: @MrDanack

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