头图

The extension method that simulates C# in Java

边城
中文

I usually use C#, JavaScript and TypeScript. But recently I needed to use Java for some reasons, so I had to pick it up again. In retrospect, the last time I wrote a complete application in Java, Java was version 1.4.

After so many years, Java does have many improvements, such as Stream, var , etc., I still know some. However, it still feels a bit tied and inflexible. This is definitely related to grammatical habits, but there is no lack of Java's own reasons. For example, the "extension method" that I often use in C# is not available in Java.

The syntax of C# " extension method " can add public methods to certain 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 grammar, here is an example (no matter if you know C# or not, as long as you have an OOP basis, you should be able to see an example)

using System;

// 定义一个 Person 类,没有定义方法
public class Person {
    public string Name { get; set; }
}

// 下面这个类中定义扩展方法 PrintName()
public static class PersonExtensions {
    public static void PrintName(this Person person) {
        Console.WriteLine($"Person name: {person.Name}");
    }
}

// 主程序,提供 Main 入口,并在这里使用扩展方法
public class Program {
    public static void Main(string[] args) {
        Person person = new Person { Name = "John" };
        person.PrintName();
    }
}

Developers with OOP basis can judge from general knowledge: The Person class has no defined methods and should not be able to call person.PrintName() . But since PrintName() is a static method, it should be possible to use PersonExtensions.PrintName(person) .

Indeed, if you try a PersonExtensions.PrintName(person) like 061a1912ebc49b, you will find that this sentence can also run correctly. But noted PrintName() declaration of the first argument plus this modification. This is the unique extension method syntax of C#, the compiler will recognize the extension method, and then translate person.PrintName() into PersonExtensions.PrintName(person) to call-this is a syntactic sugar.

C# added the "extension method" grammar in the 3.0 version released in 2007. It was more than 10 years ago. I don't know when Java will support it. But to say that Java doesn't support extension methods, that's not all right. After all, there is something called Manifold, which provides extension method features in the form of Java compiler plug-in. Plug-in support is needed in IDEA, and it feels similar to C#. Unfortunately, the monthly rental fee of $19.9 directly persuades me retreat.

But programmers often have the obsession of not hitting the south wall and not looking back. Isn't there an approximate method to deal with this problem?

Analyze the source of pain

Need to use the extension method, in fact, the main reason is one point: I want to extend the classes in the SDK, but I don't want to use the static call form. Especially when you need to chain calls, static methods are really not easy to use. Person take 061a1912ebc4f0 as an example (this time is Java code):

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 a deal is settled, and then come back to sleep. Calling with a link should be:

person.talk().walk().eat().walk().sleep()
Note: Don't mention changing Person , we assume it is packaged by a third-party SDK, and PersonExtension is the business processing class we wrote

But obviously it cannot be called like this. According PersonExtension , it should be called like this:

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

Painful? !

After suffering, let’s analyze our current needs:

  1. Chain call
  2. Nothing else...

Typical application scenarios of chain call

Since what is needed is chain call, let's think about the typical application scenario of chain call: builder mode. If we write the Extension class in a constructive mode and encapsulate the original object when using it, can we achieve chained calls?

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;
    }
}

It is very convenient to use:

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

Extended to the general case

If you stop here, this blog post is too watery.

We took a detour to solve the problem of chain calls, but people's hearts are always not easily satisfied. A new requirement has emerged: extension methods can write countless extended classes. Is there a way to make the methods defined in these countless classes? What about the connection call?

You see, in the current encapsulation class, we cannot call the method of the second encapsulation class. However, if we can switch from the current wrapper class to the second wrapper class, isn't it all right?

This conversion process, probably the process is to get the current 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 down as the calling subject... this way , We need to have an agreement:

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

In the program, the convention is usually described by an interface, so here is an interface:

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 to switch from the current Extension object to another object that implements the Extension<T> interface

As you can imagine, what to going to do is to find E and use it to construct an object of E This constructor needs to define a unique parameter, and the parameter type is T or its parent type (can be passed in). In this configuration E to extend the current package object when the object T object to the E subject to.

If no suitable constructor is found, or an error occurs during construction, an exception should be thrown to describe the type E is incorrect. Since E is a type parameter, you might as well use IllegalArgumentException . In addition, the to behavior of most extended classes should be the same, and the default method can be used to provide support. Also, it can give Extension add a static create() method instead of using new create an extended class object - make everything from Extension start.

The complete Extension is here:

public interface Extension<T> {
    /**
     * 给一个被封装的对象 value,构造一个 E 类的对象来封装它。
     */
    @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())
            // 在构造方法中找到符合要求的那一个
            .filter(c -> c.getParameterCount() == 1
                && c.getParameterTypes()[0].isAssignableFrom(value.getClass())
            )
            .findFirst()
            .orElse(null);

        try {
            // 如果没找到合适的构造函数 (cstr == null),或者其他情况下出错
            // 就抛出 IllegalArgumentException
            return (E) Objects.requireNonNull(cstr).newInstance(value);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new IllegalArgumentException("invalid implementation of Extension", e);
        }
    }

    // 为了给拿到当前封装的对象给 wrapTo 用,必须要 getValue() 接口
    T getValue();

    // wrapTo 接口及其默认实现
    default <E extends Extension<T>> E to(Class<E> type) throws IllegalArgumentException {
        return create(getValue(), type);
    }
}

Now disassemble 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();
    }
}

Concluding remarks

In general, to implement extension methods without grammatical support, the basic idea is

  1. Realize that the so-called extension methods called on the target object are actually syntactic sugar for static method calls. The first parameter of the static method is the target object.
  2. Take out the first parameter of the static method, encapsulate it in the extension class, and change the static method to an instance method at the same time. This is to avoid passing in the target object when calling.
  3. If you need chained calls, you need to use interface conventions and provide some tool functions to assist the target object to shuttle among the extended classes.

This article is mainly to try the extension method of module C# in Java without grammar/compiler support. Although there are results, it is not necessarily easy to use in actual use. Readers are requested to pay attention to analysis during actual development and consider them as appropriate.

阅读 2.1k

边城客栈
全栈技术专栏,公众号「边城客栈」,[链接]
1 篇内容引用

一路从后端走来,终于走在了前端!

54.8k 声望
24.6k 粉丝
0 条评论

一路从后端走来,终于走在了前端!

54.8k 声望
24.6k 粉丝
文章目录
宣传栏