I was wondering whether using (almost) empty derived classes to give additional semantics to a class hierarchy was a good idea (or practice) ?
Because a (not so short) example is always better than a long speech, let's assume we're writing a class hierarchy for SQL generation. You want to represent a data manipulation command (INSERT, UPDATE or DELETE), so you have two alternatives :
First version, with switch statement
enum CommandType {
Insert,
Update,
Delete
}
class SqlCommand {
String TableName { get; }
String SchemaName { get; }
CommandType CommandType { get; }
IEnumerable<string> Columns { get; }
// ... and so on
}
class SqlGenerator {
string GenerateSQLFor(SqlCommand command)
{
switch (command.CommandType)
{
case CommandType.Insert:
return generateInsertCommand(command);
case CommandType.Update:
return generateUpdateCommand(command);
case CommandType.Delete:
return generateDeleteCommand(command);
default:
throw new NotSupportedException();
}
}
}
Second version with VisitorPattern (Note that the question is NOT about whether the Visitor pattern should be used or adding new operations)
abstract class SqlCommand {
String TableName { get; }
String SchemaName { get; }
IEnumerable<string> Columns { get; }
// ... and so on
abstract void Accept(SqlCommandVisitor visitor);
}
class InsertCommand : SqlCommand
{
overrides void Accept(SqlCommandVisitor visitor)
{
visitor.VisitInsertCommand(this);
}
}
class DeleteCommand : SqlCommand
{
overrides void Accept(SqlCommandVisitor visitor)
{
visitor.VisitDeleteCommand(this);
}
}
class SqlCommandVisitor {
void InitStringBuffer() { ... }
string GetStringBuffer() { ... }
string GenerateSQLFor(SqlCommand command)
{
InitStringBuffer();
command.Accept(this);
return GetStringBuffer();
}
void Visit(SqlCommand command) { ... }
void VisitInsertCommand(InsertCommand command) { ... }
void VisitDeleteCommand(DeleteCommand command) { ... }
void VisitUpdateCommand(UpdateCommand command) { ... }
}
With both examples we achieve the same result, but :
- In the first version, my code feels more DRY, even though polymorphism is generally preferable over a switch statement.
- In the second version, I feel like I'm needlessly deriving SqlCommand just so that the code carries more meaning.
Another similar case is deriving a class from a generic class, just to give additionnal meaning to the collection, eg :
class CustomerList : List<Customer> { }
I'm left off wondering whether this is a good idea. Are there any pitfalls to doing so ?
I personally prefer the second version exactly because it adds meaning to the code, and helps me when I read my code later... But I clearly lack the experience to see any downside to this pattern.
InsertCommand
object and instead receives aDeleteCommand
, you get that tested at compile time. I'm not sure it would be as easy if you had to evaluate theCommandType
property.