The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Classes and objects
Strategy pattern
What you have to change when one more payment method is added
Each payment method (card, bank transfer, Kakao Pay) is processed in a different way. One way to write this is to branch with if-else. Another is to keep a separate implementation for each payment method and swap the implementations in and out (the Strategy pattern).
Checked against real execution · Checked with code of the same structure on JDK 21
Concepts in this lab
- Strategy pattern
- A pattern that defines an algorithm or behavior that may change as an interface and implements each variant as a separate class, so that it can be swapped at run time. The code that uses it depends only on the interface, so adding a new variant does not require changing the existing code.
- Design pattern
- A proven solution to a problem that comes up repeatedly in software design, given a name and written down. These solutions serve as a shared vocabulary among developers, so a design intention can be communicated briefly. Each one is a means of solving a specific problem, so applying one where it is not needed only adds complexity.
- Polymorphism
- This is the property that calling the same method through a reference of the same type runs different behavior depending on the kind of the actual object. A variable of a supertype (an interface or a superclass) can hold objects of several subtypes, and which implementation runs is decided at run time (dynamic method lookup). It lets you add a new implementation without changing the calling code.
- SOLID
- These are five principles of object-oriented design. They are the Single Responsibility Principle (SRP: a class has only one reason to change), the Open-Closed Principle (OCP: extend without modifying existing code), the Liskov Substitution Principle (LSP: a subtype must be able to replace its supertype), the Interface Segregation Principle (ISP: keep interfaces small so that code does not depend on methods it does not use), and the Dependency Inversion Principle (DIP: depend on abstractions, not on concrete classes). The goal is a structure that holds up well under change.
- Interface
- This is a type that defines the list of methods (the contract) that a class must implement. It cannot have instance variables, and one class can implement several of them. If the calling code depends only on this type, it is easy to change the implementing class or to replace it with a fake implementation for testing.
- Dependency injection (DI)
- This is an approach in which an object does not create the other objects it uses (its dependencies) but receives them from outside. In Spring, the container supplies the dependencies through a constructor, a setter, or a field as it creates a bean. It becomes easier to change the implementing class or to put in a fake object in a test, and the coupling between classes becomes lower.
Try these
- Default settings — Naver Pay is added to the if-else version. An existing method has to be modified.
- Adding Naver Pay to the Strategy pattern — You create one new class and you are done. The existing code stays as it is.
- How the Strategy pattern runs — The right implementation is selected without a conditional statement.
- How the if-else version runs — The conditions are compared one by one from the top.
Running with the default settings
- The handling of each payment method is written as if-else branches inside the methods of PaymentService. The refund and cancel methods have branches of the same shape.
- Adding Naver Pay: You have to insert one else if into each of the three methods pay(), refund(), and cancel(). Because you are modifying methods that already worked, you have to test card and Kakao Pay again as well. If you miss even one place, only that feature throws an exception at run time.
- A payment by Kakao Pay is requested: pay("KAKAO", order).
- The code compares type with "CARD". They do not match. Execution moves on to the next condition.
- The code compares type with "BANK". They do not match. Execution moves on to the next condition.
- The code compares type with "KAKAO". They match. The Kakao Pay handling code inside this block runs.
Result of this run
- The if-else version is the simplest and the easiest to read if there are two or three payment methods and no more will be added. The problem is that every time a payment method is added, the same branches are copied into several methods, a single method keeps getting longer, and the risk of touching one payment method while modifying another grows.
- Dependency injection in Spring is often used to swap strategies. A dependency is injected as an interface type, and the actual implementation changes with the configuration (the real payment provider in production, a fake implementation in tests).
Code
@Service
class PaymentService {
void pay(String type, Order order) {
if (type.equals("CARD")) {
// request approval from the card company ... (30 lines)
} else if (type.equals("BANK")) {
// bank transfer ... (25 lines)
} else if (type.equals("KAKAO")) {
// Kakao Pay ... (40 lines)
} else if (type.equals("NAVER")) { // added: an existing method is modified
// Naver Pay ... (35 lines)
} else {
throw new IllegalArgumentException(type);
}
}
// the same branches are repeated in refund() and cancel()
}
Assumptions of this model
The actual payment processing is left out, and only the structures are compared. That the if-else version has the same branches in refund and cancel as well is an assumption based on a common situation. The definition of the Strategy pattern follows the description in the GoF book Design Patterns, and injecting all the implementations of the same interface at once as a List is a standard Spring feature. Two things were confirmed with code of the same structure on JDK 21 on this PC: that the method of the implementation object taken from the map is the one that actually runs, and the number of comparisons in the if-else version. The part where Spring injects the implementations was not run.
Previous lab: Immutability and defensive copies · Next lab: == and equals