Properly re-design from Switch to Polymorphism (Open/Close principle)
https://softwareengineering.stackexchange.com/questions/356871
-
19-01-2021 - |
Domanda
I'm having major troubles trying to get rid of this switch statement. To put a little of context first, I'm working with asynchronous batch operations. These operations can be applied to any entity in the system. You just have to send a different namespace.
Example:
"jobs": [
{
namespace: "client",
request: {"id": 10, "name": "john"}
},
{
namespace: "invoice",
request: {"id": 99, "number": "F20170101"}
}]
After I send those jobs, the service returns data similar, but with a response key like the following:
results: [
{
namespace: "client",
request: {id: 10, name: "john"},
response: {success: false}
},
{
namespace: "invoice",
request: {id: 99, number: "F20170101"},
response: {success: true, s_id: 3310}
}]
Now here comes the funny part
I have to process the response, and based on the namespace, I have to select the appropriate repository to update our database records. So far I have this smelly thing:
if (namespace == 'client') {
customerRepository.doSomething()
} elseif (namespace == 'invoice') {
invoiceRepository.doSomethingTotallyDifferent(withDiffParams)
}
Now this clearly breaks Open/Close principle, and I can't figure out how to solve this by removing the switch
(actually a if
/elseif
spaghetti thingy, but it's the same). Thanks in advance.
PS: this is not a specific language question, so please refrain to provide answers that applies only to a specific language.
Soluzione
Your repositories should be derived from a common base class, lets call it baseRepository
. This class needs to provide two virtual methods
correspondingNamespace()
processReponse(responseKey)
Overide the methods in the derived classes, so correspondingNamespace
should return the correct namespace for each repo, for example "client" for customerRepository
. processReponse
should delegate to the individual business logic like calling doSomething
or calling doSomethingTotallyDifferent
with different parameters.
Now you can easily initialize a dictionary with the available repositories beforehand, in pseudo code:
for each repo in [customerRepository, invoiceRepository]:
repoDict[repo.correspondingNamespace()]=repo
And the response processing will look (roughly) like:
repoDict[responseKey.namespace].processReponse(responseKey)
To your comment: if you do not want to add these methods to the repos directly, you can use a separate hierarchy of strategy classes and place the two methods there. Derive customerResponseStrategy
and invoiceResponseStrategy
from responseStrategy
, and let each responseStrategy
hold a reference to the corresponding repo. That will lead to something like
for each respStrat in
[new customerResponseStrategy(customerRepository),
new invoiceResponseStrategy(invoiceRepository)]:
responseStrategyDict[respStrat .correspondingNamespace()]=respStrat
and
responseStrategyDict[responseKey.namespace].processReponse(responseKey)
Altri suggerimenti
What I've done plenty of times in projects, and that helped a LOT with new feature requests was:
interface JobResponseWorker {
bool canProcess(namespace);
void process(response);
}
class ClientResponseWorker implements JobResponseWorker {
bool canProcess(namespace) {
return namespace == "client"
}
void process(response) {
//... whatever you want to do here
}
}
class InvoiceResponseWorker implements JobResponseWorker {
bool canProcess(namespace) {
return namespace == "invoice"
}
void process(response) {
//... whatever you want to do here
}
}
workers = [new ClientResponseWorker(), new InvoiceResponseWorker()]
//your main logic
void processResponse(namespace, request, response) {
for (JobResponseWorker worker in workers) {
if (worker.canProcess(namespace)){
worker.process(response)
}
}
}
Like this, most of your main code is closed for modifications. For new workers, you Add new code (open for extension), and then only adds another object into an array (no additional logic in old code).
I think what you are looking for here is the kind of approach described by the Strategy pattern. In a nutshell you need to translate your namespaces to classes/objects. In your example you would have a Client
class and a Invoice
class. Each would have a doTheThing()
method.
What remains is that you need to have some sort of switch or map that determines which type you should be using. This might seem somewhat pointless in a simple example like this but as the number of operations and classes increases, it will greatly simplify both the code and make it much more understandable. You will also be more able to add new types without touching any of the existing logic for other types.
As long as you have only one place in your code where you have to do this discrimination you should stay with it. But if you have more places where you need to apply different behavior upon the namespace value you should go for the "polymorphic" approach.
The problem is that with a "polymorphic" approach you have to do the discrimination too because you have to select the implementation that belongs to your namespace value at some point.