Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Inversion and interfaces

Tags:

oop

php

interface

Im learning OOP, specifically interfaces. I'm also trying to learn SOLID, in this case the D.

From this site, the initial program implements a 'concretion' - in this case PDFBook is typehinted to be passed to the constructor. Later, this typehint is changed to a general EBook interface. Anything that implements this interface is accepted. Makes sense in this case.

However, even when coding to an interface, I find that there are often extra methods not defined in the interface, but are unique to that concretion. In this case, PDFBook may have a method doDPFOnlyThing not defined in any of the other classes that implement the EBook interface.

If I pass a PDFBook object to myFunc() which type-hints the EBook interface, from what I understand, if I use only the methods defined in the interface - read() - then this would adhere to DIP yes? As in, anything passed into myFunc() implementing the interface will be able to have their read() method called as it adheres to the interface contract.

myFunc(Ebook $book) {

    $book->read();
}

What if myFunc() has to use doDPFOnlyThing() available only in the PDFBook class? I assume this would add dependencies since this method only exists in the PDFBook concretion?

myFunc(Ebook $book) {

    $book->doDPFOnlyThing();
}

What is the better thing to do in this case?

like image 583
myol Avatar asked Jul 15 '15 17:07

myol


1 Answers

Whereas typehinting to interfaces over implementations is helpful in reducing coupling, it can also be a pain when trying to write generalized interfaces. As you said, it would be nice to use methods you know are there.

That said, you effectively have two different methods there. When calling myFunc and passing an EBook you should definitely only rely on methods in the interface. If a method needs to call doPDFOnlyThing and it relies on an EBook instead of a PDFBook, well that will violate the principle.

One thing you could do is:

public myFunc(EBook $book)
{
    $book->read();
}

public myPDFFunc(PDFBook $book)
{
    $book->read(); //Still valid from EBook contract
    $book->doPDFOnlyThing();
}

Although this will probably work, it is a dirty fix you will likely end up violating Open/Closed principle in the process since you will be coming back and editing the class. (Eventually the customer will want a KindleBook which has a doKindleOnlyThing method in it.)

So how does one get around this problem?

You problem of wanting to type-hint to one interface but using methods from an implementation is like having your cake and eating it too...

To solve this you will need to abstract your design more. Let's use the example that you are making a client that will read books in various formats all derived from the EBook interface implemented as the base class MyEBook. Let's start with the code below:

interface EBook
{
    public function read();
}

interface PDFBook extends EBook
{
    public function doPDFOnlyThing();
}

class MyEBook implements EBook
{
    public function read()
    {
        echo 'reading from a ' . get_class($this);
    }
}

class MyPDFBook extends MyEBook implements PDFBook
{
    public function read()
    {
        //you only need to override this method
        //if needed, otherwise you can leave it
        //out and default to the parent class
        //implementation.
        parent::read();
    }

    public function doPDFOnlyThing()
    {
        echo 'doing what a PDF does while...';
    }
}

The EBook interface contracts the read() method and the PDFBook interface extends EBook and adds the doPDFOnlyThing() method to the contract. The concrete implementations MyEBook and MyPDFBook will each use their respective interfaces.

Next we need to build some handler classes that can take any book and perform an action of some kind on them. We will use a naming convention here where all handler classes have the suffix of Reader behind them. So the handler for MyPDFBook would be MyPDFBookReader. This convention will be handy a little later.

We will start with an abstract class that can accept any implementation of EBook and store it in a class property. The class will also expect that all children classes implement a method called readBook().

abstract class GenericBookReader
{
    protected $book;

    public function __construct(EBook $book)
    {
        $this->book = $book;
    }

    abstract public function readBook();
}

Now that we have the abstract class that can accept any EBook we can build the specific implementations that will type-hint to the particular interface class - e.g. PDFBook or EBook.

class MyBookReader extends GenericBookReader
{
    public function __construct(EBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        $this->book->read();
    }
}

class MyPDFBookReader extends GenericBookReader
{
    public function __construct(PDFBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        //You are safe to use PDFBook methods here
        //because you have a guarantee they are available
        $this->book->doPDFOnlyThing();
        $this->book->read();
    }
}

Both of these concrete implementations simply send the given object in $book to the parent constructor which will then cache it in the $this->book property. Any operations that need to be done to any book when being initialized can be done in GenericBookReader and all classes will use the new method instead of having to be individually updated. Of course if a particular class needs some special initialization that can be done in their own constructors instead of the parent constructor.

At this point you have abstracted the EBook and PDFBook away from each other in their own handlers instead of in a single class. This is a step forward because now in the readBook() method of the MyPDFBookReader class you have a guarantee that doPDFOnlyThing() is available for use.

