avatarNGU

Summary

This article provides 16+ ways to gracefully refactor if-else blocks, including removing unnecessary else, using the ternary operator, merging conditional expressions, switch-case statements, guard functions, value distribution, Optional, enumeration, reflection mechanisms, polymorphism, strategy pattern, responsibility chain mode, and table-driven method.

Abstract

The article "16+ ways to gracefully refactor if-else block" discusses various techniques to improve the readability and maintainability of code by refactoring if-else blocks. The author provides examples of each technique, including removing unnecessary else, using the ternary operator, merging conditional expressions, switch-case statements, guard functions, value distribution, Optional, enumeration, reflection mechanisms, polymorphism, strategy pattern, responsibility chain mode, and table-driven method. The article also mentions advanced methods such as event-driven, rule engine, and finite state machine for complex business processes.

Bullet points

  • The article provides 16+ ways to refactor if-else blocks.
  • Removing unnecessary else can simplify code and improve readability.
  • The ternary operator can be used to simplify if-else statements.
  • Merging conditional expressions can make the logic clearer.
  • Switch-case statements can handle multiple conditions and make the code more concise.
  • Guard functions can protect other functions from invalid input.
  • Value distribution can simplify the assignment of new values to variables.
  • Optional can be used to handle non-empty judgment caused by if-else.
  • Enumeration can optimize the if-else logical branch.
  • Reflection mechanisms can obtain information about classes and methods to dynamically execute different code.
  • Polymorphism can abstract common methods into a public interface.
  • The strategy pattern can encapsulate different processing logic into different strategy classes.
  • The responsibility chain mode can effectively reduce the use of if-else and improve the simplicity, extensibility, and flexibility of code.
  • The table-driven method can separate the conditional logic from the processing logic and store the conditional logic in a table.
  • Advanced methods such as event-driven, rule engine, and finite state machine can be used for complex business processes.

16+ ways to gracefully refactor if-else block

Some of them you may not know

if-else is a very common code construct in our daily development, but it is often used incorrectly or inappropriately. This article will explain in detail how to gracefully and properly refactor.

Let’s look directly go deep in code:

Remove unnecessary else

You will often find that else blocks are not actually needed at all.

//before
public void remove_unnecessary_else_example(Integer input) {  
    if (input > 10) {  
        //do sth  
    } else {  
        //do sth else  
    }  
}  


//after  
public void remove_unnecessary_else_solution(Integer input) {  
    if (input > 10) {  
        //do sth  
        return;  
    }  
  
    //do sth else  
}

The ternary operator

The use of ternary operators can simplify some if-else, making the code more concise and readable.

//before
public Integer the_ternary_operator_example(Integer input) {  
    if (input > 10) {  
        return 50;  
    } else {  
        return 100;  
    }  
}  

//after  
public Integer the_ternary_operator_solution(Integer input) {  
    return input>10 ? 50 : 100;  
}

Merge conditional expressions

If you have a series of conditions that return the same result, you can combine them into a single conditional expression to make the logic clearer

//before
public Integer merge_conditional_expression_example(Player input) {  
    if(input.getAge() <18) {  
        return 5;  
    }  
  
    if (input.getHeight() < 1.80) {  
        return 5;  
    }  
  
    if (input.getWeight() < 50) {  
        return 5;  
    }  
  
    //do sth  
    return 0;  
}  
  
//after
public Integer merge_conditional_expression_solution(Player input) {  
    if(input.getAge() <18 || input.getHeight() < 1.80 || input.getWeight() < 50) {  
        return 5;  
    }  
  
    //do sth  
    return 0;  
}

switch-case: The most common way

switch-case statements can be used to handle multiple conditions, and the code is more concise and readable

//before
public Integer switch_example(Integer input) {  
    if (input == 1) {  
        //do sth 1  
    }  
  
    if(input == 2) {  
        //do sth 2  
    }  
  
    if(input ==3 ) {  
        //do sth 3  
    }  
  
    return 0;  
}  
  
//after
public Integer switch_solution(Integer input) {  
    switch (input) {  
        case 1: //do sth 1  
        case 2: //do sth 2  
        case 3: //do sth 3  
        default: return 0;  
    }  
}

Guard function

A guard function is a function that is used to protect other functions from invalid input. It does this by checking the input for validity and throwing an exception if the input is invalid. In a word, “return as soon as possible.”

//before
public Integer guard_function_example(Player input) {  
    if(input != null) {  
        String name = input.getName();  
        if(StringUtils.hasText(name)){  
            if(input.getAge() >18) {  
                return 60;  
            }  
        }  
    }  
}  
  
