POSTS

Why inheritance sucks: part deux

In the comments of my previous post - Why class inheritance sucks - I got a comment that shows some of the confusion with composition versus inheritance in the world of objects. Sandy said:

...I have cases where, say, if I'm building a CMS, I have a ContentObject class that has generalized methods to determine where it's been published, etc. If I have two types of content, Document and Event, I fail to see why extending ContentObject for each one is bad.

Now if I have another type, Author, and each Document needs an Author, it's a has-a relationship. Extending Author to get the author's name as part of the Document would be wrong. Instead it should be $document->author->getName();

But in the first case, if I need to implement a publishTo($page_id) method, I'd rather do it once in ContentObject, so I can change that implementation in one place. Am I missing something that makes that a bad idea?

That is a pretty standard case where it looks like the best route is Event extends ContentObject, especially with a publishTo() method that seems so universal. Given any number of factors, this might actually be the best route for any given project. I approach OOP from a flexibility standpoint with one of its biggest flex-points being loose coupling which in turn promotes reusability. I'm going to use Event as a hypothetical example.

In the normal case, you might want to add it to a page of other events, making sure its ordered correctly, etc., etc. What happens when it needs to update Google Calendar, shoot off an SMS to the administrators, and publish to a page particular ContentObject page? If that happens, Event->publishTo($page_id) gets:

  • relegated to a call from some other method such as Event->publish()
  • gets repurposed to the point that publishTo($page_id) doesn't adequately convey what it's actually doing
  • or worse it gets ignored when someone new writes the publish() code because they weren't aware of the inherited method

Assuming we want avoid all of these, a composite in the simplest form would look something like this:

class Event {
  ...  logic and such ...

  public function publish() {
    $content_object = new ContentObject();
    $content_object->publish_data('some-page', $this);
    $google_cal = new GoogleCal();
    $google_cal->add_event($this);
    $sms = new SMSSender('admins@example.mobi');
    $sms->send_event($this);
  }
}

This gives us the composition and moves the logic of saving out to other objects, but isn't very flexible. Every new publishing type requires a change to Event::publish(). This is a place where the Visitor Pattern proves useful. The publish() method becomes:

public function publish(EventPublisher $publisher) {
  $publisher->publishEvent($this);
}

With that, you completely externalize the act of publishing an Event to any object that implements the EventPublisher interface. Now any object can be used for publishing an Event without any changes required to the actual Event code. Your commit messages will be shorter and less code will have to be written which should both translate into less of a likelihood for bugs to get introduced (not that they'd make it past that stellar TDD test suite you've coded ;-)). And you can still share your ContentObject::publishTo() functionality:

class ContentObject implements EventPublisher, DocumentPublisher
{
  public function publishTo($page_id) {
    ... do publishing ...
  }

  public function publishEvent(Event $event) {
    $this->name = $event->name;
    ... etc., etc. ...
    $this->publishTo('some-page');
  }

  public function publishDocument(Document $document) {
    $this->name = $document->filename;
    ...  etc., etc. ...
    $this->publishTo('some-page');
  }
}

The careful observer will notice that while ContentObjectuses the property $name in the above example, the property that Event and Document use is different. With the above functionality you don't end up having to shoe-horn property names into specific schemas because of inheritance allowing the best practice to dominate the code instead of the best choice.

And now the original "what if" problem. Publishing all of the code might look something this:

$publisher = new MultipleEventPublisher(array(
  new ContentObject('calendar-page'),
  new GoogleCal('my-events'),
  new EventSMSPublisher('8885554321@example.mobi')
));
$event->publish($publisher);

Now instead of Event being a ContentObject, it uses one. The shift paradigm is subtle, but something that can help produce better, more flexible code.