Now to glue all of this together you need a client that reads books. The client should be able to accept any EBook, determine the type of book it is, create the appropriate Reader class and then call the readBook() method. The naming convention works nice here since we can build class names dynamically.

class BookClient
{
    public function readBook(EBook $book)
    {
        //Get the class name of $book
        $name = get_class($book);

        //Make the 'reader' class name and see if it exists
        $readerClass = $name . 'Reader';
        if (class_exists($readerClass))
        {
            //Class exists - yay!  Read the book...
            $reader = new $readerClass($book);
            $reader->readBook();
        }
    }
}

Here's the usage of these classes:

$client = new BookClient();
$client->readBook(new MyEBook());       //prints: reading from a MyBook
$client->readBook(new MyPDFBook());     //prints: doing what a PDF does while...reading from a MyPDFBook

All of this might look complex just to do a simple call to readBook() but the flexibility gained is worth it. For example, later on the customer says "Where's the support for Kindle books?" and you say "Coming right up!"

interface KindleBook extends EBook
{
    public function doKindleOnlyThing();
}

class MyKindleBook extends MyEBook implements KindleBook
{
    public function doKindleOnlyThing()
    {
        echo 'waiting FOREVER for the stupid menu to start...';
    }
}

class MyKindleBookReader extends GenericBookReader
{
    public function __construct(KindleBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        //You are safe to use KindleBook methods here
        //because you have a guarantee they are available
        $this->book->doKindleOnlyThing();
        $this->book->read();
    }
}

Example usage extended:

$client = new BookClient();
$client->readBook(new MyEBook());       //prints: reading from a MyBook
$client->readBook(new MyPDFBook());     //prints: doing what a PDF does while...reading from a MyPDFBook
$client->readBook(new MyKindleBook());  //prints: waiting FOREVER for the stupid menu to start...reading from a MyKindleBook

This particular setup using abstraction stands up well to Open/Closed principle. You had to add some code, but you didn't change any of the existing implementation - not even the client!

Hopefully this provided an extra angle to view your issue from. Look at the way you want to setup your implementations and start looking at what can be abstracted away. Sometimes it is best to keep objects in the dark about each other and have special handlers that work with them. In this example none of the books need to care how the other one work. Therefore a single class that would accept any EBook but has methods that work with specific children implementations of that interface ends up being a code smell.

Hope that helps. Below is the complete example code to copy and paste to try out yourself.

<?php

interface EBook
{
    public function read();
}

interface PDFBook extends EBook
{
    public function doPDFOnlyThing();
}

interface KindleBook extends EBook
{
    public function doKindleOnlyThing();
}

class MyEBook implements EBook
{
    public function read()
    {
        echo 'reading from a ' . get_class($this);
    }
}

class MyPDFBook extends MyEBook implements PDFBook
{
    public function read()
    {
        //you only need to override this method
        //if needed, otherwise you can leave it
        //out and default to the parent class
        //implementation.
        parent::read();
    }

    public function doPDFOnlyThing()
    {
        echo 'doing what a PDF does while...';
    }
}

class MyKindleBook extends MyEBook implements KindleBook
{
    public function doKindleOnlyThing()
    {
        echo 'waiting FOREVER for the stupid menu to start...';
    }
}

abstract class GenericBookReader
{
    protected $book;

    public function __construct(EBook $book)
    {
        $this->book = $book;
    }

    abstract public function readBook();
}

class MyBookReader extends GenericBookReader
{
    public function __construct(EBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        $this->book->read();
    }
}

class MyPDFBookReader extends GenericBookReader
{
    public function __construct(PDFBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        //You are safe to use PDFBook methods here
        //because you have a guarantee they are available
        $this->book->doPDFOnlyThing();
        $this->book->read();
    }
}

class MyKindleBookReader extends GenericBookReader
{
    public function __construct(KindleBook $book)
    {
        parent::__construct($book);
    }

    public function readBook()
    {
        //You are safe to use KindleBook methods here
        //because you have a guarantee they are available
        $this->book->doKindleOnlyThing();
        $this->book->read();
    }
}

class BookClient
{
    public function readBook(EBook $book)
    {
        //Get the class name of $book
        $name = get_class($book);

        //Make the 'reader' class name and see if it exists
        $readerClass = $name . 'Reader';
        if (class_exists($readerClass))
        {
            //Class exists - yay!  Read the book...
            $reader = new $readerClass($book);
            $reader->readBook();
        }
    }
}
$client = new BookClient();
$client->readBook(new MyEBook());       //prints: reading from a MyBook
$client->readBook(new MyPDFBook());     //prints: doing what a PDF does while...reading from a MyPDFBook
$client->readBook(new MyKindleBook());  //prints: waiting FOREVER for the stupid menu to start...reading from a MyKindleBook
like image 70
Crackertastic Avatar answered Sep 27 '22 17:09

Crackertastic