Replace Conditional with Polymorphism

Replace Conditional with Polymorphism

So, there is a good chance that you’ve used parametric polymorphism already — it’s?basically just generics. But, do you know how generics can help you reduce the use of branching statements, such as?if-else?and other control-flow statements?

if-else?is often a poor choice, especially if you want to follow the Open-Closed principle, making your application easily extensible. We’ll get into why this is, in the following section. (I have a?list of articles on this subject as well)

Back as a junior developer, I’d often resort to traditional branching techniques, plastering?if-else,?and?switch?statements all over the place.

And, I get why you’re likely doing it as well. It’s the easy thing to do. It doesn’t require you to think hard about design and interactions. All possible paths are immediately visible to you, so you don’t have to dig thru layers of abstraction and track down implementations.

But…

Control flow statements are deceivingly simple, and you pay a high price for it, later.


Any time you add an?else?or another?case, it’s like mortgaging your code quality. You’re likely incurring tech debt your future self has to pay off, or worse, your team has to pay off.

“So, what are we going to build?”

Imagine you’re tasked to build some functionality to store user-provided files.


No alt text provided for this image

I assume everyone can relate to this — or, at least has a good idea of what this entails. But, basically, you’ll receive some bytes, a file name, and an indication of the desired storage medium.

This is our initial implementation using branching statements.

Our initial, naive, “status-quo”, branching littered solution might look like something as below, where we simply branch over a discrete value.

No alt text provided for this image


Now, this implementation is not terrible in every aspect.

We got some good practices going, such as explicit dependencies, relying on interfaces rather than implementations, strongly typed configurations, early returns, defensive coding, named conditionals, and mayfly variables.

The overall code quality is okay (despite looking insane), however, there are some major drawbacks of this initial approach.

Let’s first start with?cognitive and cyclomatic complexity, the commonly used objective measures to determine code quality.

With only two branches we still manage to hit a cyclomatic complexity of 9 and 7 cognitive complexity. We’re right at the border of the recommended threshold of 10, according?to ISO 26262.

Also, notice how we have branch-coupled dependencies. The?FileSystemSavingOptions?is only used when persisting to the file system and?IFileRepository?is only used when using a database. This gets nasty real quick.

When working in a larger software organization, you’ll likely need to share this module with many other projects managed by other teams. Maintenance hell is the natural corollary of this implementation. Imagine one team wants to persist files in azure blob storage, another wants to use MongoDB.

Our naive implementation breaks with the Open/Closed principle, dictating existing classes should be closed for modification and new functionality is implemented with new classes.

You see, this quickly gets out of hand, and adding new branches is not the solution, not to mention how test efforts shot thru the roof with each additional branch.

“How can generics help us overcome this madness?”

Traditional branching techniques often pose maintainability nightmares in the long run.


I think this is why you rarely see?if-else,?goto, and?switch, in business-line, enterprise applications.

An alternative, and frankly, much easier to maintain solution, is to start thinking in terms of components and interactions, rather than simple procedural instructions.

No alt text provided for this image

You can compose a?seedworks solution, that lays the foundation for others to build upon.

With this approach, we’re shifting our focus towards understanding a system, rather than an ever-growing, hard-to-maintain method. This also allows us to read and comprehend each component in isolation from the others.

The alternative approach I’m proposing here requires us to have three main components, a file saver, context, and a sink.

So, our seedworks look something like this. I’ll get into details after the code snippet.

No alt text provided for this image

The?FileSaver?is our single entry to saving files. It’ll pick a sink based on the context type passed to?StoreFile(T). But, before it can actually pick a sink, it’ll need to have pre-registered sinks. So, we provide a method to register a sink associated with a?FileContext?type.

Sinks are implemented by inheriting from the?FileSink<T>?base class and it operates on a?FileContext?subtype.

Our foundation is now nicely laid out at this point. It’s easy to maintain, and more importantly, our colleagues in other teams can now define their own sinks and no longer rely on you to implement new functionality.

But, as I’m sure you’ve already noticed, there’s no real functionality yet. It’s all just “plumbing” code so far. It’s the stuff others shouldn’t need to see, deal with, or even want to think about.

With the foundation in place, let’s see how easily you can implement both the “save to file system” and “in database” functionality.

No alt text provided for this image

And then we have our file system functionality.


No alt text provided for this image

The implementations are similar to our previous, inextensible, branch-based design.

So, what do we gain from this?

  1. To start with, we no longer have branch-coupled dependencies. Each piece of functionality now only takes the exact dependencies required to carry out its work.
  2. Testing is a breeze due to much lower cognitive and cyclomatic complexity, dropping from 9 to 1 for the database sink and 4 for the file system sink.
  3. You adhere to the open/closed principle. Others can now easily implement their own functionality and register the sink with the FileSaver class.

I know lots of curly braces app coders have little appreciation for plumbing and the effort it takes to land an intuitive, elegant design others can easily implement.

“But, this solution is way too over-engineered!”

Sure. It might feel like that to some.


If you’re a solo developer, duck-taping your way thru a personal project, then this is possibly not what you want.

