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

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

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.

Builder model

The Builder pattern is very simple and sets the properties of an object through a chain call, which is useful in scenarios where objects have many properties. The advantage of The Builder mode is that it is free to choose the desired attributes like building blocks, and is not strongly bound. For users, it is important to know which attributes need to be set, and which attributes may be required differently in different scenarios.

Netty ServerBootStrap and Bootstrap boot device is one of the most classic builder pattern implementation, need to set up in the process of building a huge number of parameters, For example, configure the thread pool EventLoopGroup, set the Channel type, register the ChannelHandler, set Channel parameters, and bind ports. For more information about how to use the ServerBootStrap bootstrap, see Bootstrap: What do Clients and Servers Start up with? The course, I won’t say much here.

The strategy pattern

The policy mode provides multiple strategies to deal with the same problem, and these strategies can be replaced with each other, which improves the flexibility of the system to a certain extent. Policy mode is very consistent with the open closed principle, users can choose different policies without modifying the existing system, and it is easy to expand to add new policies.

Netty uses the strategy pattern in a number of places. For example, EventExecutorChooser provides different policy options for NioEventLoop, and the newChooser() method dynamically selects the mode of modulus calculation based on whether the thread pool size is a power of two. Thus improving performance. The EventExecutorChooser source code implementation 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); }} // omit other code}Copy the code

Decorator pattern

Decorator mode is an enhancement to the decorator class. It can add new features to the decorator class without modifying the decorator class. The decorator pattern is used when we need to extend functionality for a class, but the downside of this pattern is that it requires additional code. Let’s start with a simple example of how the decorator pattern should be used, as follows:

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 Read Color."); }}Copy the code

We create a Shape of the interface abstract decoration class ShapeDecorator, and maintain the Shape of the original object, FillReadColorShapeDecorator is used for decoration ShapeDecorator entity class, Instead of making any changes to the draw() method, it calls Shape’s original draw() method and then fillColor() to fill the color.

WrappedByteBuf: WrappedByteBuf: WrappedByteBuf: WrappedByteBuf: WrappedByteBuf: WrappedByteBuf

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

WrappedByteBuf is the base class for all ByteBuf decorators, which is nothing special but passes in the original ByteBuf instance as the decorator in the constructor. WrappedByteBuf has two subclasses UnreleasableByteBuf and SimpleLeakAwareByteBuf, which are really implementing enhancements to ByteBuf, For example, the release() method of the UnreleasableByteBuf class returns false to indicate that it cannot be released, as shown in the source code.

final class UnreleasableByteBuf extends WrappedByteBuf { private SwappedByteBuf swappedBuf; UnreleasableByteBuf(ByteBuf buf) { super(buf instanceof UnreleasableByteBuf ? buf.unwrap() : buf); } @Override public boolean release() { return false; } // omit other code}Copy the code

The decorator pattern and the proxy pattern both implement target class enhancement. What’s the difference between them? The implementation of the decorator pattern and the proxy pattern are indeed very similar in that the original target objects need to be maintained, with the decorator pattern focusing on adding new functionality to the target class and the proxy pattern focusing on extending existing functionality.