During request processing, the application accepts and processes the request, and then returns the response result. In this process, there are some common functions, such as authentication, monitoring, and link tracing. Many RPC frameworks provide a concept called Middleware or Interceptor that supports many of the features mentioned above in pluggable ways. Taking gRPC as an example, its working principle is shown as follows:

The interface on the server side 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
Copy the code

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

The answer is yes.

Higher-order function

Before we look at the specific solutions, we need to understand a concept called higher-order function.

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

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

The second is exactly the desired feature. Take the stream limiting interceptor as an example, and you can pass in a custom stream limiting interceptor. In this case, you need to define a higher-order function that takes a stream limiter as an argument. Then the function returned is the Interceptor required by the framework, and use the passed stream limiter in the Interceptor function to determine whether the stream limiter is needed. The specific implementation of the Interceptor 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)
	}
}

...
Copy the code

The current parameters passed in are fixed and can be implemented this way. Further, if you use more complexity, you can expect to add more parameters in the future in addition to those already identified. Therefore, the interface designed at present needs to have good expansibility. Is there any way?

Again, the answer is yes.

Functional Options

Unsurprisingly, the first point of higher-order functions is taken advantage of, and the programming pattern has a specific name: Functional Options.

You start by defining the structure for the passed parameter

type options struct {
    byMethod  bool
    byUser    bool
    byClientIP bool
}
Copy the code

Second, define a function type:

type Option func(*Options)
Copy the code

Again, 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
    }
}
Copy the code

Finally, change the provided Interceptor to:

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) } }Copy the code

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

The last

To sum up and make a point:

  1. Higher-order functions do not belong to a particular programming language. Other languages, such as C++, support similar features.
  2. Do architects need to know implementation details? Yes. Otherwise, in a given context, what can be used to support the design of so-called scalability?

Author: Cyningsun Author: www.cyningsun.com/07-19-2021/… Copyright notice: All articles on this blog are licensed under CC BY-NC-ND 3.0CN unless otherwise stated. Reprint please indicate the source!