During request processing, the application accepts and processes the request, and then returns the response. In this process, there are also some common functions, such as authentication, monitoring, link tracing. Many RPC frameworks provide concepts called Middleware or Interceptor that support many of the features discussed above in a pluggable way. Taking GRPC as an example, its working principle is shown in the figure below:

The interface of the server is as follows:

func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error)

func StreamServerInterceptor (srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error

As you can see, the interface explicitly defines input parameters and output results. If we want to implement a component ourselves and need to support the user passing in a specific configuration, is there any way to do that?

The answer is yes.

Higher-order function

Before we get into the specifics of the solution, we need to understand a concept called the higher-order function.

A higher-order function is one that supports at least one of the following:

  1. Taking one or more functions as parameters (that is, procedure parameters),
  2. Returns the function as its result

The second point is the desired feature. For example, the current limiting interceptor supports passing in custom current limiters. In this case, you need to define a higher-order function that takes a current limiter as an argument, and return a function that is Interceptor required by the framework. In the Interceptor function, you need to use the incoming current limiter to determine whether it is needed or not. The concrete implementation of the Interceptor based on the stream limiting interface is as follows:

type Limiter interface {
    Limit(key string) bool
}

func UnaryServerInterceptor(limiter Limiter) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        if limiter.Limit(info.FullMethod) {
            return nil, status.Errorf(codes.ResourceExhausted, "%s is rejected by grpc_ratelimit middleware, please retry later.", info.FullMethod)
        }
        return handler(ctx, req)
    }
}

...

Currently, the parameters passed in are fixed and can be implemented in this way. Further, if the usage is more complex, you can expect more parameters to be added in the future in addition to those already determined. Also requires the current design of the interface needs to have a very good scalability. Is there any other way?

Again, the answer is yes.

Functional Options

Unsurprisingly, it takes advantage of the first point of a higher-order function, which has a specific name: Functional Options.

Start by defining the structure for the parameters passed in

type options struct {
    byMethod  bool
    byUser    bool
    byClientIP bool
}

Second, define another function type:

type Option func(*Options)

Third, define a set of functions that modify the configuration

func ByMethod(m bool) Option {
    return func(o *options) {
        o.byMethod = m
    }
}

func ByUser(u bool) Option {
    return func(o *options) {
        o.byUser = u
    }
}

func ByClientIP(c bool) Option {
    return func(o *options) {
        o.byClientIP = c
    }
}

Finally, update the provided Interceptor as follows:

func UnaryServerInterceptor(limiter Limiter, opts ... Option) grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { default := options { byMethod: true, byUser: false, byClientIP: false, } for _, opt := range opts { opt(&default) } ... return handler(ctx, req) } }

If so, you have an extensible Interceptor that supports custom parameters.

The last

To sum up, give an opinion:

  1. Higher-order functions, however, do not belong to any particular programming language. Other languages, such as C++, support similar features.
  2. Do you need to know the implementation details as an architect? The answer is yes. Otherwise, what would support the design’s so-called extensibility in a given environment?

In this paper, the author: cyningsun this address: https://www.cyningsun.com/07-… Copyright Notice: All articles in this blog are licensed under the CC BY-NC-ND 3.0CN license unless otherwise stated. Reprint please indicate the source!