A question of abstraction and exceptions, I think

I’d just like to run this past some of you fine folk, I’m sure the solution is obvious but despite a rewrite or two I always end up and the same place.

I have a connection object, it can(could?) use many different sources to perform general CRUD operations. Soap, Pdo, XML etc…

So, I made this object pretty much a decorator around an adaptor object, and this is where i think it gets messy.

As such, here’s some pseudo code (we all like code). :cool:


class Connection
{
    protected
        $adapter;
        
    public function __construct(Connection_Adapter_Interface $adapter){
        $this->adapter = $adapter;
    }
    
    public function login(Connection_Credentials $credentials){
        $this->adapter->login($credentials);
    }
    
}

interface Connection_Adapter_Interface
{
    public function login(Connection_Credentials $credentials);
}

class Connection_Adapter_Soap implements Connection_Adapter_Interface
{
    public function login(Connection_Credentials $credentials){
        #login n stuff
    }
}

class Connection_Adapter_Pdo implements Connection_Adapter_Interface
{
    public function login(Connection_Credentials $credentials){
        #login n stuff
    }
}

Firstly, I’m worried about the maintenance of it. If I want to add a new feature, I have to add a method to the Connection object then to the Interface, and then to every Adapter.

Messy.

Secondly, I’m worried about exceptions. Say for example the PDO object throws an exception in the adapter, I’ll catch it and throw a generic Adapter_Exception but I lose the backtrace.

Even worse, what if the Soap Client throws an exception? I cannot do the same because Soap likes throwing exceptions for everything! In this instance, it throws an exception for invalid login details.

This is context which I’d like to get back to the connection object, but how aware should it be of these exceptions. It feels wrong to be letting a Soap exception bubble up out of the Adapter context.

So, given this example, Pdo throws an exception, I catch it and re-throw as an Adapter exception, I catch this in the Connection object and do what? :frowning:

I’m struggling a little, and maybe because I just have me, myself and I to discuss this with.

This is where you come in…

Suggestions, comments, chastisement most welcome. :stuck_out_tongue:

The first point you made is typical of all systems - extending functionality in interfaces always means alot of code due to the classes which extend it.

The solution I can think of for that situation is composition. Get the data connection objects to have basic functions - inserting, updating, etc etc. Some extra things for anything you can think of.

Then for a new feature, simply write a class for that feature. That class holds the chosen data connection object and uses those basic step methods to add functionality.

The exceptions - unfortunately I can see the solutions using a lot of catches and throws.

Thanks for helping out Jake.

With regards to the interfaces, I think you’re right, after all that’s what they’re there for - to enforce a contract.

If i didn’t extend the implementing Adapters to suit, then system integrity would be affected. Which of course is why I’m using them after all!

The composition idea is pretty much what I’m planning to do, however, I’m trying not to go the full DAO route(Fig 9.3 & 9.4 scary the hell out of me!) as this would mean creating DAO objects for each Adapter.

I guess, in my head, I’d like to be able to keep the abstraction level with the adaptors to minimise the amount of code I’d have to write.

Something along the lines of…


class ProductDAO
{
    protected
        $connection;
    
    public function __construct(Connection $connection){
        $this->connection = $connection;
    }
    
    public function findProductById($id){
        try{
            $result = $this->connection->query('FIND Product WHERE Id = ' . (int)$id);
            if($result->products[0] instanceof Product){
                return $result->products[0];
            }
        }catch(Connection_Exception $exception){
            
        }
        return new NullProduct();
    }
}

Still, the exceptions do worry me somewhat.

Well if it helps, you may find it useful to expose this question to the PHP Application Design forum.

It seems like there’s a level of indirection too many here. How about this:


interface Connection
{
    public function login(Connection_Credentials $credentials);
}

class Connection_Adapter_Soap implements Connection
{
    public function login(Connection_Credentials $credentials){
        #login n stuff
    }
}

class Connection_Adapter_Pdo implements Connection
{
    public function login(Connection_Credentials $credentials){
        #login n stuff
    }
}

Regarding the exceptions, you can either catch them and throw some of your own, or you can let them through. Most of the time, when an exception occurs, you need to worry about the gritty details anyway, so wrapping them often doesn’t really make that much sense. If you think it does, you can still create your own exception and let this have the original exception as a property, thus allowing debugging.

In regards to the exception matter. From what I gather, you’re concerned that by letting all the exception bubble-up, you expose higher level code to lower-level exceptions, which can increase complexity with the higher level code when it comes to dealing with this wide range of potentially unexpected exception, or otherwise bubbling through to the global exception handler which usually results in script abortion - both of which are unideal.

You’re concern then with dealing with these exceptions within the lower-level Connection class, is that you fear that if you keep the exception handling simple and dumb, you’ll lose important information such as login failures and exception stacks, which higher level objects could use. On the other hand, if you make the exception handling smart in the Connection class, you introduce complexity, and worst of all, you have to make the Connection class aware of the all the implementations of the Connection_Adapter_Interface, which would reduce cohesion and create a bit of a maintenance problem.

The solution I’d propose then, is to make all implementations of Connection_Adapter_Interface responsible for catching their own exceptions, and converting them to a standardised exception which can be easily understood by the Connection class. For example…

So to extend on your example, it may look like this…


<?php

class ConnectionAdapterException extends RuntimeException {}

class Connection
{
    protected $adapter;
    
    public function __construct(IConnectionAdapter $adapter) {
        $this->adapter = $adapter;
    }
    
    public function login(ConnectionCredentials $credentials) {
        $this->adapter->login($credentials);
    }
    
}

interface IConnectionAdapter
{
    public function login(ConnectionCredentials $credentials);
}

class ConnectionAdapter_SOAP implements IConnectionAdapter
{
    public function login(ConnectionCredentials $credentials) {
        try {
            // Login attempt here
        } catch (Exception $e) {
            if(<invalid username>)
                throw new ConnectionAdapterException("Username is incorrect.", 001);
            else if(<invalid password>)
                throw new ConnectionAdapterException("Password is incorrect.", 002);
            else
                throw new ConnectionAdapterException("Unknown exception.", 003);
        }
    }
}

class ConnectionAdapter_PDO implements IConnectionAdapter
{
    public function login(ConnectionCredentials $credentials) {
        #login n stuff
    }
} 

Note the use of the exception code field. This can be handy for communicating the type of error to the Connection class, and even means you could leave out a custom message, instead defining the message in the Connection class rather rather than in each implementation of IConnectionAdapter. Do remember though that it’s just an example, and there are other ways of tackling this.

Also note that I changed your class and interface names to make them more idiomatic. You can change them back though if you wish of course.