Category Archives: Design Patterns

Reusable solutions to a commonly occurring problems in software design

Adapter

In programming, a common problem is an interface mismatch. A class exists, but its interface is not what the system expects. The Adapter design pattern lets classes with incompatible interfaces work together.

There is no need to rewrite code or change existing classes. The Adapter “translates” one interface into another. It does this without modifying existing code.

Let’s consider an application that shows a weather forecast: WeatherClient. The application needs an object that implements the WeatherService interface. It calls the method getCurrentTemperature() on that object.

public class WeatherClient {
    WeatherService weatherService;

    public WeatherClient(WeatherService weatherService) {
        this.weatherService = weatherService;
    }

    public void showTemperature (String city) {
        System.out.println(city + ": " + weatherService.getCurrentTemperature(city));
    }
}

The WeatherService interface declares 1method, getCurrentTemperature(). It returns a String:

public interface WeatherService {
    String getCurrentTemperature(String city);
}

But the weather data comes from an external API. The ExternalWeatherApi interface declares one method, getTemperatureCelsius(). It returns a double:

public interface ExternalWeatherApi {
    public double getTemperatureCelsius (String city);
}

To connect with API, the code needs an object that implements ExternalWeatherApi. But this object can’t be passed to WeatherClient, because WeatherClient expects object implementing WeatherService:

public class Main {
    public static void main (String[] args) {
        ExternalWeatherApi weatherApi = new ExternalWeatherApi() {
            @Override
            public double getTemperatureCelsius(String city) {
                return 15.2;
            }
        };

        // WeatherClient weatherClient = new WeatherClient(weatherApi); // ERROR
    }
}

Passing weatherApi to WeatherClient causes an error. The Adapter solves this problem.

Implementacja of the Adapter pattern

The Adapter translates an object which implements ExternalWeatherApi interface to WeatherService interface.
The class ExternalApiToWeatherServiceAdapter implements WeatherService interface (required by WeatherClient). It also has an ExternalWeatherApi object (needed to call the external API) as a field.

public class ExternalApiToWeatherServiceAdapter implements WeatherService{

    ExternalWeatherApi weatherApi;

    public ExternalApiToWeatherServiceAdapter(ExternalWeatherApi weatherApi) {
        this.weatherApi = weatherApi;
    }

    @Override
    public String getCurrentTemperature(String city) {
        return weatherApi.getTemperatureCelsius(city) + " st. C";
    }
}

Thanks to this, an object of ExternalApiToWeatherServiceAdapter class can be passed to WeatherClient and WeatherClient can call getCurrentTemperature() defined by WeatherService:

public class Main {
    public static void main (String[] args) {
        ExternalWeatherApi weatherApi = new ExternalWeatherApi() {
            @Override
            public double getTemperatureCelsius(String city) {
                return 15.2;
            }
        };

        ExternalApiToWeatherServiceAdapter apiAdapter = 
             new ExternalApiToWeatherServiceAdapter(weatherApi);
        WeatherClient weatherClient = new WeatherClient(apiAdapter);
        weatherClient.showTemperature("Łódź");
    }
}

Program output in the console is: Łódź: 15.2 st. C

Summary

The Adapter pattern is a clean solution to incompatible interfaces. It enables reuse of existing code without changing it. It helps integrate different components while keeping the architecture consistent and flexible.

Adapter is common in integrations with external APIs, libraries, and legacy systems. It is especially useful when integrating old systems to new components. In production, an adapter rarely calls just one method. It often transforms whole objects to move them from one interface to another.

There is also a two-way adapter too. It adapts interfaces in both directions with one object. Such an adapter must implement both interfaces. A two-way adapter is less clear and less common.


The Adapter pattern is similar to the Facade pattern, but they have different goals:

  • the goal of Adapter is to modify an interface so it fits the client’s needs,
  • the goal of Facade is to provide a simple interface to a system or subsystem.

Facade

The main goal of the Facade design pattern is to simplify communication between a client and a complex system. In large applications business logic is spread across many components. The Facade gives one interface which hides the complexity of the internal system.

