This is part four of my refactoring series, you find the index here
Part one was a short success story, it can be found here.
Part two was a introduction to refactoring, available here.
In part three I talked about the requirements, you can read it here
You can read about the very basic refactoring methods in Part four of my series.
As I said in the last post, I’ll refactor some random open source projects, in this post it’s how updating the database schema is handled in Personal Task Manager.
If you have any suggestions what project to refactor, please leave a comment.
What the update method was before
public static void UpdateDataBase()
{
bool findNextUpdate = true;
while (findNextUpdate)
{
Configuration oldVersion = ConfigurationHelper.GetConfiguration(ConfigurationKey.DataBaseVersion);
if (UpdateFromV00ToV09(oldVersion))
continue;
if (UpdateFromV09ToV091(oldVersion))
continue;
// SNIP (10 more updates)
if (UpdateFromV_0_9_10_ToV_1_0_0(oldVersion))
continue;
if (UpdateFromV_1_0_0ToV_1_0_1(oldVersion))
continue;
if (UpdateFromV_1_0_1ToV_1_0_2(oldVersion))
continue;
findNextUpdate = false;
RegisterAddins();
}
}
Wrong:
As you can see, for every update you create, you have to touch this method, though it’s not complicated, you might simply forget it.
The continues are somewhat useless as applying a update results in the next version.
The call to RegisterAddins has nothing to do with the db updates, it should thereby be removed.
The version is a string, contained in the oldVersion variable as oldVersion.Value.ToString().
What the updates were before
private static bool UpdateFromV_1_0_1ToV_1_0_2(Configuration oldVersion)
{
if (string.Compare(oldVersion.Value.ToString().Trim(), "1.0.1") == 0)
{
try
{
DbHelper.AddColumn("ApplicationsLog", "Caption", "VarChar(255)");
ConfigurationHelper.SaveConfiguration(new Configuration(ConfigurationKey.DataBaseVersion, "1.0.2"));
return true;
}
catch (OleDbException ex)
{
Logger.WriteException(ex);
return false;
}
}
return false;
}
private static bool UpdateFromV_0_9_10_ToV_1_0_0(Configuration oldVersion)
{
if (string.Compare(oldVersion.Value.ToString().Trim(), "0.9.10") == 0)
{
try
{
DbHelper.AddColumn("Tasks", "Hidden", "Bit");
DbHelper.AddColumn("Tasks", "Priority", "Integer");
DbHelper.AddColumn("Tasks", "Notes", "VarChar(255)");
ConfigurationHelper.SaveConfiguration(new Configuration(ConfigurationKey.DataBaseVersion, "1.0.0"));
return true;
}
catch (OleDbException ex)
{
Logger.WriteException(ex);
return false;
}
}
return false;
}
Wrong:
The version string, actually object, is always manipulated in the same way
The try / catch block is always the same
Both violates DRY (don’t repeat yourself)
What the updates could look like
public abstract class DbUpdate
{
public bool CanUpgradeFrom(Version currentVersion)
{
return currentVersion == UpgradeableVersion;
}
public abstract Version UpgradeableVersion { get; }
public abstract Version NewVersion { get; }
public bool Apply()
{
try
{
ApplyDbUpdate();
return true;
}
catch (Exception ex)
{
Logger.WriteException(ex);
return false;
}
}
protected abstract void ApplyDbUpdate();
public override string ToString()
{
return UpgradeableVersion + " To " + NewVersion;
}
}
Changes:
The Version strings were compatible with the Version class, therefore the Version class is now used.
The Repeative code is encapsulated in the CanUpgradeFrom and Apply methods. DRY is now followed.
The ToString override is simply for convenience, if we have those DbUpdate instances in a collection and look at them in the debugger, you don’t have to step into them.
Encapsulating the update method into its own class
public class DbUpdater
{
public void ApplyUpdatesAsNeeded()
{
var updates = OrderByVersion(GatherAllUpdates());
var currentVersion = GetCurrentVersion();
if (IsLatestVersion(currentVersion, updates))
{
return;
}
foreach (var update in updates)
{
if (!update.CanUpgradeFrom(currentVersion))
{
continue;
}
if (!update.Apply())
break;
currentVersion = update.NewVersion;
SaveNewDbVersion(currentVersion);
}
}
private bool IsLatestVersion(Version currentVersion, IList<DbUpdate> updates)
{
if (updates.Count == 0)
{
return true;
}
return updates[updates.Count - 1].NewVersion == currentVersion;
}
private void SaveNewDbVersion(Version version)
{
ConfigurationHelper.SaveConfiguration(new Configuration(ConfigurationKey.DataBaseVersion, version.ToString()));
}
private IList<DbUpdate> OrderByVersion(IEnumerable<DbUpdate> updates)
{
var result = new List<DbUpdate>(updates);
result.Sort(delegate(DbUpdate a, DbUpdate b)
{
return Comparer<Version>.Default.Compare(a.NewVersion, b.NewVersion);
});
return result;
}
private Version GetCurrentVersion()
{
var versionString = ConfigurationHelper.GetConfiguration(ConfigurationKey.DataBaseVersion)
.Value.ToString().Trim();
return new Version(versionString);
}
private IList<DbUpdate> GatherAllUpdates()
{
var updates = new List<DbUpdate>();
var types = typeof (DbUpdate).Assembly.GetTypes();
foreach (var type in types)
{
if (!type.IsSubclassOf(typeof(DbUpdate)))
{
continue;
}
updates.Add((DbUpdate)Activator.CreateInstance(type));
}
return updates;
}
}
Changes:
If the update failed, no other updates will be applied as the version is still the same and it would thereby be useless.
With the IsLatestVersion method the load time was reduced, although the updates are obtained via reflection.
The use of reflection was necessary as no IoC container was used and I didn’t want to introduce one for just this case nor did I want to hardcode each update.
Individual updates in their new form
public class Update101To102 : DbUpdate
{
public override Version UpgradeableVersion
{
get { return new Version(1, 0, 1); }
}
public override Version NewVersion
{
get { return new Version(1, 0, 2); }
}
protected override void ApplyDbUpdate()
{
DbHelper.AddColumn("ApplicationsLog", "Caption", "VarChar(255)");
}
}
public class Update0910To100 : DbUpdate
{
public override Version UpgradeableVersion
{
get { return new Version(0, 9, 10); }
}
public override Version NewVersion
{
get { return new Version(1, 0, 0); }
}
protected override void ApplyDbUpdate()
{
DbHelper.AddColumn("Tasks", "Hidden", "Bit");
DbHelper.AddColumn("Tasks", "Priority", "Integer");
DbHelper.AddColumn("Tasks", "Notes", "VarChar(255)");
}
}
The Result:
The db updating part of the application now follows the open closed principle, you don’t have to touch any of the written code, you just add new.
The DbUpdate class follows the single responsibility principle, you only have to change the DbUpdate implementation that represents the current update if you make changes to the db schema.
If you take single responsibility a little broader as in “updating the schema”, the DbUpdater class also follows that principle. (It gathers the updates, orders and applies them, if anything of that changes it has to change too, thereby, if you take the srp strict, it actually has three responsibilities.)
You can’t forget to add the new update as you don’t have to add it anywhere, you just create the new class.
Typos are much less likely as the version is now represented with the Version class.
Updating the db schema is child’s play now.
By the way, the updating method that was used before is a typical example of organically growth, you start by adding the first update method, later a second comes in an you “just add a conditional” but before you realize it, you have an abomination waiting for copy and paste errors. Try to refactor those methods the very moment you want to add this conditional and it’s likely that more will follow. Doing it before might violate YAGNI (You Ain’t Gonna Need It) and / or KISS(Keep It Simple Stupid)
A word to testing: I didn’t create any tests as there was no way to do it without really great effort. Almost everything in that application is static thereby stubbing / mocking alone would have required some serious changes and hard coding a test would just verify that “yes you can upgrade from specific version X to specific version Y”, not that useful compared to the required effort.