I am writing a program which is a similar to Ruby's Active Record Migrations, in which that every migration has both an "Up" and "Down" in terms of creating a change to the database, "Up" meaning bring the database forward in time, and "Down" reversing that change.
My program has two main functions to achieve its use-cases
- Get a list of scripts off the filesystem
- Execute the appropriate migrations on the database
The "Up" and "Down" migrations are stored in their own separate files, but they match with a similar naming scheme
- 2014000000000001_CreateTable_up.sql
- 2014000000000001_CreateTable_down.sql
There is a 1:1 correlation between up and down scripts. Having just an up without the corresponding down script is not valid (and vise verse).
I have two classes in my project to represent these filesystem files.
public sealed class UpScript // (and DownScript, which looks identical)
{
public UpScript(long version, string name, string content)
{
Content = content;
Version = version;
Name = name;
}
public long Version { get; private set; }
public string Name { get; private set; }
public string Content { get; private set; }
}
The reason that I did not make a single class called Script
is I didn't want to create ambiguity about the purpose of a specific Script
instance. UpScript
and DownScript
classes should not be used interchangeability as they do not conform to the Liskov Substitution Principle.
The problem that I have here is a few parts of code that are dealing with both either UpScript
or DownScript
instances look almost identical. How do I reduce the duplication without losing the expressiveness of having different classes?
My current ideas with concerns are:
Abstract base class
public abstract class Script
{
protected Script(long version, string name, content)
{
// code
}
// properties
}
public sealed UpScript : Script
{
public UpScript(long version, string name, string content)
: this(version, name, content)
{}
}
public sealed DownScript : Script
{
public DownScript(long version, string name, string content)
: this(version, name, content)
{}
}
This reduces the redundant code in the two classes, however, code that creates and consumes is still very duplicated.
Single class with Direction
property
public enum Direction { Up, Down }
public sealed class Script
{
protected Script(Direction direction, long version, string name, string content)
{
// ...
}
public Direction Direction { get; private set; }
// ...
}
This code reduces the duplication, however, it adds onus on consumes to ensure the proper script is passed to it and it creates ambiguity if you ignore the Direction
property.
Single class with two Content
properties
public sealed class Script
{
protected Script(long version, string name, string upContent, string downContent)
{
// ...
}
public string UpContent { get; private set; }
public string DownContent { get; private set; }
}
This reduces duplication and is not ambiguous, however, the usage of the program is to either run the up script, or run the down scripts. With this scheme, the code that gathers and creates these Script
instances is doing twice as much work, I don't consider not doing this being premature optimization because if the user wants to do an up migration, there's no point is looking at down scripts.
With all that said I may be looking at the wrong problem to solve entirely.
If more information would be helpful in forming a suggestion, you can view ScriptMigrations on GitHub