Extension method of simulating C# in Java

Keywords: Java

I usually use C#, JavaScript and TypeScript. But recently, due to some reasons, I need to use Java and have to pick it up again. In retrospect, the last time you used java to write a complete application, java was version 1.4.

Over the years, Java has indeed made many improvements, such as Stream and var. I still know some. But it still feels a little tied to hands and feet. This must be related to syntax habits, but there are also reasons for Java itself. For example, the "extension method" I often use in C# is not available in Java.

"C #" Extension method ", you can add public methods for some classes and their subclasses without modifying the class definition or inheriting the class. When the objects of these classes call the extension method, it is the same as calling the method declared by the class itself. In order to understand this syntax, here is an example (no matter whether you can C#, you should be able to understand it as long as you have OOP Foundation)

using System;

// Define a Person class without defining a method
public class Person {
    public string Name { get; set; }
}

// The extension method PrintName() is defined in the following class
public static class PersonExtensions {
    public static void PrintName(this Person person) {
        Console.WriteLine($"Person name: {person.Name}");
    }
}

// The Main program provides the Main entry and uses the extension method here
public class Program {
    public static void Main(string[] args) {
        Person person = new Person { Name = "John" };
        person.PrintName();
    }
}

Developers with OOP foundation can judge from general knowledge that the Person class does not define a method and should not call person.PrintName(). However, since printname () is a static method, you should be able to use PersonExtensions.PrintName(person).

Indeed, if you try a statement such as person extensions. Printname (person), you will find that this statement can also run correctly. However, notice that the first parameter declared by PrintName() is decorated with this. This is a C# unique extension method syntax. The compiler will recognize the extension method, and then translate person.PrintName() into PersonExtensions.PrintName(person) to call it -- this is a syntax sugar.

C# added the syntax of "extension method" in version 3.0 released in 2007, which was more than 10 years ago. I don't know when Java can support it. But it's not all right to say that Java doesn't support extension methods. After all, there is a thing called Manifold, which provides extension method features in the form of java compiler plug-ins. Plug-in support is required in IDEA, which feels similar to c# - unfortunately, the monthly rental fee of $19.9 directly discourages me.

But programmers often have an obsession of not hitting the south wall and not looking back. Is there no approximate way to deal with this problem?

Analyze the source of pain

The main reason for using extension methods is that you want to extend classes in the SDK, but you don't want to use static calls. Especially when chain calls are needed, static methods are really difficult to use. Take Person as an example (Java code this time):

class Person {
    private String name;
    public Person(String name) { this.name = name; }
    public String getName() { return name;}
}

class PersonExtension {
    public static Person talk(Person person) { ... }
    public static Person walk(Person person) { ... }
    public static Person eat(Person person) { ... }
    public static Person sleep(Person person) { ... }
}

The business process is: go out to eat after negotiation, and then come back to sleep. Call with link should be:

person.talk().walk().eat().walk().sleep()

Note: don't mention changing the Person. Let's assume that it is encapsulated by the third-party SDK, and the Person extension is the business processing class we write

But obviously, it cannot be called like this. According to the method in PersonExtension, it should be called like this:

sleep(walk(eat(walk(talk(person)))));

Pain?!

In addition to the pain, let's analyze our current needs:

  1. call chaining
  2. Nothing else

Typical application scenarios of chain call

Since what we need is chain call, let's think about the typical application scenario of chain call: Builder mode. If we use the construction mode to write the Extension class and encapsulate the original object when using, can we realize the chain call?

class PersonExtension {
    private final Person person;

    PersonExtension(Person person) {
        this.person = person;
    }

    public PersonExtension walk() {
        out.println(person.getName() + ":walk");
        return this;
    }

    public PersonExtension talk() {
        out.println(person.getName() + ":talk");
        return this;
    }

    public PersonExtension eat() {
        out.println(person.getName() + ":eat");
        return this;
    }

    public PersonExtension sleep() {
        out.println(person.getName() + ":sleep");
        return this;
    }
}

Easy to use:

new PersonExtension(person).talk().walk().eat().walk().sleep();

Extended to general conditions

If we stop here, this blog will be too watery.

We made a detour to solve the problem of chain call, but it is not easy to meet people's hearts. A new requirement has emerged: extension methods can write countless extension classes. Is there a way to connect and call the methods defined in these countless classes?

You see, in the current encapsulated class, we can't call the method of the second encapsulated class. However, if we can convert from the current encapsulated class to the second encapsulated class, isn't it OK?

This conversion process is probably to get the currently encapsulated object (such as person), pass it as a parameter to the constructor of the next encapsulated class, construct the object of this class, and continue to write it as the calling body... In this way, we need to have a convention:

  1. The extension class must provide a constructor that can pass in encapsulated object type parameters;
  2. An extension class must implement a method that converts to another extension class

In the program, conventions are usually described by interfaces, so an interface is defined here:

public interface Extension<T> {
    <E extends Extension<T>> E to(Class<E> type);
}

The meaning of this interface is very clear:

  • The encapsulated object type is T
  • To provides switching from the current Extension object to another object that implements the Extension < T > interface

As you can imagine, what this to has to do is to find the constructor of E and use it to construct an e object. This constructor needs to be defined with unique parameters, and the parameter type is T or its parent type (which can be passed in). In this way, when constructing the e object, the T object encapsulated in the current extension object can be passed to the e object.

If a suitable constructor cannot be found or an error occurs during construction, an exception should be thrown to describe that type E is incorrect. Since e is a type parameter, you might as well use IllegalArgumentException. In addition, the to behavior of most Extension classes should be the same and can be supported by default methods. In addition, you can add a static create() method to the Extension instead of using new to create Extension class objects -- let's start with the Extension.

Here comes the complete Extension:

public interface Extension<T> {
    /**
     * For an encapsulated object value, construct an E-class object to encapsulate it.
     */
    @SuppressWarnings("unchecked")
    static <T, E extends Extension<T>> E create(T value, Class<E> extensionType)
        throws IllegalArgumentException {
        Constructor<T> cstr = (Constructor<T>) Arrays
            .stream(extensionType.getConstructors())
            // Find the one that meets the requirements in the construction method
            .filter(c -> c.getParameterCount() == 1
                && c.getParameterTypes()[0].isAssignableFrom(value.getClass())
            )
            .findFirst()
            .orElse(null);

        try {
            // If the appropriate constructor is not found (cstr == null), or in other cases, an error occurs
            // Throw the IllegalArgumentException
            return (E) Objects.requireNonNull(cstr).newInstance(value);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new IllegalArgumentException("invalid implementation of Extension", e);
        }
    }

    // In order to get the currently encapsulated object for wrapTo, you must use the getValue() interface
    T getValue();

    // wrapTo interface and its default implementation
    default <E extends Extension<T>> E to(Class<E> type) throws IllegalArgumentException {
        return create(getValue(), type);
    }
}

Now split the above PersonExtension into two extension classes for demonstration:

class PersonExt1 implements Extension<Person> {
    private final Person person;

    PersonExt1(Person person) { this.person = person; }

    @Override
    public Person getValue() { return person; }

    public PersonExt1 walk() {
        out.println(person.getName() + ":walk");
        return this;
    }

    public PersonExt1 talk() {
        out.println(person.getName() + ":talk");
        return this;
    }
}

class PersonExt2 implements Extension<Person> {
    private final Person person;

    public PersonExt2(Person person) { this.person = person; }

    @Override
    public Person getValue() { return person; }

    public PersonExt2 eat() {
        out.println(person.getName() + ":eat");
        return this;
    }

    public PersonExt2 sleep() {
        out.println(person.getName() + ":sleep");
        return this;
    }
}

Call example:

public class App {
    public static void main(String[] args) throws Exception {
        Person person = new Person("James");
        Extension.create(person, PersonExt1.class)
            .talk().walk()
            .to(PersonExt2.class).eat()
            .to(PersonExt1.class).walk()
            .to(PersonExt2.class).sleep();
    }
}

epilogue

In general, the basic idea to implement the extension method without syntax support is

  1. Realize that the so-called extension method called by the target object is actually the syntax sugar of static method call. The first parameter of the static method is the target object.
  2. The first parameter of the static method is taken out and encapsulated into the extension class. At the same time, the static method is changed into an instance method. This avoids passing in the target object when calling.
  3. If chain calls are required, interface conventions and some tool functions are required to help the target object shuttle through the extension classes.

This paper mainly attempts to extend module C# in Java without Syntax / compiler support. Although there are results, it may not be easy to use in practical use. Please pay attention to analysis and consider it as appropriate during actual development.

Posted by SchweppesAle on Sun, 21 Nov 2021 01:47:59 -0800