//after
public Integer guard_function_solution(Player input) {  
    if(input == null) {  
        return 0;  
    }  
  
    if(!StringUtils.hasText(input.getName())){  
        return 0;  
    }  
  
    if(input.getAge() <=18) {  
        return 0;  
    }  
    return 60;  
}

Value distribution

If you are assigning a new value to a variable based on some input provided, stop using if-else and take a more readable approach. If we don’t use else, we’ll be left with clean, readable code.

//before
public Integer value_assignment_example(Integer input) {  
    if (input == 1) {  
        return 10;  
    } else if(input == 2) {  
        return 20;  
    } else {  
        return 0;  
    }  
}  
  
//after
public Integer value_assignment_solution(Integer input) {  
    if(input == 1) return 10;  
    if(input == 2) return 20;  
    return 0;  
}

Optional

Sometimes if-else is more, because of the non-empty judgment caused, in this case you can use the Optional

@Slf4j  
public class OptionalWay {  
  
    public static void main(String[] args) {  
        OptionalWay optionalWay = new OptionalWay();  
  
        String s = "test";  
  
        optionalWay.oldway(s);  
        optionalWay.newway(s);  
    }  
  
    private void errorHandle() {  
        log.error("exist a null");  
    }  
  
    private void keepWorking(String s) {  
        log.info(s);  
    }  
  
    //before
    private void oldway(String s) {  
        if(s != null) {  
            keepWorking(s);  
        } else {  
            errorHandle();  
        }  
    }  
  
    //after
    private void newway(String s) {  
        Optional<String> strOptional = Optional.of(s);  
        strOptional.ifPresentOrElse(this::keepWorking, this::errorHandle);  
    }  
  
}

Enumeration: In some cases, the use of enumerations can also optimize the if-else logical branch

public class EnumWay {  
  
    public static void main(String[] args) {  
        EnumWay enumWay = new EnumWay();  
  
        enumWay.oldWay(18);  
        enumWay.newWay(18);  
    }  


    //before  
    private Integer oldWay(int i) {  
        if(i==18){  
            return 1;  
        }else if(i == 20){  
            return 2;  
        }else if(i==30){  
            return 3;  
        }  
        return 0;  
    }  



    //after
    private Integer newWay(int i) {  
        return AgeValueEnum.of(i).getValue();  
    }  
  
  
    public enum AgeValueEnum {  
        YOUND(18,1),  
        MID(20,2),  
        OLD(30,3);  
  
        private int age;  
        private int value;  
  
        public int getAge() {  
            return age;  
        }  
  
        public int getValue() {  
            return value;  
        }  
  
        AgeValueEnum(int age, int value){  
            this.age = age;  
            this.value =value;  
        }  
  
        static AgeValueEnum of(int age) {  
            for (AgeValueEnum temp : AgeValueEnum.values()) {  
                if (temp.getAge() == age) {  
                    return temp;  
                }  
            }  
            return null;  
        }  
    }  
}

Intermediate

Reflex mechanism

Reflection mechanisms can obtain information about classes and methods to dynamically execute different code.

//before
public class Shape {

    public double getArea() {
        if (this instanceof Circle) {
            return Math.PI * ((Circle) this).getRadius() * ((Circle) this).getRadius();
        } else if (this instanceof Square) {
            return ((Square) this).getSide() * ((Square) this).getSide();
        } else if (this instanceof Triangle) {
            return 0.5 * ((Triangle) this).getBase() * ((Triangle) this).getHeight();
        } else {
            throw new IllegalArgumentException("Unknown shape type!");
        }
    }
}

public class Circle extends Shape {

    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

public class Square extends Shape {

    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }
}

public class Triangle extends Shape {

    private double base;
    private double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    public double getBase() {
        return base;
    }

    public double getHeight() {
        return height;
    }
}

public class Main {

    public static void main(String[] args) {
        Shape circle = new Circle(5);
        System.out.println("Circle area: " + circle.getArea());

        Shape square = new Square(4);
        System.out.println("Square area: " + square.getArea());

        Shape triangle = new Triangle(3, 4);
        System.out.println("Triangle area: " + triangle.getArea());
    }
}




//after
public abstract class Shape {

    public abstract double getArea();

    public static Shape getShape(String shapeType) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Class<?> clazz = Class.forName(shapeType);
        return (Shape) clazz.newInstance();
    }
}

public class Circle extends Shape {

    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

public class Square extends Shape {

    private double side;

    public Square(double side) {
        this.side = side;
    }

    public double getSide() {
        return side;
    }

