Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

The use of design patterns is often tested in the interview process, learning design patterns do not memorize, combined with excellent project source code to understand the use of design patterns will get twice the result with half the effort. Netty source code used a large number of design patterns, common design patterns are reflected in Netty source code. In this lesson, we will take a look at some of the design patterns that are included in the Netty source code. We hope that this will help you understand the design patterns better.

The singleton pattern

The singleton pattern is the most common design pattern that ensures that there is only one instance globally, avoiding thread-safety issues. There are many ways to implement the singleton pattern, of which I recommend three best practices: double-checked lock, static inner class, hungry and enumeration. The double-checked lock and static inner class are lazy singleton, and hungry and enumeration are hungry singleton.

Double check lock

In multithreaded environments, to improve instance initialization performance, instead of locking the method every time an instance is acquired, the method is locked when the instance is not created, as shown below:

public class SingletonTest { private volatile static SingletonTest instance; public SingletonTest getInstance() { if (instance == null) { synchronized (this) { if (instance == null) { instance = new SingletonTest(); } } } return instance; }}Copy the code

Static inner class mode

Static inner class implementation singletons cleverly make use of Java class loading mechanism to ensure its thread safety in multi-threaded environment. When a class is loaded, its static inner class is not loaded at the same time, it is initialized only the first time it is called, and we cannot get the inner properties by reflection. Thus, static inner class implementations of singletons are more secure and can prevent reflection intrusion. 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(); }}Copy the code

The hungry way

Hanchian singletons are very simple to implement. Instances are created when the class is loaded. The Hanhanian approach uses private constructors to initialize a global single instance and is decorated with public static final to implement lazy loading and thread-safety. The implementation is as follows:

public class SingletonTest { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; }}Copy the code

Enumeration methods

Enumerations are a natural singleton implementation and are highly recommended in project development. It ensures the uniqueness of instances during serialization and deserialization without worrying about thread safety. Enumeration mode to achieve a single example as shown below:

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(); }Copy the code

NioEventLoop constantly polls registered I/O events using the core method SELECT (). Netty provides a SelectStrategy object that controls the behavior of the select loop. There are three policies: CONTINUE, SELECT, and BUSY_WAIT. The default implementation of the SelectStrategy object is the hunchman 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; }}Copy the code

In addition, Netty also has a lot of hungry way to implement singleton practices, such as MqttEncoder, ReadTimeoutException, etc..

Factory method pattern

The factory pattern encapsulates the object creation process without requiring consumers to care about the details of object creation. In any scenario where complex objects need to be generated, the factory pattern can be implemented. There are three types of factory patterns: simple factory pattern, factory method pattern and abstract factory pattern.

Simple factory pattern. Define a factory class that returns instances of different types based on the parameter type. It is suitable for scenarios where there are not many object instance types. If there are too many object instance types, the corresponding creation logic should be added in the factory class for each type added, which violates the open and closed principle.

Factory method pattern. An updated version of the simple factory pattern, instead of providing a single factory class to create instances of all objects, has different factory classes for each type of object instance, and only one object instance of each specific factory class can be created.

Abstract factory pattern. Rarely used, suitable for scenarios where multiple products are created. If you follow the factory method pattern, you need to implement multiple factory methods in a specific factory class, which is very unfriendly. The abstract factory pattern is to separate these factory methods into abstract factory classes, then create factory objects and retrieve factory methods through composition.

Netty uses the factory method pattern, which is one of the most commonly used factory patterns in project development. How is the factory method pattern used? Let’s start with a simple example:

public class TSLAFactory implements CarFactory { @Override public Car createCar() { return new TSLA(); } } public class BMWFactory implements CarFactory { @Override public Car createCar() { return new BMW(); }}Copy the code

Netty uses the factory method pattern when creating channels because the server and client channels are different. Netty combines the reflection and factory method patterns, using only one factory Class and then building the corresponding Channel based on the Class parameter passed in, rather than creating a factory Class for each Channel type. 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)"; }}Copy the code

Although reflection techniques can effectively reduce the amount of data in factory classes, reflection has a performance penalty compared to creating factory classes directly, so it should be used with caution in performance-sensitive scenarios.