Let's talk about interfaces...
Lots of developers struggle to grasp the concept of the "interface" in PHP. But once they get the idea and understand how it works, they then struggle with a new problem: writing interfaces that are too complicated.
This might seem like a contradiction in terms: after all, we want to create comprehensiveinterfaces that describe the behavior of a particular object type. And since we don't generally want to extend the interface, shouldn't the interface we write be complete?
Yes, and no. Let me explain.
The overbearing interface
Let's take a look at a common database interface that many of my students come up with when they're tasked with writing a database layer:
<?php
interface MyDBObject {
public function connect($host, $db, $user, $pass);
public function query($query, array $params);
public function prepare($query);
public function beginTransaction();
public function commit();
public function rollback();
}
This interface looks good: it handles the connection and the connection-level functionality without adding in query-specific behaviors. That's what we want.
But then I pose this question to them: what happens when you encounter a database that doesn't support transactions?
Most modern databases are ACID compliant. But some still aren't. And what happens when you have to implement one of those?
Their answer is often to simply leave the transaction-aware methods blank. But this is a problem, because the Interface Segregation Principle says that no object should be required to implement a method it doesn't use.
Oops.
Fixing the interface
PHP doesn't require that we use a single interface for each object we create. In fact, the beauty of PHP interfaces is that we can implement lots of them, even though we don't have the ability to do multiple inheritance.
So, to solve this problem we can break this interface into *two separate interfaces*:
<?php
interface MyDBObject {
public function connect($host, $db, $user, $pass);
public function query($query, array $params);
public function prepare($query);
}
interface TransactionAware extends MyDBObject {
public function beginTransaction();
public function commit();
public function rollback();
}
There, now we have two interfaces we can use. Great! This lets us implement only those methods we need for older, non-ACID compliant databases, while giving us the methods we need for modern database applications.
Avoiding "God" objects
Another common error is the creation of an interface that does *everything*. Even if a behavior is in scope for a particular object, that doesn't mean we should necesarily implement it in this object.
Let me explain.
The Imagick library in PHP has a single object that has literally hundreds of public methods. The object interface is huge. I have it on slides in a presentation and four slides isn't enough to show the whole interface. This interface is behemoth.
This is a classic "God" object. The object is enormous, and probably does far more than it should.
My general rule of thumb is that an object should have no more than six to eight public methods or it is probably overreaching. Of course, this is not a hard and fast rule: there are many objects with a single role that have ten or twelve methods. But it is a good rule for the start of the evaluation process, and for taking a hard look at your object.
The bottom line
Creating an overbearing object or an inflexible interface is a good way to end up with objects that describe behaviors that are out of scope or not even implemented. We want to avoid both. Discrete, clean interfaces are preferable in all cases, and that's what we should strive to achieve.
No comments:
Post a Comment