    @Override
    public double getArea() {
        return side * side;
    }
}

public class Triangle extends Shape {

    private double base;
    private double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    public double getBase() {
        return base;
    }

    public double getHeight() {
        return height;
    }

    @Override
    public double getArea() {
        return 0.5 * base * height;
    }
}

public class Main {

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Shape circle = Shape.getShape("Circle");
        circle.setRadius(5);
        System.out.println("Circle area: " + circle.getArea());

        Shape square = Shape.getShape("Square");
        square.setSide(4);
        System.out.println("Square area: " + square.getArea());

        Shape triangle = Shape.getShape("Triangle");
        triangle.setBase(3);
        triangle.setHeight(4);
        System.out.println("Triangle area: " + triangle.getArea());
    }
}

polymorphism

We can abstract some common methods of some operations (such as some states) into a public interface, and then implement these interfaces for these operations to complete different logic, in the call we only need to pass the corresponding operation class, the external operation method is the same.

//before
public class Animal {

    public void makeSound() {
        if (this instanceof Dog) {
            System.out.println("woof!");
        } else if (this instanceof Cat) {
            System.out.println("meow!");
        } else if (this instanceof Cow) {
            System.out.println("moo!");
        } else {
            System.out.println("UNKNOWN!");
        }
    }
}

  

public class Dog extends Animal {

}

  

public class Cat extends Animal {

}

  

public class Cow extends Animal {  

}

  

public class Main {
  
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.makeSound();
  
        Animal cat = new Cat();
        cat.makeSound();

        Animal cow = new Cow();
        cow.makeSound();
    }

}


//after


public abstract class Animal {
  
    public abstract void makeSound();
} 

public class Dog extends Animal {

    @Override
    public void makeSound() {
        System.out.println("woof!");
    }
}

public class Cat extends Animal {

    @Override
    public void makeSound() {
        System.out.println("meow!");
    }
}

public class Cow extends Animal {

    @Override
    public void makeSound() {
        System.out.println("moo!");
    }
}
  
public class Main {

    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.makeSound();
  
        Animal cat = new Cat();
        cat.makeSound();
  
        Animal cow = new Cow();
        bird.makeSound();
    }
}

Strategy pattern

The policy pattern can encapsulate different processing logic into different policy classes, thus avoiding the use of multiple if else code blocks

//before
public class StrategyMain {  
  
    public static void main(String[] args) {  
  
        oldway(18);  
        newway(18);  
    }  
  
    private static void oldway(Integer age) {  
        if(age.equals(18)) {  
            //do sth young  
        }else if(age.equals(20)){  
            //do sth mid  
        }else if(age.equals(30)){  
            //do sth old  
        }else {  
            //do sth else  
        }  
    }  
  
    private static void newway(Integer age) {  
        IAgeService ageService = AgeServiceFactory.getAgeServiceImpl(age);  
        ageService.value();  
    }  
}

//after
public interface IAgeService {  
    void value();  
}

public class YoungAgeServiceImpl implements IAgeService {  
    @Override  
    public void value() {  
        // do sth young  
    }  
}

public class MidAgeServiceImpl implements IAgeService {  
    @Override  
    public void value() {  
        // do sth mid  
    }  
}

public class OldAgeServiceImpl implements IAgeService {  
    @Override  
    public void value() {  
        // do sth old  
    }  
}

public class AgeServiceFactory {  
    private static final Map<Integer, IAgeService> map = new HashMap<>();  
    static {  
        map.put(18, new YoungAgeServiceImpl());  
        map.put(20, new MidAgeServiceImpl());  
        map.put(30, new OldAgeServiceImpl());  
    }  
    public static IAgeService getAgeServiceImpl(Integer age) {  
        return map.get(age);  
    }  
}

Responsibility chain mode

The responsibility chain pattern is an important design pattern, which can effectively reduce the use of if-else and improve the simplicity, extensibility and flexibility of code. When the conditional expression in if-else is too flexible to abstract the data in the condition into a table and judge it in a uniform way, then the judgment of the condition should be given to each functional component. These components are connected in series in the form of chains to form a complete function.

//before
public class FileHandler {

    public void handle(File file) {
        if (file instanceof TextFile) {
            System.out.println("Handle text file");
        } else if (file instanceof ImageFile) {
            System.out.println("Handle image file");
        } else if (file instanceof VideoFile) {
            System.out.println("Handle video file");
        } else {
            throw new IllegalArgumentException("Unknown file type!");
        }
    }
}

public class TextFile extends File {

}

public class ImageFile extends File {

}

public class VideoFile extends File {

}

public class Main {

