how to manage your objects

A question popped into my head one night: "how to manage objects in our code".

Xiaoyi was me when I first started working. He said: Create an object through new and then use it directly.

public class HelloWorld {
	public void hello() {
		System.out.println("hello world!");
	}
}
HelloWorld helloWorld = new HelloWorld();
helloWorld.hello();

You see, I have a HelloWorld class, I can directly create an object with new, and then I can use all the methods in this object, how simple.

Eryi is me who has been working for two years. He said to Xiaoyi with a look of contempt, "Don't do HelloWorld all day long, okay? Also, you can't do anything other than new. Can you pursue it a little bit?"

Xiaoyi said to Eryi, what else do you think other than new?

The second game says that object instances can be created through newInstance of Class or newInstance of Constructor.

But you have to remember that the newInstance of Class can only instantiate objects for those classes that have a visible (Accessible) no-argument constructor, and Constructor does not have these restrictions.

Da Yi is me who has been working for three years. He said that although all of your methods can be used to create objects, they are all created manually, which are too primitive and too low in productivity.

If a worker wants to do a good job, he must first sharpen his tool, and we also have to find an efficient productivity tool. Do you know about IOC containers?

In the past, when we wanted to call the method of another object in an object, we used new or reflection to manually create the object, but it was too tiring to do this every time, and the coupling between classes was also very high.

Through the IOC container, we can hand over all the objects to the container for management. We only need to define the object before using it, and then when the object is used, the IOC container will help us initialize the object. Is this more convenient? Woolen cloth?

After the big game, he gave an example:

@Bean
public class RegisterService {
	public void register() {
		// do register
	}
}
@Bean
public class LoginService {
	public void login() {
		// do login
	}
}
@Bean
public class HelloWorld {
	@Autowired
	private RegisterService registerService;
	@Autowired
	private LoginService loginService;
	public void hello() {
		// 注册
		registerService.register();
		// ...
		// 登录
		loginService.login();
	}
}

Through an annotation called Bean, the IOC container scans all the classes marked by Bean when the system starts, instantiates these classes, and then saves all the objects in the container. Then scan all the properties or methods marked by Autowired, find objects that match (by name or type, etc.) from the container, and assign specific objects to these properties. In this way, we can use these objects directly. Isn't it very happy as a hand-out party?

Lao Yi is me who has been working for five years. After listening to Da Yi's words, he raised a question. For new projects, this IOC container can be used, but for those legacy projects, it is necessary to use IOC to transform. Not quite true.

I give an example, in a legacy old project, there is a core interface Handler:

public interface Handler<REQ, RES> {
    RES handle(REQ request);
}

The Handler interface has many implementation classes. We need to call different Handler implementation classes for different requests. It is obviously not suitable to use the IOC container to manage these implementation classes, because we do not know which Handler to use before processing. Implementation class.

After thinking about it, if the Handler interface has only a few fixed implementation classes, and only one is used for processing, then it is possible to determine which Handler to use by configuring it before starting. @Conditional determines the specific object to load based on certain conditions, but it is really tricky to determine the type of the Handler object when it is used.

Lao Yi saw that everyone stopped talking, so he continued talking.

In order to use different Handlers to process different requests when calling methods, it is necessary to determine two classes, one is the request class and the other is the processing class, and the request class and the processing class must be in one-to-one correspondence.

Suppose our request class is a Packet class, and each specific request class inherits from this base class.

So if you want to determine what type of each specific Packet is, there are many ways, you can take a unique name for each Packet, for example:

public abstract class Packet {
    public abstract String name();
}

You can also specify a flag for each Packet, for example:

public abstract class Packet {
    public abstract int symbol();
}

But either way, each Packet implementation class needs to implement the methods in the abstract class to "mark" what kind of Packet it is.

Let's take the second way as an example, assuming we have two specific Packets:

public class RegisterPacket extends Packet {
	// 注册所需要的其他参数
	int symbol() {
		return 1;
	}
}
public class LoginPacket extends Packet {
	// 登录所需要的其他参数
	int symbol() {
		return 2;
	}
}

In this way, when we receive the request object, we can know which type of Packet the request is by calling request.symbol(), then we just need to find the specific Handler implementation class to handle it.

The request class has been determined, how to determine the Handler processing class? Can we also define a symbol method in the Handler interface, like this:

public interface Handler<REQ, RES> {
    int symbol();
    RES handle(REQ request);
}

In this case, as long as the symbol method is implemented in all implementation classes to mark what kind of request the Handler is used to handle.

public RegisterHandler implements Handler<RegisterPacket, RES> {
    int symbol(){
    	return 1;
    }
    RES handle(RegisterPacket request){
    	// 具体的处理方法
    }
}
public LoginHandler implements Handler<LoginPacket, RES> {
    int symbol(){
    	return 2;
    }
    RES handle(LoginPacket request){
    	// 具体的处理方法
    }
} 

Finally, all the Handler implementation classes are instantiated and saved in a HandlerProvider, and then go to the HandlerProvider to get it when you want to use it:

public interface HandlerProvider {
    Handler getHandler(int symbol);
}

So how to get all the implementation classes of Handler, there are two ways.

One is to obtain it by ServiceLoader.load(Handler.class), but this way of using spi needs to create a xxx.Handler file in the resources/META-INF/services/ directory of the project, and in the file List the full class qualifiers of all Handler implementation classes.

Another relatively simple way is to get the implementation classes of all Handlers by scanning.

So far, our implementation is OK, but there is a problem, that is, we add a method to the Handler interface, which invades the original code.

In order to keep the original code unchanged, we can define an annotation to mark all Handler implementation classes, such as this:

@Symbol(1)
public RegisterHandler implements Handler<RegisterPacket, RES> {
    RES handle(RegisterPacket request){
    	// 具体的处理方法
    }
}
@Symbol(2)
public LoginHandler implements Handler<LoginPacket, RES> {
    RES handle(LoginPacket request){
    	// 具体的处理方法
    }
} 

In this way, the implementation of Handler is decoupled from the annotation, and all Handler implementation classes can be obtained by scanning the @Symbol annotation, but the disadvantage of this is that if I forget to add the @Symbol annotation to a Handler implementation class , At that time, the Handler will not be obtained.

After listening to Lao Yi's words, everyone fell into contemplation. Damn, you can still play like this, it's really interesting.

At this time and now, I, who is Houyi, said, if I have an interface, it only has a few fixed implementation classes, I don't want to do such a heavy implementation, but I also need to dynamically obtain the implementation class to handle the request, so what should I do?

For example I have a serialized interface like this:

public interface Serializer {
    byte[] serialize(Packet packet);
}

Then there are only five specific serialization implementation classes, as follows:

public class JdkSerializer implements Serializer {
	@Override
    public byte[] serialize(Packet packet) {
    	// 具体的序列化操作
    }
}
public class FastJsonSerializer implements Serializer {
	@Override
    public byte[] serialize(Packet packet) {
    	// 具体的序列化操作
    }
}
public class HessianSerializer implements Serializer {
	@Override
    public byte[] serialize(Packet packet) {
    	// 具体的序列化操作
    }
}
public class KryoSerializer implements Serializer {
	@Override
    public byte[] serialize(Packet packet) {
    	// 具体的序列化操作
    }
}
public class ProtoStuffSerializer implements Serializer {
	@Override
    public byte[] serialize(Packet packet) {
    	// 具体的序列化操作
    }
}

So how do we determine which serialization method to use to serialize the parameter packet?

It is indeed possible to use the set that Lao Yi just said, but it is too troublesome. You have to define symbols for Packet, mark the implementation classes of Hander, and scan all implementation classes.

I only have five implementation classes, so I don't need to do that much trouble.

In fact, it is very simple. You only need to define an enumeration class to represent the serialization algorithm, and then add an algorithm method to Packet to indicate which serialization algorithm to use, as shown below:

public enum SerializeAlgorithm {
    JDK((byte) 1),
    FAST_JSON((byte) 2),
    HESSIAN((byte) 3),
    KRYO((byte) 4),
    PROTO_STUFF((byte) 5);
    private byte type;
    SerializeAlgorithm(byte type) {
        this.type = type;
    }
}
public abstract class Packet implements Serializable {
	public abstract byte algorithm();
}

Then define a SerializerChooser to select different Serializer implementation classes according to different algorithms:

public interface SerializerChooser {
    Serializer choose(byte algorithm);
}

Because the corresponding serialization interface can be known according to the algorithm, there is no need to scan, and several serialized implementation classes can be directly enumerated. The instance of the object can use the singleton mode, as shown below:

public class DefaultSerializerChooser implements SerializerChooser {
    private DefaultSerializerChooser() {

    }
    public static SerializerChooser getInstance() {
        return Singleton.get(DefaultSerializerChooser.class);
    }
    @Override
    public Serializer choose(byte algorithm) {
        SerializeAlgorithm serializeAlgorithm = SerializeAlgorithm.getEnum(algorithm);
        switch (serializeAlgorithm) {
            case JDK: {
                return Singleton.get(JdkSerializer.class);
            }
            case FAST_JSON: {
                return Singleton.get(FastJsonSerializer.class);
            }
            case HESSIAN: {
                return Singleton.get(HessianSerializer.class);
            }
            case KRYO: {
                return Singleton.get(KryoSerializer.class);
            }
            case PROTO_STUFF: {
                return Singleton.get(ProtoStuffSerializer.class);
            }
            default: {
                return null;
            }
        }
    }
}

After I finished speaking, everyone fell into contemplation again, I know that everyone is thinking, and they will progress and grow with each thought, just as I grow after thinking.

The little bird will grow into an old bird one day, and I am still on the road of growth.

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324129395&siteId=291194637