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.