DEV Community

loading...

An IoC pattern to avoid repetitive if statements

Tomaz Lemos
Software engineer, very passionate about coding. But you can also find me playing samba somewhere in Rio de Janeiro.
Updated on ・6 min read

Clean factory

In my previous post I shared two patterns that helped avoiding repetitive if statements, and we had a great discussion about imperative, if-based styles and declarative ones.

In this post I want to share another pattern which I think is great to expand our programming toolset.

I'll address some of the tradeoffs I see in it against the more traditional if-based approach, and I'd really like you to tag along and share what pros and cons you see in them in the comments. Let's do this together?

The examples are in Java but it should translate well to any other OO language.

If you prefer, skip right to the code

The pattern

This pattern is based on Java's Spring framework, and it's main point is that it offloads from the main class to the instances themselves the responsibility of knowing whether or not it is capable of, or should, handle a specific input, thus inverting the flow control.

It's widely used by Spring to find out which converter or mapper to use for a given input. Part of its value derives from being able to use unknown custom implementations of the interfaces provided by the user, but I do find value in using it for known types as well.

The context

For context, let's say we have an application that has to handle lots of different kinds of data, based on a what's the data's url’s country passed as a path parameter. Something like https://dev.to/myapp/data?url=http://www.google.com.br (I didn't escape the path param's url to make it clearer).

The application's job would be to retrieve the IncomingData from the url, and then pass it to the proper DataHandler provided by the Selector.

We might have a DataHandler interface that would be implemented by many classes, such as BrazilianDataHandler, SpanishDataHandler, FrenchDataHandler, etc, and a Selector to provide the correct instances.

Looking at the code should make it more clear, so let's do just that.

The usual if-based approach

Let’s start with an interface such as:

public interface DataHandler {
    ProcessedData handleData(IncomingData data);
}

A concrete implementation of the interface might look like:

// Other countries' handlers could be implemented the same way
public class BrazilianDataHandler implements DataHandler {
    @Override
    public ProcessedData handleData(IncomingData data) {
        // do stuff;
    }
}

Now let’s add the if-based selector:

public class DataHandlerSelector {

    private final DataHandler brazilianDataHandler;
    private final DataHandler argentinianDataHandler;
    private final DataHandler australianDataHandler;
    private final DataHandler frenchDataHandler;
    // Lots of other countries...

    public DataHandlerSelector(BrazilianDataHandler brazilianDataHandler,
                                        ArgentinianDataHandler argentinianDataHandler,
                                        AustralianDataHandler australianDataHandler,
                                        FrenchDataHandler frenchDataHandler) {

        this.brazilianDataHandler = brazilianDataHandler;
        this.argentinianDataHandler = argentinianDataHandler;
        this.australianDataHandler = australianDataHandler;
        this.frenchDataHandler = frenchDataHandler;
    }

    public DataHandler obtainDataHandlerFor(String url) {
        Assert.notNull(url, () -> "Url is null!");

        DataHandler dataHandlerToReturn;

        if (url.contains(".br")) {
            dataHandlerToReturn = brazilianDataHandler;
        } else if (url.contains(".ar")) {
            dataHandlerToReturn = argentinianDataHandler;
        } else if (url.contains(".au")) {
            dataHandlerToReturn = australianDataHandler;
        } else if (url.contains(".fr")) {
            dataHandlerToReturn = frenchDataHandler;
        // Lots of other countries...
        } else {
            throw new IllegalArgumentException("Url not supported: " + url);
        }

        return dataHandlerToReturn;
    }

    private static class Assert {
        public static void notNull(Object input, Supplier<String> errorMessageSupplier) {
            if (input == null) throw new IllegalArgumentException(errorMessageSupplier.get());
        }
    }
}

And then a factory to instantiate the selector and it’s dependencies:

public class DataHandlerSelectorFactory {

   public static DataHandlerSelector createSelector() {
       return new DataHandlerSelector(new BrazilianDataHandler(), new ArgentinianDataHandler(), new AustralianDataHandler(), new FrenchDataHandler());
   }
}

Simple, right? This pattern has the great benefit of having a straightforward logic, which often leads to easier reasoning as it's a very common pattern.

