44

Consider your have an enumeration that looks like this:

enum ProcessingType {
    CONFABULATION, RETICULATION, SPLICING;
}

And you have a web-service that looks like this:

class WebService {

    Service service;

    @Mapping("/operation/{processingType}")
    void process(@RequestBody Data data, @RequestParameter ProcessingType processingType) {
        service.process(data, processingType);
    }
}

And this is the service with the business logic:

class Service {
    public void process(Data data, ProcessingType processingType) {
        if (processingType == CONFABULATION) {
            confabulate(data);
        } else if (processingType == RETICULATION) {
            reticulate(data);
        } else if (processingType == SPLICING) {
            confabulate(data);
            reticulate(data);
        } else {
            throw Exception("unexpected processing type");
        }
    }

    private void confabulate(Data data) {
        // ...
    }

    private void reticulate(Data data) {
        // ...
    }

}

Seen from the web controller, the business service only has one public method, with an argument to decide which processing it needs to do.

Does this pattern have a name, and is it considered an anti-pattern?

Alternatively, one could have done this:

  1. Remove the enumeration;
  2. Remove the process() method from the service;
  3. Set the visibility of confabulate() and reticulate() as public in the service;
  4. Have 3 methods in the web service that call confabulate(), reticulate() or both.
13
  • 6
    Another alternative is that since the ProcessingType options are known statically. You could just define a rest endpoint for each (e.g. @Mapping("/operation/confabulation"), etc.).
    – edalorzo
    Oct 8, 2021 at 19:57
  • 1
    I may be wrong but doesn’t this look a lot like simulating polymorphism? Oct 9, 2021 at 7:29
  • 3
    In programming, half of everyone will tell you something is an antipattern, while the other half will claim the same thing is not an antipattern. Write code that makes sense to you, or if you're working on a team, write code that's easily readable and changable by everyone. There are no hard rules. Antipatterns don't actually exist. (Now of course, someone is going to reply that they do exist, which would exemplify my point perfectly.)
    – user91988
    Oct 10, 2021 at 20:16
  • 14
    So basically you bulletproof your argument by saying that everything that contradicts you just proves you right. I bet that's a way to win lots of discussions.
    – edalorzo
    Oct 10, 2021 at 21:10
  • 1
    @chepner: we have method references since Java 8 as well. And I don't see any reason they wouldn't work here. IMHO this code violates OCP (open closed principle), because the class is not open for extension without modification. A method reference / function pointer would fit nicely, IMHO. Using the enum will result in violation of DRY (don't repeat yourself) sooner or later, since you implement the same if/else in some other place. Oct 11, 2021 at 14:26

13 Answers 13

35

Does this pattern have a name,

That depends on who you ask. Some folk treat patterns as only applicable to OOP and see them as more like implementation patterns in that, for example the UML used in the GoF book, lays out what the pattern looks like. If you are of that view, then it's not a pattern.

If you take a broader view that a pattern describes a design concept and isn't interested in the implementation (to me the clue is in the name "design pattern" here) then yes, you are using the strategy pattern. Quoting from Wikipedia, "[it is] a software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use"

and is it considered an anti-pattern?

Not as you show it. You have one function that matches against the enum values and picks a function to call (ie it implements the strategy pattern). What you have there is good: it's simple, easy to understand and easy to maintain. I'd maybe move the enum to within WebService as it keeps the enum and the decision tree in one place, but that's the only improvement I'd suggest here.

However, the moment you create another function, elsewhere in the code, that also matches against those enum values and picks a function to call, you start to cause problems for yourself. If you change the enum, you have to remember to change the decision tree in two places. Add a third, fourth, etc. place and the code quickly becomes a maintenance nightmare. And that is how this approach can become an anti-pattern.

9
  • 6
    Even when you do end up with multiple uses of the enum, there's still a relatively straightforward fix to that: Use the "real" (OOP) strategy pattern, and write a function (method) that converts your enum values into strategy objects. Then, for each place where you would have interpreted the enum values, you instead make a polymorphic call into the strategy object. The language enforces that every abstract method must be overridden in all concrete subclasses, so you cannot "forget" one of the methods. In this way, the enum still exists, but you only interpret it once.
    – Kevin
    Oct 8, 2021 at 21:14
  • 23
    This is not a Strategy pattern In a proper strategy pattern, the algorithm is contained outside the dispatcher logic, and passed in, typically using an interface. Allowing near infinite possibilities to extend the code. Here the algorithm is inside existing code, and the enum just chooses. There is no way to extend, for the outside to provide a whole new algorithm. E.g. see tutorialspoint.com/design_pattern/strategy_pattern.htm
    – user949300
    Oct 9, 2021 at 15:56
  • 3
    Not sure why this answer is so highly upvoted. This answer focuses on how it might, technically, fit the definition of a strategy pattern in a none-OOP setting (and, as @user949300 said, that is debatable). It ignores that in an OOP setting it is definitely an anti-pattern, and should be tackled in some other way.
    – Omegastick
    Oct 10, 2021 at 12:41
  • 2
    @Omegastick, I'd suggest it's highly upvoted because most people think you mistaken in your view that it is an anti-pattern.
    – David Arno
    Oct 10, 2021 at 12:53
  • 2
    It is not a strategy pattern if you just invoke a function statically in the code, but it can be a strategy pattern if the function can be chosen dynamically, for that you would need to resort to e.g. high-order programming.
    – edalorzo
    Oct 10, 2021 at 14:56
15

Multiple Dispatch

I believe the pattern you describe is called multiple dispatch.

This is a form of polymorphism. You can compare it to subtype polymorphism of the kind offered by OOP languages that support class inheritance. Those languages usually resorts to single dispatch, at runtime, to choose an implementation to satisfy a virtual call. Single dispatch in this case is based on the type of the object at runtime (a single value). Multiple dispatch can choose which way to go based on multiple values. You have a function type, multiple implementations and we polymorphically choose one at runtime based on one of multiple acceptable values, hence multiple dispatch.

In languages that support multi-methods (e.g. Clojure), this is not a problem at all because the language itself can express this relationship between a specific value and a function that needs to be called for it.

The way you're solving the problem is just the natural way to do it when you don't have multiple dispatch support built in the language.

Alternatively, you could have used switches or tables (e.g. a map).

switch processingType {
   case CONFABULATION: return confabulation(data);
   case RETICULATION: return reticulation(data);
   case SPLICE: return splice(data);
}

Additionally, if your language supports functions as first-class citizens, you could have a table do that multiple dispatch for you.

dispatch[CONFABULATION] = confabulation
dispatch[RETICULATION] = reticulation
dispatch[SPLICE] = splice

Then you can simply do:

dispatch[processingType](data)

On the Subjective Interpretation of Patterns

Since others have commented on the subjectivity of patterns, I wanted to add a few quotes here and hopefully contribute to that part of the discussion as well.

The famous Design Patterns book, whose authors are often referred to as the Gang of Four (GoF), clarifies from the start that patterns are subject to interpretation according to context.

Point of view affects one’s interpretation of what is and isn’t a pattern. One person’s pattern can be another person’s primitive building block.

Later in that same introductory chapter, it says:

The choice of programming language is important because it influences one’s point of view. Our patterns assume Smalltalk/C++-level language features and that choice determines what can and cannot be implemented easily. If we assumed procedural languages, we might have included design patterns called “Inheritance,” “Encapsulation,” and “Polymorphism.” Similarly, some of our patterns are supported directly by the less common object-oriented languages. CLOS has multi-methods, for example, which lessen the need for a pattern such as Visitor […] In fact, there are enough differences between Smalltalk and C++ to mean that some patterns can be expressed more easily in one language than the other.

So, you probably wouldn’t think of inheritance as a pattern if you were working with a language that fully supports it. However, in languages without support for it, you would resort to other tricks to simulate inheritance when you need it and you would call that a pattern. For example, consider how Google Go uses struct composition and some syntactic sugar (using some method set resolution voodoo) to make us feel we have a form of struct inheritance, in a language feature conveniently called “embedded fields”.

Another example, in GoF’s book we find a pattern called decorator, but developers of programming languages where functions are values use this under a fundamental feature of their language: high-order functions. Functions are so easily composed for them that “decorating” one does not need a pattern name, it is ingrained in their language and in the way they think (and languages shape the way we think.

Is this a Strategy Pattern?

Other answers already discuss it, so I will try to contribute new angles to the analysis.

My take is that your code can only be a strategy pattern if the algorithm is dynamically chosen, i.e. strategy calls cannot be statically defined in code. Additionally, the strategy must be passed to a context that will use it to provide a service operation to a client.

strategy = dispatch[algorithmType]
context(strategy)
context.operation()

Alternatively, I would still accept context.operation(strategy). However, I know opinions might differ on what constitutes a valid strategy pattern.

Anyways, you can probably see that in languages that support functions as first-class citizens, the so-called strategy pattern is just a regular function that encapsulates an algorithm. In OOP languages the function is encapsulated in an object (called a strategy) that you use to reach the function inside it (that’s the case of e.g. Java).

What matters with patterns is to understand the problem they solve and the tradeoffs of using them. Implementations might defer depending on language features.

In your case, what matters is that it is better to have individual functions managing separate types of data processing than having a single function with code bifurcations based on the enum value. The latter can rapidly become a maintenance nightmare.

The flow of the switches themselves becomes confusing. Hard to read. Hard to decipher. When a new case comes in, the programmer must find every place it can be involved (often finding all but one of them). I like to call this “switch creep.”

Design Patterns Explained by Alan Shalloway

Additionally, you can probably see how powerful it is that the three algorithms operate on the same data structure, instead of having different data structures for every algorithm. This allows the definition of a single “function signature”, a “public interface” that you can use to exploit the pattern of polymorphism i.e. Implement three different functions whose type is the same, but behave completely different. That’s how I could define a functional dispatch using a map in my example above.

3
  • 1
    What you are doing with dispatch[processingType](data) has nothing to do with functional programming. This is just a hashmap with syntactic sugar to call a defined method on the underlying value. Languages which support functions as first-class citizens is what you are looking for. You can do the same thing in C# and C++ and these languages are by no way functional in nature.
    – bash0r
    Oct 9, 2021 at 22:03
  • 2
    I have changed my description as suggested because it is more precise. However, I must say that there is no precise definition for what is functional programming. Many would accept the only requirement is functions as values, since that opens the door of high-order programming. Others might require more: immutability, recursion (as in recursive style, tail recursion, recursive data structures), laziness, mathematical syntax, pure functions, etc. Languages like C++, C# and Java have ways to provide functions as values, so some might debate they are functional programming languages.
    – edalorzo
    Oct 10, 2021 at 15:44
  • @bash0r, odd statement there. C# supports functions as first-class citizens, has recently added tuples, records, pattern matching and natural function types to the language and work is underway in discriminated unions for a future release. Yet still some folk like yourself claim it is "by no way functional in nature". In my view, you are mistaken and C# is well on it's way to being a true multi-paradigm language.
    – David Arno
    Oct 11, 2021 at 9:21
15

One potential issue with this pattern is that the knowledge of which implementation should be used appears to be encoded in more than one place. That is, there appears to be a 1:1 mapping between an externally exposed concept (ProcessingType), and the implementation of each type.

This fact suggests a question: if the internal implementations should be hidden, then why does the caller know about each individual one by name, and if the internal implementations shouldn't be hidden, why not just expose those as part of the exercisable API?

That I'm asking these questions doesn't guarantee that anything is actually awry. If the code in question is functioning as a kind of "seam-crosser" that translates an external grammar into an internal/domain ubiquitous language, and it is merely a possibly-temporary coincidence that these are the same terms (for now), then this may be perfectly well justified. (It is less likely to be justified, though, if the use case is simply one piece of code calling another piece of code, and there's no serialization occurring such as in a web service, and no translation happening such as in handling a text-based CLI command.)

On the other hand, some clear and deliberate thinking on which of the various interoperating pieces & systems should know various aspects of the full problem and entire distributed system could yield some valuable insight.

Perhaps these additional questions might help you zero in on what you are trying to accomplish:

  1. Should the caller actually know about confabulation, reticulation, and splicing? Or should it just submit some data and let the callee decide? Is it effectively handing in nouns, or does it really want to use verbs? Who's the authority, here? What creates value to have the decider of which action separate from the implementer of each action?

  2. Where are changes mostly likely to occur in this scheme, and which changes are likely to occur at the same rate, in the same areas? Have you designed things so that any single reason for the system to change is likely to affect only one, or very few, code modules? Could the internal implementation increase from three verbs into five or six with the outside caller remaining ignorant of this? Is the problem space or domain of the callee distinct enough from the problem space of the caller that adopting the caller's terms could end up improperly distorting the organization of and concepts within the internal domain?

  3. When you think about the different aspects of actions that must be carried out, and the different parts of your system, can you identify any clear delineations about which part of the system should know each aspect? For example, these aspects don't automatically have to be co-located: when an action should happen, what action should happen and in which cases, which part actually triggers an action happening, which part knows how to carry out an action, which part knows the answers to all of these questions and connects them so they coordinate properly, and so on. But however close or far these things are, each does represent a single reason that code might change—suggesting they should at least be individual functions, if not promoted to classes or even entire libraries or separate systems.

Asking these question is not creating complexity. Asking these questions is discovering inherent complexity and then aligning code to the real world to achieve the lowest functional complexity over time: the fewest bugs, the least amount of failures, code that is the easiest to understand, the easiest to change or extend, the easiest to test, with the least amount of technical debt.

In my book, adequately addressing real-world complexity in order to achieve these "easies" is actually reducing complexity overall.

5

Yes, it's somewhat of an anti pattern.

The object oriented way of "deciding what to call" is to leverage polymorphism.

Your code looks like Java. Java enums can have methods. Just call the enum Operation, with a method process(), like this:

enum ProcessingType {
    CONFABULATION{
        process(Data data) {
            //logic goes here
        }
    }, 

    RETICULATION{ ... }, 
    SPLICING { ... };

    abstract process(Data data);
}

Then your web service method can do this:

@Mapping("/operation/{processingType}")
void process(@RequestBody Data data, @RequestParameter Operation operation) {
    operation.process(data);
}

And the Service class becomes unnecessary. And you can add new operations in only one place.

However, in languages that don't have enums with methods that can be automatically mapped from controller methods, the easiest way might be still the switching logic in its own method.

4
  • 3
    However, would you put complex business logic in an enum? For example, transactions, I/O, etc? I don't have an argument against it, but I have the feeling something is not right, that enums are not for complex business transactions.
    – edalorzo
    Oct 8, 2021 at 19:54
  • 3
    But by putting the logic into the enum, this logic would be exposed to clients who (have to) know the enum. Isn't this exposing too much? Or would you make those methods just accessible inside the package? Oct 9, 2021 at 0:46
  • 1
    @PaŭloEbermann: we're looking at a web service here. It's called via HTML. The client only has to know the name of the enum constants, and the value it sends is mapped by the web framework (see the RequestParameter annotation) to the enum. Oct 9, 2021 at 16:12
  • 1
    Still, an enum can be use as property type in a DTO, in a model object, in a function parameter, etc. And this one carries with it all the power of a service layer operation that basically can be accessed anywhere the enum value goes. I just don't think that is a good design.
    – edalorzo
    Oct 10, 2021 at 15:49
4

Yes, this anti-pattern code smell has a name. It’s called primitive obsession.

The refactoring suggested by others also has a name: replace conditional with strategy.

I would add more, but you asked for a name, and now that you know the names, you should have no trouble finding articles, videos, and books that explain these concepts better than I could.

2
  • 1
    Probably the most correct and on-point answer among others. Though, I know that answers consisting only of links are discouraged, the links here are just names of the established concepts.
    – hijarian
    Oct 11, 2021 at 20:33
  • I agree with the assessment of the issue but I think strategy is overkill. You just need a Processor interface and implementations of that.
    – JimmyJames
    Oct 12, 2021 at 15:27
2

Is the choice of CONFABULATION, RETICULATION, SPLICING really dynamically chosen by the consuming client?

If so, I would follow the advice of the other answers, but if not I would have three separate functions/methods instead of conflating them at some point, only to have to tease them apart later.

1

In this particular example, yes. Especially since Service.process() has absolutely no logic of it's own. I would either expose the three public methods, or, follow the @MichaelBorgwardt suggestion and put the methods inside the enums. If Service.process had extensive logic of its own, say, updating and validating the data, and then called confabulate as a "minor" side effect, things might be different. (ignoring SRP)

However, in general, no. For example, most languages have methods like map and filter, which iterate over an array or collection and call another function, which is passed in as an argument. This is a very powerful feature. e.g., newArray = myArray.map(theFunction)

Details vary by language, here are the docs for Javascript array.map

1

I don't know if there's a common pattern name for your code, but I'd call it a Bad Abstraction.

The main question is, does this pattern benefit:

  • The API user?
  • The service developer?

From the API user point-of-view, they have three different API endpoints:

  • /operation/confabulation,
  • /operation/reticulation,
  • /operation/splicing

To the API user, the fact that they end up in the same process() method is completely invisible. So, to the API user you present three different actions, having nothing in common (just using a common /operation/ prefix).

It's only your service-internal code that deliberately combines them into one WebService.process() method by translating the second path element into an enumeration value parameter, only to conditionally branch to different actions in the next layer Service.process(). So, you're subsuming confabulation, reticulation, and splicing under a common name process() only in the top two of your software layers.

Of course, there's no technical reason against such a pattern, but it's a bad abstraction and degrades readability. It obscures the fact that under the hood, three very different things are done by the process() method.

Try to write a (e.g. JavaDoc) documentation what the process() methods do, and you'll find yourself combining three different descriptions and a lot of conditional wording. And try to find a good method name that describes what the method does (you'll surely agree that process() doesn't tell anything). Abstractions should make things easier to understand, and the process() method existence and naming fails that goal.

So, instead of a benefit to the service-internal architecture, I see two methods without a clear, concise task - which could easily replaced with better ones by not combining the three different actions into a common method.

Summary: You should always be able to describe a method's purpose with a short, one-liner sentence. If it's hard to find such a description, your abstraction is wrong.

0

As the caller, do I know how you use that parameter? No, I pass in the ProcessingType, and I expect the function to do what it is supposed to do. How it does that I don't care. I don't tell you which function to call.

So do you want my interface to change? Do you want me to know all the different methods that I could call? I just want to call one function, and tell it what to do.

0

Does this pattern have a name?

It looks a bit like the strategy pattern or command pattern, but it isn't since your ProcessingType is not callable. See @MichaelBorgwardt's answer for how that would look.

I would argue that it is rather the interpreter pattern with a very limited language.

Alternatively, one could simply have 3 methods in the web service that call confabulate(), reticulate() or both.

Yes, do that! Much simpler, less over-engineered.

You don't need the enum unless you actually intend to store it somewhere or make it part of a data structure. Sure, your web API is an enumeration of endpoints anyway, but unless you want to make that explicit (e.g. even in a client library) you don't need to create them as values. Ultimately, any web servers does interpret the HTTP "language".

You don't need the process method in the service, unless it is called from multiple places. It really seems to be the responsibility of your webservice to figure out what /operation/splicing means.

And even if there are multiple callers that want to splice stuff, and really the knowledge about splicing should be in the service and not the web service, then just write a splice() method for the service.

0

The if ... else if ... else if ... else can quickly become an anti-pattern.

I'll use a specific example, sending commands to a spacecraft. (Similar arguments apply to sending commands to a robot.) All that one has as an interface from the perspective of a ground controller for a spacecraft is to send a CCSDS (Consultative Committee for Space Data Systems) message to that spacecraft. CCSDS can handle audio, video, and commands. I'll look at the commanding aspect.

CCSD messages comprise a message length, the message itself, and possibly some auxiliary data. Regarding the message itself, a command to a spacecraft comprises an enumerated command value and command-specific command data.

Spacecraft oftentimes can handle hundreds of different commands, many of which have differently formatted command data. The command data oftentimes have interesting formats (and that is being nice to CCSDS). Unpacking that command payload data requires computer science expertise. Determining whether a command is or is not valid given the spacecraft mode also requires computer science expertise. On the other hand, handling the command and determining whether the command data are valid requires domain expertise.

This suggests multiple functions, one function to receive, unpack, validate, and dispatch all of the incoming commands, and one or more domain-specific functions to validate / carry out commands that apply to that domain.

The communication interface function will inevitable be extremely ugly from a cyclomatic complexity perspective if the function that processes the incoming message has a huge switch statement. I have signed my John Hancock as a reviewer (with complaints) regarding command handling functions whose cyclomatic complexity are well into the hundreds. I don't like signing my John Hancock to functions with a cyclomatic complexity that exceeds ten, let alone a switch statement with 500 cases. I would vastly prefer to see a data structure that maps command IDs to command handling functions than a 500 case switch statement.

-1

Flag arguments

Software development, especially good practices, are always a matter of contextual intent. If you describe the same thing but stress its description differently, one description can "feel" wrong and the other can "feel" right.

The direct answer to your question is yes, having your consumer knowingly branch your method's sublogic is a bad approach. The consumer should not steer the implementation.

This is a flag argument. It often manifests as a boolean, but your enum is doing the same thing, it represents a set of flag arguments.

But, and this is key, having your consumer pass a value, and unbeknownst to them this passed value ends up being using by your method in a way that it happens to call a different (sub)method, is perfectly fine. Here, the consumer only provided some info, and the implementation is the one who steered itself.

The difference between these two is the implication that the consumer knows what is happening behind the scenes based on this value. They shouldn't. That's the point of encapsulation: only the implementation needs to know how the implementation works and what it does.

Maybe an example would help here. This is a bad implementation, and what your question seems to imply is the case:

// Consumer

var court = new CourtOfJustice();
var defendant = new Person();
var shouldBeTriedAsAMinor = true;

court.Prosecute(defendant, shouldBeTriedAsAMinor);

// Implementation

class CourtOfJustice 
{
    public void Prosecute(Person defendant, bool shouldBeTriedAsAMinor)
    {
        if(shouldBeTriedAsAMinor)
            ProsecuteMinor(defendant);
        else
            Prosecute(defendant);
    }

    private void Prosecute(Person defendant)
    {
        // ...
    }

    private void ProsecuteMinor(Person defendant)
    {
        // ...
    }
}

The following is a good implementation. Notice how it barely differs from the bad one, and still fits with your question's description:

// Consumer

var court = new CourtOfJustice();
var defendant = new Person();
var defendantIsAMinor = true;

court.Prosecute(defendant, defendantIsAMinor);

// Implementation

class CourtOfJustice 
{
    public void Prosecute(Person defendant, bool defendantIsAMinor)
    {
        if(defendantIsAMinor)
            ProsecuteMinor(defendant);
        else
            Prosecute(defendant);
    }

    private void Prosecute(Person defendant)
    {
        // ...
    }

    private void ProsecuteMinor(Person defendant)
    {
        // ...
    }
}

The only thing that has changed is the name of the method parameter, but you also have to consider that the context has changed, i.e. the implication that the consumer somehow decides how the defendant should be tried. They shouldn't. All the consumer is doing here is providing information (is the defendant a minor?). It is the court which decides how to try a defendant.

As far as the consumer is concerned, they don't even know what difference it makes whether the defendant is a minor or not. Maybe it doesn't change how they are prosecuted, it only changes what prison they get sent to. Or whether the court documents will be sent to a parent instead of them. The consumer does not know the impact of the value they pass. They only know that the court has asked them to provide this information.


To summarize, the only issue with flag arguments is if the consumer knows to use the input data to steer the implementation. As long as the consumer merely provides input, without any expectation of knowing how this will impact the implementation, it's not a bad approach.


Strategies

You may think that because your enum specifically lists operations, which inherently imply methods, that you are therefore unavoidably violating this principle. Well, sort of.

The issue here is that the choice of what operation to execute is being made by the consumer. This in turn means that the choice of calling the right method should not be implemented by the implementation, because it is not the one making the choice.

Whoever makes the choice, also has to reference the choice they made.

This is what is called the strategy pattern. Instead of passing an enum value, you pass an object which is capable of performing the needed operation. Therefore, the implementation receives a "black box", does not know what operation this will perform, but the implementation can still call it.

Note: this really only makes sense when your called method is doing more than just calling the submethod. But I will get to that afterwards.

For example:

// Consumer

process(data, new Confabulator());

// Implementation

public void process(Data data, IOperation operation) 
{
    operation.Execute(data);

    // some more logic related to the "process" method itself
}

public interface IOperation
{
    void Execute(Data data);
}

public class Confabulator : IOperation
{
    public void Execute(Data data)
    {
        // ...
    }
}

public class Reticulator : IOperation
{
    public void Execute(Data data)
    {
        // ...
    }
}

Notice how it is the consumer who chooses what operation to perform, and also chooses the right strategy to perform this chosen operation. The implementation merely executes whatever operation the consumer passed into it.

However, in your code there is no such some more logic related to the "process" method itself. This means that the process method does nothing except call the passed operation, and therefore it has become redundant. It can be wholly removed:

// Consumer

IOperation operation = new Confabulator();

operation.Execute(data);

// Implementation

public interface IOperation
{
    void Execute(Data data);
}

public class Confabulator : IOperation
{
    public void Execute(Data data)
    {
        // ...
    }
}

public class Reticulator : IOperation
{
    public void Execute(Data data)
    {
        // ...
    }
}

If the process method had contained more logic, the previous snippet would be useful. But when it doesn't, the last snippet already does what you need.

-1

For your example, yes, it's an anti-pattern.

The WebService handler already allows for multiple functions. You can have a URI that maps to each function:

class WebService {

    Service service;

    @Mapping("/confabulation/")
    void confabulation(@RequestBody Data data) {
        // ...
    }
    
    @Mapping("/reticulation/")
    void reticulation(@RequestBody Data data) {
        // ...
    }
    
    @Mapping("/splicing/")
    void splicing(@RequestBody Data data) {
        // ...
    }
}

Update: @edalorzo also mentions this in a comment on the OP: "You could just define a rest endpoint for each"

The same applies within software (without Web Service exposure):

Writing a software language within a software language is an anti-pattern. I personally call it the "code in code" anti-pattern which feels similar to the "database in database" anti-pattern.

Assuming that process merely does a switch, an extra layer of indirection is added without increasing value.

The caller should just call directly: eg service.CONFABULATION(data);. If an end-user is given a choice for the 3, have the switch on the UI side.


There are plenty of cases where such a "routing" design MAY BE warranted. Nothing clear-cut comes to my mind right now.

I would suggest that you look for: Value-add - some sort of business-logic being applied, and you need that to be done once in a single place.

In fact, there are plenty of cases where a "routing" design should NOT necessarily apply:

  • Authorisation - This need not be centralised. You just need CONFABULATION to call IsAuthorised("CONFABULATION") near the start of the function/service.
  • Invalid Hint - Where the ProcessingType is just a hint, and the data is the source of truth/validity. This need not be centralised. CONFABULATION checks for IsValidForConfabulation(data), if not, it might then use a shared-function to find the correct new function: ProcessDataWithCorrectFunction(data).

I would be interested in exploring a range of possible "routing" scenarios, to determine whether a central "routing" service function is the ONLY solution or clearly superior among alternatives.

4
  • I like to get comments for anything downvoted. Oct 10, 2021 at 23:02
  • 1
    (not voted) I've seen a DSL implemented in C#. It felt quite good to use - and in case anything was not supported, I could fall back to C# features. That was definitely "code in code" but it turned out really well. Oct 11, 2021 at 14:14
  • @ThomasWeller thanks for the comment. I'm not sure how DSL applies here. I am simply responding to the question. Having ProcessingType is like a design decision in an RPC protocol - an unnecessary design at that. The Web Router already enables mapping of a URI to function. You could have a Router in a Router in a Router - what's the point? Oct 11, 2021 at 23:38
  • I have updated my answer some more with a code example. Oct 11, 2021 at 23:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.