I have looked at some answers and searched on Google, but I could not find anything helpful (i.e., that wouldn't have awkward side effects).
My problem, in abstract, is that I have an object and need to perform a long sequence of operations on it; I think of it as a sort of assembly line, like building a car.
I believe these objects would be called Method Objects.
So in this example at some point I would have a CarWithoutUpholstery on which I would then need to run installBackSeat, installFrontSeat, installWoodenInserts (the operations do not interfere with one another, and might even be done in parallel). These operations are performed by CarWithoutUpholstery.worker() and yield a new object which would be a CarWithUpholstery, on which I would then run maybe cleanInsides(), verifyNoUpholsteryDefects(), and so on.
The operations in a single phase are already independent, i.e., I am already grappling with a subset of them that may be executed in any order (front and rear seats may be installed in any order).
My logic currently uses Reflection for simplicity of implementation.
That is, once I have a CarWithoutUpholstery, the object inspects itself for methods called performSomething(). At that point it executes all these methods:
myObject.perform001SomeOperation(); myObject.perform002SomeOtherOperation(); ...
while checking errors and stuff. While order of operation is unimportant, I have assigned a lexicographic order in case I ever discover some order is important after all. This contradicts YAGNI, but it costed very little - a simple sort() - and it could save a massive method renaming (or introducing some other method of performing tests, e.g. an array of methods) down the line.
A different example
Let us say that instead of building a car I have to compile a Secret Police report on somebody, and submit it to my Evil Overlord. My final object will be a ReadyReport. To construct it I start by gathering basic information (name, surname, spouse...). This is my Phase A. Depending on whether there is a spouse or not, I may then have to proceed to phases B1 or B2, and gather sexuality data on one or two people. This is made of several different queries to different Evil Minions controlling night life, street cams, sex shop sales receipts and what not. And so on and so forth.
If the victim has no family, I will not even enter the GetInformationAboutFamily phase, but if I do, then it's irrelevant whether I first target the father or the mother or the siblings (if any). But I cannot do that if I haven't performed a FamilyStatusCheck, which therefore belongs to an earlier phase.
It all works wonderfully...
- if I need some additional operation I only need to add a private method,
- if the operation is common to several phases I can have it inherited from a superclass,
- operations are simple and self-contained. No value from one operation is ever required by any of the others (operations that do are performed in a different phase),
- objects down the line do not need to perform many tests since they could not even exist if their creator objects hadn't verified those conditions in the first place. I.e., when placing inserts in the dashboard, cleaning the dashboard and verifying the dashboard, I needn't verify that a dashboard is actually there.
- it allows for easy testing. I can easily mock a partial object and run any method on it, and all operations are deterministic black boxes.
The problem arose when I added one last operation in one of my method objects, which caused the overall module to exceed a mandatory complexity index ("less than N private methods").
I have already taken the matter upstairs and suggested that in this case, the wealth of private methods is not a telltale of disaster. The complexity is there, but it's there because the operation is complex, and actually it's not all that complex - it's just long.
Using the Evil Overlord example, my problem is that the Evil Overlord (aka He Who Shall Not Be Denied) having requested all dietary information, my Dietary Minions telling me that I need to query restaurants, kitchenettes, street vendors, unlicensed street vendors, greenhouse owners etc., and the Evil (sub)Overlord - familiarly known as He Who Also Shall Not Be Denied - complaining that I'm performing too many queries in the GetDietaryInformation phase.
Note: I am aware that from several points of view this is not a problem at all (ignoring possible performance issues etc.). All that's happening is that a specific metric is unhappy, and there is justification for that.
What I think I could do
Apart from the first, all these options are doable and, I think, defensible.
- I have verified that I can be sneaky and declare half my methods
protected. But I'd be exploiting a weakness in the testing procedure, and apart from justifying myself when caught, I don't like this. Also, it's a stopgap measure. What if the number of required operations doubles? Unlikely, but what then?
- I can split arbitrarily this phase into AnnealedObjectAlpha, AnnealedObjectBravo and AnnealedObjectCharlie, and have one third the operations being performed at each stage. I am under the impression that this actually adds complexity (N-1 more classes), with no benefit except for passing a test. I can of course hold that a CarWithFrontSeatsInstalled and a CarWithAllSeatsInstalled are logically successive stages. The risk of a Bravo method being later required by Alpha is small, and even smaller if I play it well. But still.
- I can bunch different operations, remotely similar, in a single one.
performAllSeatsInstallation(). This is only a stopgap measure and it does increase the complexity of the single operation. If I ever need to do operations A and B in a different order, and I have packed them inside E = (A+C) and F (B+D), I'll have to unbundle E and F and shuffle the code around.
- I can use an array of lambda functions and neatly sidestep the check altogether, but I find that clunky. This is however the best alternative so far. It would get rid of reflection. The two problems I have is that I would probably be asked to rewrite all method objects, not only the hypothetic
CarWithEngineInstalled, and while that would be very good job security, it really doesn't appeal all that much; and that the code coverage checker has issues with lambdas (which are solvable, but still).
- Which, you think, is my best option?
- Is there a better way I haven't considered? (perhaps I'd better come clean and ask directly what is it?)
- Is this design hopelessly flawed, and I'd better admit defeat and ditch - this architecture altogether? Not good for my career, but would writing ill-designed code be any better in the long term?
- Is my current choice actually the One True Way, and I need to fight to get better quality metrics (and/or instrumentation) installed? For this last option I'd need references... I can't just wave my hand at the @PHB while murmuring These aren't the metrics you're looking for. No matter how much I'd like to be able to