There are some drawbacks I see, tough:

  • The Selector logic has to be modified every time we need to include another country
  • Lots of language specific words and symbols obscuring the business logic, which would be the condition - handler correlation
  • It tends to get huge over time, with lots of copying and pasting, possibly leading to bugs and poorer comprehensibility and maintainability
  • It is a single point of failure, meaning if some logic gets messed up you can mess several or all countries at once (think if you accidentally delete an "else if" line and the line above it...)
  • You may need to see both the Selector and the implementation class to get the big picture about what the implementation class does

What do you think, do you agree with these points?

The IoC Selector pattern

In this Selector pattern the interface gets a new shouldHandle method:

public interface DataHandler {
    ProcessedData handleData(IncomingData data);
    boolean shouldHandle(String url);
}

And the Selector becomes:

public class DataHandlerSelector {

    private final List<DataHandler> dataHandlerList;

    public DataHandlerSelector(DataHandler... dataHandlers) {
        dataHandlerList = Arrays.asList(dataHandlers);
    }

    public DataHandler obtainDataHandlerFor(String url) {
        Assert.notNull(url, () -> "Url is null!");
        return dataHandlerList
                .stream()
                .filter(handler -> handler.shouldHandle(url))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Url not supported: " + url));

    // You can also check if more than one instance has been found by using List.size() on the list returned by a collect(toList()) at the end of the stream call.
    }

    private static class Assert {
        public static void notNull(Object input, Supplier<String> errorMessageSupplier) {
            if (input == null) throw new IllegalArgumentException(errorMessageSupplier.get());
        }
    }
}

Let's take a look at a sample implementation of the new interface:

public class BrazilianDataHandler implements DataHandler {
    @Override
    public ProcessedData handleData(IncomingData data) {
        // do stuff;
    }

    @Override
    public boolean shouldHandle(String url) {
        return url.contains(".br");
    }
}

In this example we have changed the centralized approach of the first Selector into a decentralized approach, handing down to the implementations the responsibility of knowing whether or not it should act on a given input.

Overall, I like designs that forces us to make our intentions clear and makes it easy to figure out which are the business rules and exceptional cases.

I think this approach brings as advantages:

  • No modification needed in the Selector class when adding a new implementation
  • The condition and the logic itself are together, allowing for better clarity and cohesion
  • No cluttered language specific words clouding business rules
  • No copying and pasting
  • No single point of failure

And as drawbacks:

  • We loose sight of the greater picture of conditions
  • May take longer to reason about the first time you see it
  • One condition may interfere with the other if it is greedy and comes before the other

What about you, can you see any other advantages or drawbacks?

The wicked new requirement

Ok, now a new requirement has arrived, and there are some classes that will have to be verified first.

