I currently have an immutable type called Gene
, that only has 2 fields:
double value;
Interval intervalOfAllowedValues;
I'll need sometimes to have a Gene
switch randomly its value to some other value as long as it is still in the range defined in intervalOfAllowedValues
.
I had made for this, a special method
public Gene RandomMutation() { ... }
//it returns a Gene because this class is immutable!
that'd take care of the situation, using a INumberGenerator.GenerateDouble(...)
. The problem is that either RandomMutation()
accepts a IRandomNumberGenerator
as argument or then Gene
will have to take one by constructor injection.
Ne开发者_运维技巧ither of the solutions are of my liking:
If RandomMutation()
accepts the number generator by argument, it means that now not only does Gene
have to know about INumberGenenerator
but also the class that contains it.
If on the other hand I pass by constructor injection an INumberGenerator
to this Gene
, most of the time I'll have it there without making any use of it, which doesn't seem nice. I feel it kinda defeats the purpose of having this Gene
object exist as an value type.
It also raises the subtle question of whether 2 different Gene
s are equal if they have different number generators. If yes, how to Unit-Test it?
There is a third option: I take RandomMutation()
away from the Gene
class. The problem now is that the class that contains a Gene
will have to know both about Gene
and about Interval
, which might not be desirable. Also, behaviour should be near its data, and that wouldn't be the case when following this approach.
There is yet a 4th(!) approach: making the number generator global (Singleton). That'd work wonders, but it'd go against the philosophy of making every dependency explicit.
How'd you handle this situation?
Thanks
I think your third option is definitely the way to go, not only because of testability, but also because of general design considerations. A gene is, in the end, a way to transfer data but has no real behavior of its own. The mutation is simply something that happens to that data. As such, it makes sense for RandomMutation to live somewhere else where it still has the ability to act on the genes.
In terms of testability, that is also by far the cleanest approach that keeps everything explicit and easily replaceable for stubbing, if necessary.
I see nothing wrong with having a GeneMutator class that has a mutate method that accepts a Gene and returns a mutated Gene (and keeps a reference to the INumberGenerator).
Interval
seems pretty generic, I don't see the downside to it being visible outside of Gene
.
If testability is your goal, I like your third option. Having behavior near data is (to my thinking) a secondary goal, while testability and SRP are primary goals.
My second choice would be to use use constructor injection to put the service in Gene (or setter injection for that matter, although I'm not a fan). Your main objections seems to be "most of the time I'll have it there without making any use of it, which doesn't seem nice." This is true of having the method directly on the class as well--the difference is that you are simply moving that functionality to an injected class.
It is not a useful metric to evaluate how much a dependency like INumberGenerator
is utilized throughout the lifetime of a particular Gene
instance. It is only useful to see how many times it is used by the definition of Gene
. If it is used even once, it deserves to be treated like a first-class dependency.
The bigger issue is in having a struct
with dependencies at all. RandomMutation
must have an INumberGenerator
to function, but no matter whether you use constructor or property injection, anyone can call new Gene()
and create an instance with null dependencies. There is simply no way for you to enforce that dependencies have been supplied to a struct
.
You have a couple of options. The first is to define Gene
as a class
, which will let you declare INumberGenerator
as a required dependency through a constructor argument. This is the object-oriented answer.
Your second option is use a more functional approach, which leaves Gene
as a struct
and defines the functionality externally. Functional programs tend to separate data structures from the logic which acts upon them (see LINQ), as opposed to OO which strives to combine behavior and data. Your problem sounds like it would best benefit from being modeled in this fashion.
The core issue still remains, though, which is that you need both a Gene
and an INumberGenerator
in order to mutate randomly. Since we have determined that Gene
probably shouldn't know about INumberGenerator
directly, you can model it as an extension method:
public static Gene Mutate(this INumberGenerator generator, Gene gene)
{
// ...
}
This takes the INumberGenerator
dependency out of Gene
and bubbles it up to the object which knows when genes should be mutated.
精彩评论