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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With