简体   繁体   中英

Need to use random number generator in a value type kind of object but don't want the burden of its dependency

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.

Neither 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM