29 Programming Thoughts, Which Design Patterns Are Applied in Netty

29 Programming thoughts, which design patterns are applied in Netty #

The application of design patterns is often asked in interviews. It is important to not just memorize design patterns, but to understand their usage by studying the source code of excellent projects. Netty source code incorporates a large number of design patterns, and common design patterns are reflected in the Netty source code. In this lesson, we will summarize the design patterns included in the Netty source code, hoping to help you have a deeper understanding of Netty’s design essence and be able to explain your understanding of design patterns to the interviewer, combined with Netty’s source code.

Singleton Pattern #

The Singleton pattern is the most common design pattern. It ensures that there is only one instance globally, avoiding thread safety issues. There are many ways to implement the Singleton pattern, and I recommend three best practices: Double Checked Locking, Static Inner Class, Eager Initialization, and Enum. Among them, Double Checked Locking and Static Inner Class are lazy initialization singletons, while Eager Initialization and Enum are eager initialization singletons.

Double Checked Locking #

In a multi-threaded environment, in order to improve the performance of instance initialization, locking is not added to the method every time the instance is accessed. Instead, locking is only performed when the instance has not been created, as shown below:

public class SingletonTest {

    private SingletonTest instance;

    public static SingletonTest getInstance() {

        if (instance == null) {

            synchronized (this) {

                if (instance == null) {

                    instance = new SingletonTest();

                }

            }

        }

        return instance;

    }

}

Static Inner Class #

The Static Inner Class Singleton pattern cleverly utilizes the Java class loading mechanism to ensure its thread safety in a multi-threaded environment. When a class is loaded, its static inner class is not loaded at the same time. It is only initialized when it is first called, and we cannot access its internal properties through reflection. Therefore, the Static Inner Class Singleton pattern is safer, as it can prevent intrusion through reflection. The specific implementation is as follows:

public class SingletonTest {

    private SingletonTest() {

    }

    public static Singleton getInstance() {

        return SingletonInstance.instance;

    }

    private static class SingletonInstance {

        private static final Singleton instance = new Singleton();

    }

}

Eager Initialization #

Eager Initialization provides a very simple way to implement a singleton. The instance is created when the class is loaded. Eager Initialization initializes the global single instance using a private constructor and decorates it with public static final to achieve lazy loading and ensure thread safety. The implementation is as follows:

public class SingletonTest {

    private static Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {

        return instance;

    }

}

Enum #

The Enum pattern is a natural way to implement a singleton and is highly recommended in project development. It ensures the uniqueness of instances during serialization and deserialization, and we don’t have to worry about thread safety issues. An example of implementing a singleton using the Enum pattern is as follows:

public enum SingletonTest {

    SERVICE_A {

        @Override

        protected void hello() {

            System.out.println("hello, service A");

        }

    },

    SERVICE_B {

        @Override

        protected void hello() {

            System.out.println("hello, service B");

        }

    };

    protected abstract void hello();

}

In the course “Source Code: Deciphering Netty’s Reactor Thread Model”, we introduced the core principle of NioEventLoop. NioEventLoop continuously polls the registered I/O events through the core method select(). Netty provides the SelectStrategy object for selecting strategies to control the behavior of the select loop, including three strategies: CONTINUE, SELECT, and BUSY_WAIT. The DefaultSelectStrategy object, which is the default implementation of SelectStrategy, uses Eager Initialization singleton. The source code is as follows:

final class DefaultSelectStrategy implements SelectStrategy {

    static final SelectStrategy INSTANCE = new DefaultSelectStrategy();

    private DefaultSelectStrategy() { }

    @Override

    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {

        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;

    }

}

In addition, there are many examples of using Eager Initialization to implement singletons in Netty, such as MqttEncoder, ReadTimeoutException, etc.

Factory Method Pattern #