In this case we will have the .gov and .org urls that have to be addressed by special handlers regardless of their countries (I know that doesn't make that much sense but just bear with me 😄).

The if-based Selector's implementation might be something like:

public class DataHandlerSelector {

    // Constructor, properties and Assert class omitted for brevity

    public DataHandler obtainDataHandlerFor(String url) {
        Assert.notNull(url, () -> "Url is null!");

        DataHandler dataHandlerToReturn;

        // .gov and .org must come first
        if (url.contains(".gov")) {
            dataHandlerToReturn = governmentDataHandler;
        } else if (url.contains(".org")) {
            dataHandlerToReturn = organizationDataHandler
        } else if (url.contains(".br")) {
            dataHandlerToReturn = brazilianDataHandler;
        } else if (url.contains(".ar")) {
            dataHandlerToReturn = argentinianDataHandler;
        } else if (url.contains(".au")) {
            dataHandlerToReturn = australianDataHandler;
        } else if (url.contains(".fr")) {
            dataHandlerToReturn = frenchDataHandler;
        // Lots of other countries...
        } else {
            throw new IllegalArgumentException("Url not supported!");
        }

        return dataHandlerToReturn;
    }
}

Easy to implement right? But the way I see it, in this case the new added business rule is enforced only by that comment, which may or may not stand the test of time. We might be able to extract a method to put the first two ones in a different place but I do begin to smell spaghetti... We can discuss better solutions for this pattern in the comments if you have any.

And what about with the IoC Selector pattern?

Well, we might just create two lists:


public class DataHandlerSelector {

   private final List<DataHandler> firstHandlersList;
   private final List<DataHandler> secondHandlersList;

   public DataHandlerSelector(GovernmentDataHandler governmentDataHandler,
                              OrganizationDataHandler organisationDataHandler,
                              BrazilianDataHandler brazilianDataHandler,
                              ArgentinianDataHandler argentinianDataHandler,
                              AustralianDataHandler australianDataHandler,
                              FrenchDataHandler frenchDataHandler) {

        firstHandlersList = List.of(governmentDataHandler,
                                  organisationDataHandler);

        secondHandlersList = List.of(brazilianDataHandler,
                                  argentinianDataHandler,
                                  australianDataHandler,
                                  frenchDataHandler);


    public DataHandler obtainDataHandlerFor(String url) {
        Assert.notNull(url, () -> "Url is null!");
        return firstHandlersList
                .stream()
                .filter(handler -> handler.shouldHandle(url))
                .findFirst()
                .orElseGet(() -> getFromSecondHandlerList(url));
    }

    private DataHandler getFromSecondHandlerList(String url) {
        return secondHandlersList
                           .stream()
                           .filter(handler -> handler.shouldHandle(url))
                           .findFirst()
                           .orElseThrow(() -> new IllegalArgumentException("Url not supported!"));
    }

    private static class Assert {
        public static void notNull(Object input, Supplier<String> errorMessageSupplier) {
            if (input == null) throw new IllegalArgumentException(errorMessageSupplier.get());
        }
    }
}

It sure is easier to code the first pattern, but in which do you think the new business rule is more clear? In my option, dealing with well reasoned patterns and interfaces usually leads to more reflection than action, and thus probably to better, more maintainable code, and with a clearer set of business rules and responsibilities.

But, of course, every pattern has its day of glory when applied in the proper situation, right?

So, what do you think are the pros and cons of each pattern? What points I made do you agree or disagree with? Is there another requirement change we should compare the two patterns against?

Let's address this in the comments!

Thanks a lot to @wrldwzrd89 , @khtony , @bertilmuth and @t_dardzhonov for the early feedback on this post!

Discussion (21)

Collapse
bertilmuth profile image
Bertil Muth

Hi Tomaz.
Thanks for the article, I liked reading it. You do a great job getting your point across.

If you are open to some improvement suggestions, here are some.

The code as you provide it doesn't compile. This may be confusing to Java newcomers.
The DataHandler interface uses the IncomingData and ProcessedData types in the handleData() method, but the BrazilianDataHandler uses String types for the same method.
All but the first occurence of the DataHandlerFactory class miss the Assert class - at least a comment would be helpful there (like // rest of class omitted for brevity).

Concerning the pattern itself: the factory could be simplified to take any number of DataHandler arguments, like so:

public class DataHandlerFactory {

    private final List<DataHandler> dataHandlerList;

    public DataHandlerFactory(DataHandler... dataHandlers) {
        dataHandlerList = Arrays.asList(dataHandlers);
    }

    public DataHandler obtainDataHandlerFor(String url) {
        Assert.notNull(url, () -> "Url is null!");
        return dataHandlerList
                .stream()
                .filter(handler -> handler.shouldHandle(url))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Url not supported!"));

    // You also check if more than one instance has been found by using List.size() on the list returned by a collect(toList()) at the end of the stream call.
    }

    private static class Assert {
        public static void notNull(Object input, Supplier<String> errorMessageSupplier) {
            if (input == null) throw new IllegalArgumentException(errorMessageSupplier.get());
        }
    }
}

That way, you wouldn't even need to adapt it when a new handler is introduced.

Collapse
tomazfernandes profile image
Tomaz Lemos Author

Hi Bertil,

Thanks for your feedback! The String input and return types where wrong leftovers from a earlier version, I have fixed it, and also put the comment where the Assert class is missing.

By saying the code doesn’t compile, you mean you think I should provide implementation to all classes, such as the handlers, IncomingData and ProcessingData? Perhaps that would introduce too much not important code? Or at least I should make clear I’m not providing those classes.

The constructor idea is indeed better, I’ll change it as soon as I get to my computer. In my original post I used Spring for DI and received a List in the constructor, but a fellow DEV correctly suggested I should make the examples framework agnostic, and I missed this improvement.

Thanks a lot for your feedback!

Collapse
bertilmuth profile image
Bertil Muth

Hi Tomaz.

By saying the code doesn’t compile, I really just meant the types and the missing Assert class (as described in my comment).

For me, the incompleteness is less of a problem.

Thanks for reacting so quickly, and happy new year :-) !

Thread Thread
tomazfernandes profile image
Tomaz Lemos Author

Thanks a lot Bertil, and happy new year to you too!

Collapse
wrldwzrd89 profile image
Eric Ahnell

I like this idea a lot, effectively, you're registering, iterating and picking with a lot less overhead / boilerplate. You can call it a lot of things - it is a form of dependency injection, though not the one I'm used to - but it's definitely a way to clean up what would otherwise be a tangled knot of logic. I approve!

Collapse
tomazfernandes profile image
Tomaz Lemos Author

Hi Eric, I’m really glad you liked it!

Yeah, I’ve always thought of this pattern as a kind of factory in that it provides a specific implementation based on a parameter, but it does lack the characteristic of instantiating the classes which is the basis for a factory, so I guess we’ll have to come up with a better name!

Thanks a lot for your support!

Collapse
tomazfernandes profile image
Tomaz Lemos Author

Hi Trifon!

Are you suggesting to put the condition directly in the handleData method? Then how would the factory know what instance to return? I do like the Tell-don’t-ask principle, but didn’t get how you’d apply it here.

We might put a if (!shouldHandle(url)) throw ... in the beginning of the handleData method to offer some protection against fools.

As I said I’ve borrowed this pattern from Spring, in an earlier version I had put a piece of their code, perhaps I’ll put it again to illustrate how they use it.

Thanks a lot for your feedback!

tomazfernandes profile image
Tomaz Lemos Author

Perhaps in this case a Chain of Responsibility would be a better pattern? The instance tries to handle the input, if it can't it relays to the next in the chain...

About can and should, I guess it's a matter of semantics... Maybe an instance could handle an input but shouldn't? That's a good point to reason about.

I'm really glad you liked the idea, thanks a lot for your feedback!

Collapse
tomazfernandes profile image
Tomaz Lemos Author

That's a fairly good point. Without DI this class would probably indeed have the responsibility of instantiating those classes, but the DI pattern we're using implies the instances will be instantiated somewhere else (Spring's container, for instance) and injected through the constructor.

Thread Thread
tomazfernandes profile image
Tomaz Lemos Author

Maybe we could have the DataHandlerSelector, and then a DataHandlerSelectorFactory to do the actual instantiation of all the classes involved, I guess it would be more accurate.

Collapse
faridkhan profile image
Farid-khan

Hello Tomaz. I want to create a program which will find the day of week for the given date using function? Could you help me?

Collapse
tomazfernandes profile image
Tomaz Lemos Author

Hi Farid! I think StackOverflow (stackoverflow.com/) is a better place for this kind of question, why don’t you post your question there?

Don’t forget to mention the language you’ll be using and to provide a little more context on what you need.

Then if you want post the link to your question back here so we can help you!

Best of luck!

Collapse
faridkhan profile image
Farid-khan

Oh okay sir.

Stay healthy and blessed!

Collapse
eth2234 profile image
Ebrahim Hasan

Hi thomaz, I fall in love with this approach, how do you think I could get better at it? Already did it to a lot of codes but want to be better!

Collapse
tomazfernandes profile image
Tomaz Lemos Author

Hi EthTS, I’m glad you liked it! I have added some more class implementations to the code so it’s easier to compile an example, perhaps you should try that, what do you think? Do you have any specific doubts about it?

Thanks for your feedback!

Collapse
eth2234 profile image
Ebrahim Hasan

Wow, thanks for taking the time to add the new class implementations! this sounds amazing, I will actually convert some of my clients websites/projects into that pattern!

Thread Thread
tomazfernandes profile image
Tomaz Lemos Author

Great EthTS, feel free to ask if you have any doubts, and thanks again you for your feedback!

Collapse
ziabatimane profile image
Ziabat Imane

Hello Tomaz.

Thank you, I liked your idea.

Wondering how can we use it in case we have many levels of nested if-else, something like :

If condition A is met then do something
If condition A and condition B is met do something else
If condition A is met and condition C is met perform some different action