views:

55

answers:

5

I have a couple different custom backup programs and I want to combine them into a single one. Doing so, I'm working on my OO design to help keep the driving program simple and easy to follow by abstracting my server and database information.

I'm using two different hosts (Discount ASP.net and Rackspace). Both of them go about their backups in a slightly different manner.

I've tried looking into a few different approaches, but here is the route that I think makes most sense.

I have my base class:

public abstract class DBServer
{
    public List<Database> Databases { get; set; }
    public abstract bool MakeBackupCall(Database d);
}

and then two derived classes:

public class DASPServer : DBServer
{
    public string APIKey { get; set; }

    public override bool MakeBackupCall(Database d)
    {
         // do some stuff
         return true;
    }
}

public class RackspaceServer : DBServer
{
    public override bool MakeBackupCall(Database d)
    {
        // do some different stuff
        return true;
    }
}

The problem comes in with my related object Database. Since the backup processes are different for each host, I require different information for the databases. For example, for Discount ASP.net I need a version (2000, 2005, 2008) for the database so I know which of their web services to call. For Rackspace, I need the external server name, database username and passwords to create a connection string. Because of this, I tried creating a database object with the following hierarchy.

public abstract class Database
{
    public string Name { get; set; }     
}

public class DASPDatabase : Database
{
    public string Version { get; set; }
}

public class RackspaceDatabase : Database
{
    public string ServerName { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }

    protected string ConnectionString
    {
        get { return string.Format("Data Source={0};Network Library=;Connection Timeout=30;Packet Size=4096;Integrated Security=no;Encrypt=no;Initial Catalog={1};User ID={2};Password={3}", ServerName, Name, UserName, Password); }
    }
}

What I want to do, is ensure that my DASPServer instance always gets instances of DASPDatabases, and the same with Rackspace. Alternatively, I would like a smack in the head if I am going about this in the wrong direction.

Many thanks in advance

A: 

Just check the type at runtime and throw an ArgumentException if the passed database is of the wrong type.

Eric Petroelje
That sounds like a hack since the parameter specification will show Database, but secretly I require DASPDatabase?
senloe
A: 

I'm not aware of any way to, at compile time, ensure that MakeBackupCall() uses the correct Database sub-class. At run-time, however, you can check that the passed parameter is of the correct sub-type.

TreDubZedd
+3  A: 

Make your DBServer class generic, then specify the actual Database type for the implementing classes.

public abstract class DBServer<TDatabase> where TDatabase : Database
{
    public List<TDatabase> Databases { get; set; }
    public abstract bool MakeBackupCall( TDatabase d );
}

public class DASPServer : DBServer<DASPDatabase>
{
    public string APIKey { get; set; }

    public override bool MakeBackupCall( DASPDatabase d )
    {
        // do some stuff
        return true;
    }
}

public class RackspaceServer : DBServer<RackspaceDatabase>
{
    public override bool MakeBackupCall( RackspaceDatabase d )
    {
        // do some different stuff
        return true;
    }
}    
Jerod Houghtelling
brilliant!! Thank you. I should spend more time looking at generics. This is the second time today they've helped me out.
senloe
the problem I'm running into with this is I can't instantiate my object as DBServer myserver = new DASPServer() so I can perform the methods on a generic DBServer object, not caring which specific derived type it is. Any way around that?
senloe
That situation does become a little more tricky. You may have to duplicate the interface so that there is generic and non-generic versions.
Jerod Houghtelling
+1  A: 

You could hide the base implementation of MakeBackupCall :

public new bool MakeBackupCall(RackspaceDatabase d)
{
    // Do something
}

That way, if the method is called on a RackspaceServer variable, the compiler will enforce the type of the paramter to be BackspaceDatabase. But of course it won't work if it is called on a DBServer variable, since the base implementation would be called...

An acceptable compromise would be to do the actual implementation in a protected virtual method that takes a Database parameter :

public abstract class DBServer
{
    public List<Database> Databases { get; set; }

    // Non-virtual; the actual implementation is not done in that method anyway
    public bool MakeBackupCall(Database d)
    {
        return MakeBackupCallCore(d);
    }

    // Actual implementation goes there
    protected abstract MakeBackupCallCore(Database d);
}

public class RackspaceServer : DBServer
{
    // Hide the base method
    public new bool MakeBackupCall(BackspaceDatabase d)
    {
        return MakeBackupCallCore(d);
    }

    // Do the actual implementation here, and ensure it is really a BackspaceDatabase
    protected virtual bool MakeBackupCallCore(Database d)
    {
        BackspaceDatabase bd = d as BackspaceDatabase;
        if (bd == null)
            throw new ArgumentException("d must be a BackspaceDatabase");

        // do some different stuff with bd
        return true;
    }
}

That way, you can enforce the type at compile time when you explicitly use a BackspaceServer, and still use the derived implementation when you use a DbServer without knowing its actual type.

A similar approach is used in ADO.NET classes : for instance, SqlConnection.CreateCommand() returns a SqlCommand, even though the base DbConnection.CreateCommand() returns a DbCommand. The base method is hidden, and the actual implementation is protected abstract (CreateDbCommand)

Thomas Levesque
A: 

I would be inclined to abstract out the data access layer altogether - put it behind an interface (basically it's Dependency Inversion), then develop data access implementations that suit the backend you're dealing with. Th esituation you end up with is one where you simpoly drop a new concrete data access implementation into the bin directory (and appropriate config) and you're away - you don't have to recompile and redeploy the whole system.

Depending on the specifics of the dependancies you're dealing with you could even combine the two implementations into the same assembly. In this case it's more of a Strategy Pattern.

In both cases you can select the concrete data access implementation by config.

Adrian K