I'll try to provide exactly the amount of context that is necessary to understand the construct.
I built an API (well, a small part of one) that works and is kind of usable, but rather unpretty and badly testable, and most of all unpretty. Testability is also an issue.
The problem domain has the concept of an 'image' in the sense of a logical image of a database record. This image can be extracted from an execution context passed into the logic from the calling application, based on certain rules that need to have defaults but also be configurable on different levels.
The goal is to be able to just take a dependency in an interface called IFullImageSource
anywhere in the application (I am using an IoC container) and call its parameterless GetImage()
method, having the implementation handle everything else.
The IFullImageSource
implementation:
public class SelectingFullImageSource : IFullImageSource
{
private readonly IImageRulesSet _rules;
private readonly IPluginExecutionContext _context;
public SelectingFullImageSource(IImageRulesSet rules, IPluginExecutionContext context)
{
this._rules = rules;
this._context = context;
}
public Entity GetImage()
{
StepStage stage = this._context.GetStepStage();
IImageRule rule = this._rules.GetRule(stage);
return rule.GetImage(this._context);
}
}
Everything needed to determine how to extract the image is contained within this part of the object graph; the rest of the application does not need to care at all.
Now let's go to the other end and start with the small parts.
IImageRulesSet
encapsulates a collection of IImageRule
objects keyed by StepStage
values (StepStage
has overridden equality members); I will show that in a moment. The smallest components here are the IImageRule
implementations. They generally look like this:
public class TargetImageRule : IImageRule
{
public Entity GetImage(IPluginExecutionContext context)
{
return ((Entity)context.InputParameters["Target"]).Clone();
}
}
or
public class PreImageRule : IImageRule
{
private readonly string _imageName;
public PreImageRule() { }
public PreImageRule(string imageName)
{
this._imageName = imageName;
}
public Entity GetImage(IPluginExecutionContext context)
{
string imageName = this._imageName ?? context.PrimaryEntityName;
return context.PreEntityImages[imageName];
}
}
These don't include the StepStage
part because while that is determined solely from the context as well, the same IImageRule
implementation can apply to several StepStage
values.
The default rules set is provided as a dictionary of StepStage
and IImageRule
:
public class DefaultImageRules : Dictionary<StepStage, IImageRule>, IDefaultImageRules
{
public DefaultImageRules() : this(null, null) { }
public DefaultImageRules(string preImageName, string postImageName)
: base(new Dictionary<StepStage, IImageRule>()
{
{new StepStage("Create", StepStage.PreStage), new TargetImageRule()},
{new StepStage("Create", StepStage.PostStage), new TargetImageRule()},
{new StepStage("Update", StepStage.PreStage), new MergeImageRule(new PreImageRule(preImageName), new TargetImageRule())},
{new StepStage("Update", StepStage.PostStage), new PostImageRule(postImageName)},
{new StepStage("Delete", StepStage.PreStage), new PreImageRule(preImageName)},
{new StepStage("Delete", StepStage.PostStage), new PreImageRule(preImageName)},
}) { }
}
This is where the unprettiness starts. The constructor "fallthrough" with null
parameters (of all things!) seems rather sloppy; the same goes for the rule implementations - although local defaults are always available.
That means the following: Simply creating a DefaultImageRules
object will pass null
for all name parameters, but through
container.Register<IDefaultImageRules>(new DefaultImageRules("preImage", null));
the developer can rename one of the images while the rule structure itself stays in place.
(Note: The IDefaultImageRules
interface is only an empty marker interface deriving from IDictionary<StepStage, IImageRule>
for IoC and mockability; I can probably do away with this and use a derived class for unit tests that has
this.Clear();
in its constructor before setting it up for the test again.)
This is also not testable very well. I can easily check whether the correct IImageRule
implementation is returned for a StepStage
value, but I can only test proper passing of the image names by creating an IPluginExecutionContext
stub and setting it up with the expected image, then passing it into the IImageRule
instance returned by the dictionary. This is actually testing two things at once - but then again that might not be an issue because the IImageRule
implementations are individually tested anyway and "guaranteed" to work correctly.
Still, the whole thing seems a bit flaky.
This is being used in the following class that fulfills the IImageRulesSet
dependency of the very first one I showed (yes, all those names are also still an issue):
public class ImageRulesSet : IImageRulesSet
{
private readonly IDictionary<StepStage, IImageRule> _rules;
private readonly IDictionary<StepStage, IImageRule> _customRules;
private readonly IMergeImageRulesDictionaries _mergeImageRulesDictionaries;
public ImageRulesSet(IDefaultImageRules rules, ICustomImageRules customRules, IMergeImageRuleDictionaries mergeImageRulesDictionaries)
{
this._rules = rules;
this._customRules = customRules;
this._mergeImageRulesDictionaries = mergeImageRulesDictionaries;
}
private bool _rulesHaveBeenMerged = false;
private IDictionary<StepStage, IImageRule> Rules
{
get
{
if (!this._rulesHaveBeenMerged)
{
this._mergeImageRulesDictionaries.Merge(this._rules, this._customRules);
this._rulesHaveBeenMerged = true;
}
return this._rules;
}
}
public IImageRule GetRule(StepStage stepStage)
{
return this.Rules[stepStage];
}
}
The ICustomImageRules
/CustomImageRules
combination is exactly the same as the DefaultImageRules
thing, except that the dictionary is empty by default.
The IMergeRuleDictionaries
simply merges both, giving precedence to the 'custom' entries for identical StepStage
values to allow for overriding default rules in addition to adding new ones.
That is about it; the object graph in total looks like this:
SelectingFullImageSource : IFullImageSource
ImageRulesSet : IImageRulesSet
DefaultImageRules : IDefaultImageRules
IImageRule implementations
CustomImageRules : ICustomImageRules
IImageRule implementations
DefaultMergeRulesDictionaries : IMergeRulesDictionaries
IPluginExecutionContext (from calling application)
The configuration effort is the minimum possible, I think - registering instances of IDefaultImageRules
and ICustomImageRules
in the IoC container; the values for the custom rules can be written as a collection initializer for the dictionary.
What I'm looking for are opinions on the quality of this solution with respect to the SOLID principles and testability, as I currently think it is less than ideal, as well as (first and foremost) a more elegant solution. The only things I really need to retain are the outermost interface (IFullImageSource
) and the degree of simplicity in configuring the rules - the exact way of doing that can be entirely different.
Oh, and suggestions about naming are also always welcome.