    public static void main(String[] args) {
        FileHandler fileHandler = new FileHandler();

        TextFile textFile = new TextFile();
        fileHandler.handle(textFile);

        ImageFile imageFile = new ImageFile();
        fileHandler.handle(imageFile);

        VideoFile videoFile = new VideoFile();
        fileHandler.handle(videoFile);

        UnknownFile unknownFile = new UnknownFile(); 
        fileHandler.handle(unknownFile);
    }
}


//after
public interface Handler {

    void handle(File file);

    void setNextHandler(Handler nextHandler);
}

public class TextFileHandler implements Handler {

    private Handler nextHandler;

    @Override
    public void handle(File file) {
        if (file instanceof TextFile) {
            System.out.println("Handle text file");
        } else {
            if (nextHandler != null) {
                nextHandler.handle(file);
            }
        }
    }

    @Override
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }
}

public class ImageFileHandler implements Handler {

    private Handler nextHandler;

    @Override
    public void handle(File file) {
        if (file instanceof ImageFile) {
            System.out.println("Handle image file");
        } else {
            if (nextHandler != null) {
                nextHandler.handle(file);
            }
        }
    }

    @Override
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }
}

public class VideoFileHandler implements Handler {

    @Override
    public void handle(File file) {
        if (file instanceof VideoFile) {
            System.out.println("Handle video file");
        }
    }
}

public class Main {

    public static void main(String[] args) {
        Handler textFileHandler = new TextFileHandler();
        Handler imageFileHandler = new ImageFileHandler();
        Handler videoFileHandler = new VideoFileHandler();

        textFileHandler.setNextHandler(imageFileHandler);
        imageFileHandler.setNextHandler(videoFileHandler);

        TextFile textFile = new TextFile();
        textFileHandler.handle(textFile);

        ImageFile imageFile = new ImageFile();
        textFileHandler.handle(imageFile);

        VideoFile videoFile = new VideoFile();
        textFileHandler.handle(videoFile);

        UnknownFile unknownFile = new UnknownFile(); 
        textFileHandler.handle(unknownFile);
    }
}

Table driven method

A programming pattern that essentially separates the conditional logic from the processing logic and stores the conditional logic in a table, thereby avoiding the use of numerous if-else statements.

public String doAction1(Player input) {  
    //do sth 1  
    return "sth 1";  
}  
  
public String doAction2(Player input) {  
    //do sth 2  
    return "sth 2";  
}  
  
public String doAction3(Player input) {  
    //do sth 3  
    return "sth 3";  
}

//before
public void table_driven_example(Player input) {  
    Integer age = input.getAge();  
    if (age.equals(18)) {  
        doAction1(input);  
    } else if (age.equals(20)) {  
        doAction2(input);  
    } else if (age.equals(30)) {  
        doAction3(input);  
    }  
}


//after
public void table_driven_solution(Player input) {  
    Map<Integer, Function> actionMappings = new HashMap<>();  
  
    actionMappings.put(18, (someParams) -> doAction1(input));  
    actionMappings.put(20, (someParams) -> doAction2(input));  
    actionMappings.put(30, (someParams) -> doAction3(input));  
  
    actionMappings.get(input.getAge()).apply(input);  
}

Advanced

For some scenarios with complex business processes that require dynamic processing, the following advanced methods can be considered. Since these methods need to be combined with specific business and frameworks, the code for these methods will not be demonstrated in this article. If you are interested, you can refer to some my previous articles:

Event-Driven

By associating different event types with corresponding processing mechanisms, complex logic can be implemented and decoupling can be achieved.

Implementation method: Guava / Spring / message queue.

You can refer to this article for further understanding:

Rule Engine

A rule engine allows you to define and dynamically execute rules, processing business logic by arranging conditions. It is suitable for processing complex business logic.

Recommended frameworks: Drools, mvel, LiteFlow…

Finite State Machine

A state machine can also be seen as a table-driven approach. It is a mapping between the current state and events, and the processing function. Of course, there will also be a state transition processing after the processing is successful.

Applicable scenarios: State machines have natural advantages in functions such as protocol stacks and order processing. This is because these scenarios naturally have states and state flows.

You can refer to this article for further understanding:

Thanks for reading!

If you like it or feel it helped pls click Applaud. Thanks :)

Happy coding. See you next time.

If you enjoyed this article, consider trying out the AI service I recommend. It provides the same performance and functions to ChatGPT Plus(GPT-4) but more cost-effective, at just $6/month (Special offer for $1/month). Click here to try ZAI.chat.

Programming
Coding
Development
Developer
Architecture
Recommended from ReadMedium