If you have too many dependencies being passed around, the general technique is to eliminate those dependencies higher up in the call stack, by changing the order decisions are made. This is easiest to explain with an example:
getPath(config, user) {
if (config.isB2B())
return b2bpath(config, user);
else
return b2cpath(config, user);
}
b2bpath(config, user) {
if (!config.allowedToAccessPath(user))
return accessDeniedPage();
else
return "My fancy b2b page";
}
b2cpath(config, user) {
if (!config.allowedToAccessPath(user))
return accessDeniedPage();
else
return "My fancy b2c page";
}
You are repeating the authorization check down at the lowest levels of the call stack, so move it up:
getPath(config, user) {
if (!config.allowedToAccessPath(user))
return accessDeniedPage();
if (config.isB2B())
return b2bpath();
else
return b2cpath();
}
b2bpath() {
return "My fancy b2b page";
}
b2cpath() {
return "My fancy b2c page";
}
Then repeat to see if you can move some of the decisions into the code that calls getPath
. This is a simple example, but I see the former kind of code all over the place with more layers. Start with decisions at your lowest layers, and try to figure out ways to move them up. Sometimes this requires judicious use of inheritance, like:
getPath(config, user) {
module = config.isB2B() ? B2BModule() : B2CModule()
if (!config.allowedToAccessPath(user))
return module.accessDeniedPage();
return module.getPath();
}
It's very rare not to be able to simplify dependencies this way. It's a matter of trying different arrangements until you find one that works.
If you have workflow-type dependencies, not just data dependencies, as in your first example, you can separate them out using something like this:
step1 = new ValidateHeaderId(inputHeaderId);
step2 = new FindShipment(retShipmentRepository);
step3 = new ValidateUserPermission(user, inputPostCode, inputEmail);
step4 = new SaveReturnOrder(inputLines, inputReference);
step5 = new CheckSaveStatus();
notSet = new NotSetPage(templateEngine, searchPage);
notPermitted = new NotPermittedPage(templateEngine, searchPage);
saveErrors = new SaveErrorsPage();
success = new SuccessPage();
requestTokenConfirmation = new RequestTokenConfirmation();
steps = [step1, [notSet, step2], [notSet, step3], [notPermitted, step4],
[saveErrors, step5], [success, requestTokenConfirmation]];
executeSteps(steps);
This recognizes you have a series of steps which each produce some sort of result and choose the next step. executeSteps
abstracts away the repetition of calling run()
on each step, and passing the output from the previous step into the next step. This allows the steps to be stored in a data structure instead of a function, which can then be built up in several different ways, including by some sort of registration process or config file. Once each step object has been created, its dependencies no longer need to be tracked outside it. I believe the rules engines from BobDalgleish's answer are basically pre-existing libraries to help you do this more easily.
Best Answer
First of all, the example you provided is not incredibly inefficient; its only slightly inefficient; its inefficiency is below the perceptible level. But, in any case, let's move on with the question.
The way I understand it, when we speak of separation of UI and Logic, we mean avoidance of close coupling.
Close coupling refers to the situation in which the UI knows (and invokes) the logic, and the logic knows (and invokes) the UI. To avoid close coupling one does not need to resort to abolishing coupling altogether. (That's what you seem to be aiming at by demolishing the interface between them down to a least-common-denominator string interface.) All one needs to do is to employ loose coupling.
Loose coupling means that A knows B, but B does not know A. In other words, the two parties involved play distinct client and server roles, where the client knows the server, but the server does not know the client.
In the case of UI and logic, the best way of arranging this in my opinion is by seeing the logic as a server, and the UI as a client. So, the UI is built for the logic, has knowledge of the logic, and invokes the logic, while the logic knows nothing about the UI, and simply responds to the requests that it receives. (And these requests happen to come from the UI, but the logic does not know that.)
To put it in more practical terms, nowhere within the source code files of the logic should you find any include/import/using statements that refer to UI files, while the source code files of the UI will be full of include/import/using statements that refer to Logic files.
So, to come back to your case, there is absolutely nothing wrong with the fact that the UI code which populates the combo-box knows about the hamburger class. There would be a problem if the hamburger class knew anything about combo boxes.
Incidentally, this design allows another thing which you should expect from such a system: it should be possible to plug as many different UIs as you wish to the logic, and the whole thing should still work.