Thanks to the Facade, the client does not need to know details of each class. It only uses the interface provided by the Facade.

Let’s consider a Report Generator application for managing documents. The application has several classes and methods. Called in the right order, they return a report.

The enum Role stores user roles used in the application:

public enum Role {
    ADMIN,
    USER;
}

The User class represents a user who wants to generate a document. The user must be authorized:

public class User {
    private String username;
    private Role role;

    public User(String username, Role role) {
        this.username = username;
        this.role = role;
    }

    public Role getRole() {
        return role;
    }
}

The Report class represents the report to generate:

public class Report {
    private String content;

    Report(String content) { this.content = content; }

    public void exportToPDF() {
        System.out.println("Exporting report to PDF");
        System.out.println(content);
    }
}

The AuthService class handles user authorization:

public class AuthService {
    public boolean hasAccess(User user) {
        return "ADMIN".equals(user.getRole());
    }
}

The DataService class generates data for the report:

public class DataService {
    public String fetchDataForReport() {
        return "Q2 financial data: [revenue, costs, profit...]";
    }
}

The ReportService class generates reports and returns a Report object:

public class ReportService {
    public Report generateReport(String data) {
        return new Report("Report generated based on:\n" + data);
    }
}

Another application (Client) connects to Report Generator. The Client is an external application that connects to the Report Generator and generates a report:

public class Client {
    public static void main (String[] args) {
        User admin = new User("John", Role.ADMIN);

        AuthService authService = new AuthService();
        DataService dataService = new DataService();
        ReportService reportService = new ReportService();

        if (authService.hasAccess(admin)) {
            String data = dataService.fetchDataForReport();
            Report report = reportService.generateReport(data);
            report.exportToPDF();
        } else {
            System.out.println("No permission to generate the report.");
        }
    }
}

The Client class must know how the Report Generator works. It must know all its classes and methods. It must know the order of method calls to generate a report. It also has direct access to Report Generator classes. The Facade gives the Client one interface and hides the details.

Implementation of the Facade pattern

The report-generation logic is moved from Client class to ReportFacade class. The ReportFacade class exposes 1 interface, generateReport(). The Client uses this interface:

public class ReportFacade {

    private AuthService authService = new AuthService();
    private DataService dataService = new DataService();
    private ReportService reportService = new ReportService();

    public void generateReport (User user) {
        if (authService.hasAccess(user)) {
            String data = dataService.fetchDataForReport();
            Report report = reportService.generateReport(data);
            report.exportToPDF();
        } else {
            System.out.println("No permission to generate the report.");
        }
    }
}

Access to other classes and methods is blocked. In Java, classes can be moved to a separate package and the public access modifier can be removed. Then only ReportFacade will have access. The Client will not.

The Client class now calls only generateReport() from the Facade. In this case, the User class cannot be hidden. The Client must use it.

public class Client {
    public static void main (String[] args) {
        User admin = new User("Jan", Role.ADMIN);

        ReportFacade reportFacade = new ReportFacade();
        reportFacade.generateReport(admin);
    }
}

The Client no longer needs to know the order of method calls. All report logic is hidden. The Client needs to know only one interface.

Summary

The Facade pattern is a clean way to organize complex systems. It offers simple and consistent access points. The Facade isolates the client from the system. It separates the business logic layer from internal methods. This makes code safer and easier to change.

The Facade is common in the service layer of web apps and in integrations with external services. The Facade acts like a gate. It ties many classes and components into one logical entry point.


The Facade pattern is similar to the Adapter pattern, but they have different goals:

  • the goal of Facade is to provide a simple interface to a system or subsystem,
  • the goal of Adapter is to modify an interface so it fits the client’s needs.

Strategy

Assume that, depending on conditions, the application must perform different actions. You can extract such a set of interchangeable actions (algorithms) into separate classes. These actions are strategies. The Strategy design pattern lets you change strategies dynamically at runtime.

