This is the seventh day of my participation in the First Challenge 2022. For details: First Challenge 2022.

Usage scenarios

There are several scenarios where an existing class is “interface incompatible” with the target:

  • Encapsulate a flawed interface design
  • Unify the interface design of multiple classes
  • Replace dependent external systems
  • Compatible with older interfaces
  • Adapt to different formats of data

Suppose the target interface is as follows

public interface Target {

    void m1(a);

    void m2(a);

    void m3(a);

}
Copy the code

Our class is as follows

public class Source {

    public void ma(a) {
        System.out.println("hello1");
    }

    public void mb(a) {
        System.out.println("hello2");
    }

    public void m3(a) {
        System.out.println("hello3"); }}Copy the code

Obviously, the method names in the Source class are not exactly the same as those in the Target interface, so we cannot directly make the Source class implement the Target interface. To achieve the ability to convert Source to Target. To do this, we need to use the adapter pattern.

The adapter pattern can be implemented in two ways:

  • Through inheritance
  • By combining

Through inheritance

The class diagram

Adaptor inherits Source class and realizes Target interface, and realizes the interface in Target. It is important to note that not all methods in Target are implemented in the Adaptor class because the M3 () method Source is already implemented and can be inherited directly, which is the biggest difference from composition.

Code implementation

The Target and Source classes are the same as above

Adaptor

public class Adaptor extends Source implements Target {

    @Override
    public void m1(a) {
        super.ma();
    }

    @Override
    public void m2(a) {
        super.mb(); }}Copy the code

Main

public class Main {

    public static void main(String[] args) {
        Target target = newAdaptor(); target.m1(); target.m2(); target.m3(); }}Copy the code

By combining

The class diagram

The Adaptor class contains a Source member variable, injected through the constructor, and implements the Target interface. Note that all methods that implement Target are required here.

Code implementation

The Target and Source classes are the same as above

Adaptor

public class Adaptor implements Target {

    private Source source;

    public Adaptor(Source source) {
        this.source = source;
    }

    @Override
    public void m1(a) {
        source.ma();
    }

    @Override
    public void m2(a) {
        source.mb();
    }

    @Override
    public void m3(a) { source.m3(); }}Copy the code

Main

public class Main {

    public static void main(String[] args) {
        Source source = new Source();
        Target target = newAdaptor(source); target.m1(); target.m2(); target.m3(); }}Copy the code

Application of the adapter pattern to Java logging

Slf4j is a logging framework that you are no doubt familiar with. It provides a unified interface specification for printing logs. However, it only defines the interface, not the implementation, and needs to work with other logging frameworks (Log4J, LogBack…). To use.

Slf4j was invented later than JUL, JCL, log4J and other logging frameworks, so it was not possible for these logging frameworks to sacrifice version compatibility to adapt their interfaces to the Slf4j interface specification. Slf4j also has this in mind, so it provides not only a uniform interface definition, but also adapters for different logging frameworks. The interfaces of different logging frameworks are re-encapsulated and adapted into a unified Slf4j interface definition.

Code examples:

// slf4j unified interface definition
package org.slf4j;
public interface Logger {
  public boolean isTraceEnabled(a);
  public void trace(String msg);
  public void trace(String format, Object arg);
  public void trace(String format, Object arg1, Object arg2);
  public void trace(String format, Object[] argArray);
  public void trace(String msg, Throwable t);
 
  public boolean isDebugEnabled(a);
  public void debug(String msg);
  public void debug(String format, Object arg);
  public void debug(String format, Object arg1, Object arg2)
  public void debug(String format, Object[] argArray)
  public void debug(String msg, Throwable t);
  / /... Omit info, WARN, error, and a bunch of other interfaces
}

// An adapter for the log4j logging framework
// Log4jLoggerAdapter implements the LocationAwareLogger interface.
// The LocationAwareLogger inherits from the Logger interface.
The Log4jLoggerAdapter implements the Logger interface.
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase
  implements LocationAwareLogger.Serializable {
  final transient org.apache.log4j.Logger logger; // log4j
 
  public boolean isDebugEnabled(a) {
    return logger.isDebugEnabled();
  }
 
  public void debug(String msg) {
    logger.log(FQCN, Level.DEBUG, msg, null);
  }
 
  public void debug(String format, Object arg) {
    if(logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); }}public void debug(String format, Object arg1, Object arg2) {
    if(logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.format(format, arg1, arg2); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); }}public void debug(String format, Object[] argArray) {
    if(logger.isDebugEnabled()) { FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray); logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); }}public void debug(String msg, Throwable t) {
    logger.log(FQCN, Level.DEBUG, msg, t);
  }
  / /... Omit a bunch of interface implementations...
}
Copy the code

Therefore, when developing business systems or developing frameworks and components, we uniformly use the interface provided by Slf4j to write the code for printing logs, and use which logging framework to implement (Log4j, Logback…). , can be specified dynamically (using Java’s SPI technology) by importing the appropriate SDK into the project.

Differences between proxy, bridge, decorator, and adapter design patterns

Proxy, bridge, decorator, adapter, these four patterns are more commonly used structural design patterns. Their code structure is very similar. Broadly speaking, they can all be referred to as Wrapper patterns, where the original class is rewrapped with the Wrapper class.

Although the code structure is similar, the four design patterns have very different intentions, meaning different problems to solve and application scenarios, which are the main differences between them.

Proxy mode: The proxy mode defines a proxy class for the original class without changing the interface of the original class. The primary purpose of the proxy mode is to control access, rather than enhance functionality, which is the biggest difference from the decorator mode.

Bridge pattern: The purpose of the bridge pattern is to separate the interface part from the implementation part so that they can be changed relatively easily and independently.

Decorator pattern: The Decorator pattern enhances the functionality of the original class without changing the interface of the original class and supports nested use of multiple decorators.

Adapter pattern: The adapter pattern is an afterthought strategy. The adapter provides a different interface from the original class, whereas the proxy and decorator patterns provide the same interface as the original class.