The factory pattern encapsulates the process of object creation, so that the user does not need to be concerned with the details of object creation. The factory pattern can be used in scenarios where complex objects need to be created. There are three types of factory patterns: Simple Factory Pattern, Factory Method Pattern, and Abstract Factory Pattern.

  • Simple Factory Pattern: Defines a factory class that returns different types of instances based on the parameter type. This pattern is suitable for scenarios with a small number of object instance types. If there are too many object instance types, adding a new type would require adding corresponding creation logic to the factory class, which violates the Open-Closed Principle.
  • Factory Method Pattern: An upgraded version of the simple factory pattern. Instead of providing a unified factory class to create all types of object instances, each type of object instance corresponds to a different factory class. Each concrete factory class can only create one type of object instance.
  • Abstract Factory Pattern: Less commonly used, this pattern is suitable for scenarios where multiple products need to be created. If we follow the implementation approach of the factory method pattern, we would need to implement multiple factory methods in the concrete factory class, which is not very friendly. The abstract factory pattern separates these factory methods into an abstract factory class, and then creates factory objects and obtains factory methods through composition.

Netty uses the factory method pattern, which is also the most commonly used factory pattern in project development. How is the factory method pattern used? Let’s look at a simple example first:

public class TSLAFactory implements CarFactory {

    @Override

    public Car createCar() {

        return new TSLA();

    }

}

public class BMWFactory implements CarFactory {

    @Override

    public Car createCar() {

        return new BMW();

    }

}

Netty uses the factory method pattern when creating channels, because the server-side and client-side channels are different. Netty combines reflection with the factory method pattern, using only one factory class to construct the corresponding channel based on the Class parameter passed in, without needing to create a factory class for each channel type. The specific source code implementation is as follows:

public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {

    private final Constructor<? extends T> constructor;

    public ReflectiveChannelFactory(Class<? extends T> clazz) {

        ObjectUtil.checkNotNull(clazz, "clazz");

        try {

            this.constructor = clazz.getConstructor();

        } catch (NoSuchMethodException e) {

            throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +

                    " does not have a public non-arg constructor", e);

        }

    }

    @Override

    public T newChannel() {

        try {

            return constructor.newInstance();

        } catch (Throwable t) {

            throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);

        }

    }

    @Override

    public String toString() {

        return StringUtil.simpleClassName(ReflectiveChannelFactory.class) +

                '(' + StringUtil.simpleClassName(constructor.getDeclaringClass()) + ".class)";

    }

}

Although using reflection technology can effectively reduce the amount of factory class data, there is a performance loss compared to creating factory classes directly. Therefore, reflection should be used with caution in performance-sensitive scenarios.

Chain of Responsibility Pattern #

After completing the earlier courses in this column, I believe everyone is already familiar with the Chain of Responsibility pattern, which naturally brings to mind ChannelPipeline and ChannelHandler. ChannelPipeline is internally composed of a group of ChannelHandler instances, which are linked together by a bidirectional linked list, as shown in the following diagram:

Drawing 0.png

For the implementation of the Chain of Responsibility pattern in Netty, it also follows the four basic elements of the Chain of Responsibility pattern:

  • Responsibility Handler Interface: The ChannelHandler corresponds to the responsibility handler interface. ChannelHandler has two important sub-interfaces: ChannelInboundHandler and ChannelOutboundHandler, which intercept various inbound and outbound I/O events.
  • Dynamically create the chain of responsibility and add or remove responsibility handlers: ChannelPipeline is responsible for creating the chain of responsibility. It is internally implemented using a bidirectional linked list. The internal structure of ChannelPipeline is defined as follows:
public class DefaultChannelPipeline implements ChannelPipeline {

    static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);

    private static final String HEAD_NAME = generateName0(HeadContext.class);

    private static final String TAIL_NAME = generateName0(TailContext.class);

    // Omitted other code

    final AbstractChannelHandlerContext head; // Head node

    final AbstractChannelHandlerContext tail; // Tail node

    private final Channel channel;

    private final ChannelFuture succeededFuture;

    private final VoidChannelPromise voidPromise;

    private final boolean touch = ResourceLeakDetector.isEnabled();



    // Omitted other code

}