Thanks to Strategy, you can separate concrete actions from classes. The code becomes clearer and easier to extend. Users don’t need to know all class interfaces – they only set a strategy and call it.

Let’s consider an application that connects to several different APIs. The application must authenticate with each API, but each API requires a different authentication method.

The enum AuthType stores the authentication methods supported by the application:

public enum AuthType {
    BASIC,
    OAUTH,
    JWT;
}

The APIConnector class handles authentication:

public class APIConnector {
    
    public void authBasic (AuthType authType) {
        if (authType.equals(AuthType.BASIC)) {
            System.out.println("Authentication using Basic Auth");
        } else {
            System.err.println("Invalid authentication type");
        }
    }

    public void authOauth (AuthType authType) {
        if (authType.equals(AuthType.OAUTH)) {
            System.out.println("Authentication using OAuth2");
        } else {
            System.err.println("Invalid authentication type");
        }
    }

    public void authJwt (AuthType authType) {
        if (authType.equals(AuthType.JWT)) {
            System.out.println("Authentication using JWT");
        } else {
            System.err.println("Invalid authentication type");
        }
    }
}

Authentication requires creating an APIConnector object and calling the right method:

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();
        connector.authBasic(AuthType.BASIC);
    }
}

This solution has two drawbacks:

  • the user (Main class) must know the interfaces of APIConnector to use it,
  • and each new authentication method requires changes in APIConnector.

You can modify APIConnector to have only one interface:

public class APIConnector {

    public void authenticate (AuthType authType) {
        switch (authType) {
            case BASIC:
                System.out.println("Authentication using Basic Auth");
                break;
            case OAUTH:
                System.out.println("Authentication using OAuth2");
                break;
            case JWT:
                System.out.println("Authentication using JWT");
                break;
            default:
                System.err.println("Unknown authentication type");
        }
    }
} 

Now using APIConnector is easier (Main needs to know only one interface, authenticate()), but each new method still requires a change in APIConnector. The Strategy pattern solves this problem.

Implementation of the Strategy pattern

The AuthStrategy interface declares one method, authenticate():

public interface AuthStrategy {
    void authenticate();
}

The BasicAuthStrategy, OAuthStrategy, and JwtAuthStrategy classes implement the interface. These are concrete strategies. Each one implements authenticate() in a different way and handle different way of authentication:

public class BasicAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using Basic Auth");
    }
}
public class OAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using OAuth2");
    }
}
public class JwtAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using JWT");
    }
}

The APIConnector class has a private field that holds an object that implements AuthStrategy, and a setter. In the authenticate() method you don’t need previous complicated logic. It only calls authenticate() on the object from the field:

public class APIConnector {

    private AuthStrategy authStrategy;

    public void authenticate () {
        this.authStrategy.authenticate();
    }

    public void setAuthStrategy(AuthStrategy authStrategy) {
        this.authStrategy = authStrategy;
    }
}

In the Main class, you still create an APIConnector. Then you set the right strategy and call authenticate() method.

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();

        connector.setAuthStrategy(new BasicAuthStrategy());
        connector.authenticate();
    }
}

Because the field authStrategy is of type AuthStrategy, you can assign any class that implements this interface: BasicAuthStrategy, OAuthStrategy, or JwtAuthStrategy. AuthStrategy is an abstraction layer between APIConnector and the concrete strategies. When you call authenticate(), the app calls the proper implementation automatically.

The AuthType enum is no longer needed.

Adding a new authentication method no longer requires changes in APIConnector. You only add a new strategy.

Summary

The Strategy pattern is a practical application of the open–closed principle and the dependency inversion principle.

Strategy is a popular and simple pattern. It is used when you need flexible changes to behavior. The pattern makes testing, extension, and maintenance easier.


The Strategy pattern and the State pattern both let you change behavior at runtime, but they have different goals:

  • Strategy encapsulates algorithms that are injected from outside and the client chooses which algorithm to use,
  • State defines behavior for each state of an object ahead of time and the change in behavior happens automatically with state changes.