I’m sure there are even the few senior developers or misguided architects out there proclaiming you should?only write code a junior can understand, which is obviously bad advice.

Plumbing code is nonetheless what makes you build your apps faster, so you might as well get some experience writing this type of code by yourself unless you only ever want to rely on what’s available on NuGet, npm, packagist, etc.

Parting words.

When you need to implement additional functionality, try to take a few seconds and reflect on how you can add to your system without modifying existing code.?Adding new code is typically safer than modifying existing code.


There’s surely an initial upfront cost of coming up with an elegant, extensible solution, but it’s definitely worth it.

Feel free to leave a response and let me hear what you think about hardcoding branches vs. flexible open/closed design.

In Java :


Replace Conditional with Polymorphism



Problem

You have a conditional that performs various actions depending on object type or properties.


class Bird {
  // ...
  double getSpeed() {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new RuntimeException("Should be unreachable");
  }
}        


Solution

Create subclasses matching the branches of the conditional. In them, create a shared method and move code from the corresponding branch of the conditional to it. Then replace the conditional with the relevant method call. The result is that the proper implementation will be attained via polymorphism depending on the object class.

abstract class Bird {
  // ...
  abstract double getSpeed();
}

class European extends Bird {
  double getSpeed() {
    return getBaseSpeed();
  }
}
class African extends Bird {
  double getSpeed() {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.getSpeed();        

Why Refactor

This refactoring technique can help if your code contains operators performing various tasks that vary based on:

  • Class of the object or interface that it implements
  • Value of an object’s field
  • Result of calling one of an object’s methods

If a new object property or type appears, you will need to search for and add code in all similar conditionals. Thus the benefit of this technique is multiplied if there are multiple conditionals scattered throughout all of an object’s methods.

Benefits

  • This technique adheres to the?Tell-Don’t-Ask?principle: instead of asking an object about its state and then performing actions based on this, it’s much easier to simply tell the object what it needs to do and let it decide for itself how to do that.
  • Removes duplicate code. You get rid of many almost identical conditionals.
  • If you need to add a new execution variant, all you need to do is add a new subclass without touching the existing code (Open/Closed Principle).

How to Refactor

Preparing to Refactor

For this refactoring technique, you should have a ready hierarchy of classes that will contain alternative behaviors. If you don’t have a hierarchy like this, create one. Other techniques will help to make this happen:

  • Replace Type Code with Subclasses. Subclasses will be created for all values of a particular object property. This approach is simple but less flexible since you can’t create subclasses for the other properties of the object.
  • Replace Type Code with State/Strategy. A class will be dedicated for a particular object property and subclasses will be created from it for each value of the property. The current class will contain references to the objects of this type and delegate execution to them.

The following steps assume that you have already created the hierarchy.

Refactoring Steps

  1. If the conditional is in a method that performs other actions as well, perform?Extract Method.
  2. For each hierarchy subclass, redefine the method that contains the conditional and copy the code of the corresponding conditional branch to that location.
  3. Delete this branch from the conditional.
  4. Repeat replacement until the conditional is empty. Then delete the conditional and declare the method abstract.

Thanks To : refactoring.guru

要查看或添加评论,请登录

Omar Ismail的更多文章

  • OAuth Grant Types (Authorization Code Grant)

    OAuth Grant Types (Authorization Code Grant)

    The authorization code grant type is used to obtain both access tokens and refresh tokens. The grant type uses the…

  • What is Apache Kafka?

    What is Apache Kafka?

    Reference : https://www.qlik.

    2 条评论
  • Multi-Tenant Architecture in a Nutshell

    Multi-Tenant Architecture in a Nutshell

    Thanks to the original writer and article :…

  • Microservices Communication!

    Microservices Communication!

    Thanks To: https://medium.com/design-microservices-architecture-with-patterns/microservices-communications-f319f8d76b71…

    2 条评论
  • What Are the New Features of SpringBoot3 ?

    What Are the New Features of SpringBoot3 ?

    Thanks to : https://medium.com/javarevisited/what-are-the-new-features-of-springboot3-6ddba9af664 1.

    1 条评论
  • OAuth 2.0!

    OAuth 2.0!

    Thanks to the original writer : https://medium.com/@isharaaruna OAuth2.

    2 条评论
  • How to Draw a Technical Architecture Diagram

    How to Draw a Technical Architecture Diagram

    Thanks to the original writer and article : https://levelup.gitconnected.

    2 条评论
  • Event Sourcing Versus Event-Driven Architecture

    Event Sourcing Versus Event-Driven Architecture

    Thanks to the original writer and article :…

  • Best Practices For Your API Versioning Strategy

    Best Practices For Your API Versioning Strategy

    API versioning is critical. But do you know all of the API versioning best practices? Is your API versioning strategy…

    1 条评论
  • Enterprise Architecture Tools

    Enterprise Architecture Tools

    Thanks to the original writer and article : https://medium.com/geekculture/enterprise-architecture-tools-b8165c8c9d7…

社区洞察

其他会员也浏览了