ChannelPipeline provides a series of add and remove related interfaces for dynamically adding and removing ChannelHandler handlers, as shown below:

Drawing 1.png

Context #

From the definition of the internal structure of ChannelPipeline, it can be seen that ChannelHandlerContext is responsible for saving context information of chain nodes. ChannelHandlerContext is an encapsulation of ChannelHandler, and each ChannelHandler corresponds to a ChannelHandlerContext. In fact, what the ChannelPipeline maintains is the relationship with ChannelHandlerContext.

Responsibility propagation and termination mechanism #

ChannelHandlerContext provides a series of fire methods for event propagation, as shown below:

Drawing 2.png

Taking the channelRead method of ChannelInboundHandlerAdapter as an example, ChannelHandlerContext will call the fireChannelRead method by default to pass the event to the next handler. If we override the channelRead method of ChannelInboundHandlerAdapter and do not call fireChannelRead to propagate the event, it means that the event propagation has been terminated.

Observer pattern #

The Observer pattern has two roles: observer and observable. The observable publishes messages and the observer subscribes to messages. Observers who have not subscribed will not receive messages. First, let’s take a look at how the Observer pattern is implemented through a simple example.

// Observable

public interface Observable {

    void registerObserver(Observer observer);

    void removeObserver(Observer observer);

    void notifyObservers(String message);

}

// Observer

public interface Observer {

    void notify(String message);

}

// Default implementation of Observable

public class DefaultObservable implements Observable {

    private final List<Observer> observers = new ArrayList<>();

    @Override

    public void registerObserver(Observer observer) {

        observers.add(observer);

    }

    @Override

    public void removeObserver(Observer observer) {

        observers.remove(observer);

    }

    @Override

    public void notifyObservers(String message) {

        for (Observer observer : observers) {

            observer.notify(message);

        }

    }

}

In Netty, the Observer pattern is widely used, but it is not as intuitive as the sample code above. We often use the ChannelFuture#addListener interface, which is the implementation of the Observer pattern. Let’s first take a look at the usage of ChannelFuture:

ChannelFuture channelFuture = channel.writeAndFlush(object);

channelFuture.addListener(future -> {

    if (future.isSuccess()) {

        // do something

    } else {

        // do something

    }

});

The addListener method adds a listener to ChannelFuture and notifies the registered listener immediately when ChannelFuture is completed. Therefore, ChannelFuture is observable, and the addListener method is used to add observers.

Builder pattern #

The Builder pattern is very simple, using chained calls to set object properties. It is very useful in scenarios where there are many object properties. The advantage of the Builder pattern is that it allows the user to freely choose the required properties like building blocks, and it is not strongly coupled. For users, it is necessary to know which properties need to be set, and different scenarios may require different properties.

In Netty, the ServerBootstrap and Bootstrap bootstrappers are the most classic implementations of the Builder pattern. There are many parameters that need to be set during the construction process, such as configuring the thread pool EventLoopGroup, setting the Channel type, registering ChannelHandlers, setting Channel parameters, and binding ports. The specific usage of the ServerBootstrap bootstrapper can be referred to in the course “What do clients and servers need to do to boot?”. I will not repeat it here.

Strategy pattern #

The Strategy pattern provides multiple strategies for handling the same problem, and these strategies can be replaced with each other, which improves the flexibility of the system to some extent. The Strategy pattern is very consistent with the Open-Closed principle. Users can select different strategies without modifying the existing system, and it is easy to extend and add new strategies.

Netty uses the Strategy pattern in many places. For example, EventExecutorChooser provides different strategies for choosing NioEventLoop. The newChooser() method dynamically selects the modulus operation method based on whether the size of the thread pool is a power of two, thereby improving performance. The source code implementation of EventExecutorChooser is as follows:

public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory {

    public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory();

    private DefaultEventExecutorChooserFactory() { }

    @SuppressWarnings("unchecked")

    @Override

    public EventExecutorChooser newChooser(EventExecutor[] executors) {

        if (isPowerOfTwo(executors.length)) {

            return new PowerOfTwoEventExecutorChooser(executors);

        } else {

            return new GenericEventExecutorChooser(executors);

        }

    }

    

    // Omitted other code

}

Decorator pattern #

The decorator pattern enhances the functionality of the decorated class without modifying the decorated class itself, and can add new functional features to the decorated class. The disadvantage of this pattern is that it requires additional code. Let’s first learn how to use the decorator pattern through a simple example, as shown below:

public interface Shape {
void draw();

}

class Circle implements Shape {

@Override

public void draw() {

System.out.print("draw a circle.");

}

}

abstract class ShapeDecorator implements Shape {

protected Shape shapeDecorated;

public ShapeDecorator(Shape shapeDecorated) {

this.shapeDecorated = shapeDecorated;

}

public void draw() {

shapeDecorated.draw();

}

}

class FillReadColorShapeDecorator extends ShapeDecorator {

public FillReadColorShapeDecorator(Shape shapeDecorated) {

super(shapeDecorated);

}

@Override

public void draw() {

shapeDecorated.draw();

fillColor();

}

private void fillColor() {

System.out.println("Fill Red Color.");

}

}

We have created an abstract decorator class ShapeDecorator for the Shape interface, which maintains the original Shape object. FillRedColorShapeDecorator is a concrete class that decorates ShapeDecorator. It does not modify the draw() method, but instead directly calls the draw() method of the Shape object and then calls the fillColor() method to fill in the color.

Now let's take a look at how WrappedByteBuf decorates ByteBuf in Netty, as shown in the source code below:

class WrappedByteBuf extends ByteBuf {

protected final ByteBuf buf;

protected WrappedByteBuf(ByteBuf buf) {

if (buf == null) {

throw new NullPointerException("buf");

}

this.buf = buf;

}

@Override

public final boolean hasMemoryAddress() {

return buf.hasMemoryAddress();

}

@Override

public final long memoryAddress() {

return buf.memoryAddress();

}



// Other code omitted

}

WrappedByteBuf is the base class for all ByteBuf decorators. It is not particularly special, but it does accept the original ByteBuf instance as the decorated object in its constructor. WrappedByteBuf has two subclasses: UnreleasableByteBuf and SimpleLeakAwareByteBuf. They are the actual decorators that enhance the functionality of ByteBuf. For example, the release() method of the UnreleasableByteBuf class simply returns false, indicating that it cannot be released. Here is the source code implementation:

final class UnreleasableByteBuf extends WrappedByteBuf {

private SwappedByteBuf swappedBuf;

UnreleasableByteBuf(ByteBuf buf) {

super(buf instanceof UnreleasableByteBuf ? buf.unwrap() : buf);

}

@Override

public boolean release() {

return false;

}



// Other code omitted

}

You may have a question: since both the decorator pattern and the proxy pattern enhance the target class, what is the difference between them? The implementation of the decorator pattern and the proxy pattern is indeed very similar, as they both require maintaining the original target object. The decorator pattern focuses on adding new functionality to the target class, while the proxy pattern focuses more on extending existing functionality.

### Conclusion

When learning design patterns, it is important not to memorize them blindly. You should not only absorb the ideas behind design patterns, but also understand why they are used. A good way to exercise your code design skills is to read the source code of excellent frameworks. Netty is a very rich learning resource. We need to understand the usage scenarios of design patterns in the source code, continuously absorb and digest them, and be able to apply them in project development.

The design patterns introduced in this lesson may not cover all the patterns used in Netty. There are still many to explore. As a homework assignment, learn how the following design patterns are used in Netty:

- Template Method pattern: implemented in the init process of ServerBootstrap and Bootstrap.
- Iterator pattern: CompositeByteBuf.
- Adapter pattern: ScheduledFutureTask.