High order play method of default value setting of Golang skill

Recently, GRPC has been used to find a very good design, which is worth learning.

In our daily method writing, we want to set a default value for a field, and do not pass this parameter in the scenario without customization. However, Golang does not provide the ability to set the default value of method parameters in dynamic languages such as PHP and Python.

Low level players should deal with the problem of default value

Take a shopping cart for example. For example, I have the following structure of a shopping cart, in which CartExts is an extended attribute, and it has its own default value. Users hope that this parameter will not be passed if the default value is not changed. However, because Golang cannot set the default value in the parameter, there are only the following options:

  1. Provide an initialization function. All ext fields are used as parameters. If you do not need to pass zero values of this type, the complexity will be exposed to the caller;
  2. In the initialization function, ext as a parameter is the same as 1. The complexity lies in the caller;
  3. Multiple initialization functions are provided to set internal default values for each scene.

Let's see what the code will do

const (
    CommonCart = "common"
    BuyNowCart = "buyNow"
)

type CartExts struct {
    CartType string
    TTL      time.Duration
}

type DemoCart struct {
    UserID string
    ItemID string
    Sku    int64
    Ext    CartExts
}

var DefaultExt = CartExts{
    CartType: CommonCart,       // Default is normal shopping cart type
    TTL:      time.Minute * 60, // Default 60min expiration
}

// Mode 1: each extended data is used as a parameter
func NewCart(userID string, Sku int64, TTL time.Duration, cartType string) *DemoCart {
    ext := DefaultExt
    if TTL > 0 {
        ext.TTL = TTL
    }
    if cartType == BuyNowCart {
        ext.CartType = cartType
    }

    return &DemoCart{
        UserID: userID,
        Sku:    Sku,
        Ext:    ext,
    }
}

// Mode 2: independent initialization function of multiple scenarios; mode 2 will rely on a basic function
func NewCartScenes01(userID string, Sku int64, cartType string) *DemoCart {
    return NewCart(userID, Sku, time.Minute*60, cartType)
}

func NewCartScenes02(userID string, Sku int64, TTL time.Duration) *DemoCart {
    return NewCart(userID, Sku, TTL, "")
}

The above code seems to be OK, but the most important consideration of our code design is stability and change. We need to be open to extension, close to modification and high cohesion of code. So if it's the code above, you add a field or reduce a field in CartExts. Does every place need to be modified? Or if there are many fields in CartExts, do constructors in different scenarios have to write many? So briefly summarize the problems of the above methods.

  1. It is not convenient to extend the CartExts field;
  2. If there are many CartExts fields, the constructor parameters are long, ugly and hard to maintain;
  3. All the field construction logic is redundant in NewCart, and the noodle code is not elegant;
  4. If CartExts is used as a parameter, too much detail is exposed to the caller.

Next, let's see how GRPC is done, learn excellent examples, and improve our code ability.

From this you can also realize that the code is the beauty of writing!

GRPC high level player setting default

Source code from: grpc@v1.28.1 edition. In order to highlight the main goal, the code has been deleted.


// dialOptions is defined in detail in google.golang.org/grpc/dialoptions.go
type dialOptions struct {
    // ... ...
    insecure    bool
    timeout     time.Duration
    // ... ...
}

// ClientConn is defined in detail in google.golang.org/grpc/clientconn.go
type ClientConn struct {
    // ... ...
    authority    string
    dopts        dialOptions // This is our focus, all the optional fields are here
    csMgr        *connectivityStateManager
    
    // ... ...
}

// Create a grpc link
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
    cc := &ClientConn{
        target:            target,
        csMgr:             &connectivityStateManager{},
        conns:             make(map[*addrConn]struct{}),
        dopts:             defaultDialOptions(), // Default options
        blockingpicker:    newPickerWrapper(),
        czData:            new(channelzData),
        firstResolveEvent: grpcsync.NewEvent(),
    }
    // ... ...

    // Modify and change to user's default value
    for _, opt := range opts {
        opt.apply(&cc.dopts)
    }
    // ... ...
}

The meaning of the above code is very clear. It can be considered that DialContext function is a grpc link creation function. Its internal main purpose is to build ClientConn as a structure and return value. The defaultDialOptions function returns the default value provided by the system for the dopts field. If you want to customize optional properties, you can control them with the variable parameter opts.

After the above improvements, we are surprised to find that this constructor is very beautiful. No matter how the dopts field is increased or decreased, the constructor does not need to be changed; defaultDialOptions can also be changed from a public field to a private field, which is more cohesive and friendly to the caller.

So how did it all come true? Let's learn the realization idea together.

Encapsulation of DialOption

First, the first technique here is DialOption. We optimize the optional fields by optional parameters, which will increase the embarrassment of constructor parameters, but to achieve this, we need to ensure that the types of optional fields are consistent, which is impossible in actual work. Therefore, the highest means in the program field are used. If one layer cannot be realized, another layer will be added.

Through this interface type, the unification of different field types is realized, and the input parameters of the constructor are simplified. Take a look at this interface.

type DialOption interface {
    apply(*dialOptions)
}

This interface has a method whose parameter is of type * dialOptions. We can also see from the code at the for loop above that what is passed in is& cc.dopts . Simply put, the object to be modified is passed in. The application method implements the modification logic.

So, since this is an interface, there must be a specific implementation. Take a look at the implementation.

// Empty realization, do nothing
type EmptyDialOption struct{}

func (EmptyDialOption) apply(*dialOptions) {}

// The most frequently used places
type funcDialOption struct {
    f func(*dialOptions)
}

func (fdo *funcDialOption) apply(do *dialOptions) {
    fdo.f(do)
}

func newFuncDialOption(f func(*dialOptions)) *funcDialOption {
    return &funcDialOption{
        f: f,
    }
}

Let's focus on the implementation of funcDialOption. This is a high-level usage, which shows that the edge function in Golang is a first-class citizen. It has a constructor and implements the DialOption interface.

The newFuncDialOption constructor takes a function as the only argument and saves the passed in function to the field f of funcDialOption. Let's see that the parameter type of this parameter function is * dialOptions, which is consistent with the parameter of the apply method. This is the second focus of the design.

Now it's time to see the implementation of the apply method. It is very simple, in fact, it is the method passed in when calling to construct funcDialOption. It can be understood as equivalent to being an agent. Drop the object to be modified by apply into the f method. So the important logic is implemented by the parameter method of newFuncDialOption.

Now let's see where in grpc the newFuncDialOption constructor is called.

Call to newFuncDialOption

Because the * funcDialOption returned by newFuncDialOption implements the DialOption interface, you can follow the example to find out where it was called grpc.DialContext Parameters that can be passed in by the opts constructor.

There are many places to call this method. We only focus on the methods corresponding to the two fields listed in the article: execute and timeout.


// The following methods are defined in detail in google.golang.org/grpc/dialoptions.go
// Enable unsafe transmission
func WithInsecure() DialOption {
    return newFuncDialOption(func(o *dialOptions) {
        o.insecure = true
    })
}

// Set timeout
func WithTimeout(d time.Duration) DialOption {
    return newFuncDialOption(func(o *dialOptions) {
        o.timeout = d
    })
}

Let's experience the exquisite design here:

  1. First, for each field, a method is provided to set its corresponding value. Since the type returned by each method is DialOption, this ensures that grpc.DialContext Method can use optional parameters because the types are consistent;
  2. The real type returned is * funcDialOption, but it implements the interface DialOption, which increases scalability.

grpc.DialContext Call to

After completing the above program construction, now let's stand in the perspective of use and feel the infinite style.


opts := []grpc.DialOption{
    grpc.WithTimeout(1000),
    grpc.WithInsecure(),
}

conn, err := grpc.DialContext(context.Background(), target, opts...)
// ... ...

Of course, the focus here is the slice of opts, whose element is the object that implements DialOption interface. The above two methods are wrapped as * funcDialOption objects, which implement DialOption interface, so the return value of these function calls is the element of this slice.

Now we can enter grpc.DialContext Inside this method, see how it's called inside. Traverse the opts, and then call the apply method in turn to complete the setting.

// Modify and change to user's default value
for _, opt := range opts {
    opt.apply(&cc.dopts)
}

After such a layer of packaging, although a lot of code has been increased, it can obviously feel the beauty and scalability of the whole code have been improved. Next, let's see how our own demo can be improved?

Improve DEMO code

First of all, we need to transform the structure, turn cartExts into cartExts, and design a encapsulation type to wrap all extension fields, and use this encapsulation type as an optional parameter of the constructor.


const (
    CommonCart = "common"
    BuyNowCart = "buyNow"
)

type cartExts struct {
    CartType string
    TTL      time.Duration
}

type CartExt interface {
    apply(*cartExts)
}

// Here's a new type, mark this function. Related skills later
type tempFunc func(*cartExts)

// Implement the CartExt interface
type funcCartExt struct {
    f tempFunc
}

// Implemented interface
func (fdo *funcCartExt) apply(e *cartExts) {
    fdo.f(e)
}

func newFuncCartExt(f tempFunc) *funcCartExt {
    return &funcCartExt{f: f}
}

type DemoCart struct {
    UserID string
    ItemID string
    Sku    int64
    Ext    cartExts
}

var DefaultExt = cartExts{
    CartType: CommonCart,       // Default is normal shopping cart type
    TTL:      time.Minute * 60, // Default 60min expiration
}

func NewCart(userID string, Sku int64, exts ...CartExt) *DemoCart {
    c := &DemoCart{
        UserID: userID,
        Sku:    Sku,
        Ext:    DefaultExt, // Set default
    }
    
    // Traverse to set
    for _, ext := range exts {
        ext.apply(&c.Ext)
    }

    return c
}

After this toss, does our code look like grpc code? As a last step, you need to wrap a function around each field of cartExts.


func WithCartType(cartType string) CartExt {
    return newFuncCartExt(func(exts *cartExts) {
        exts.CartType = cartType
    })
}

func WithTTL(d time.Duration) CartExt {
    return newFuncCartExt(func(exts *cartExts) {
        exts.TTL = d
    })
}

For users, you only need to do the following:

exts := []CartExt{
    WithCartType(CommonCart),
    WithTTL(1000),
}

NewCart("dayu", 888, exts...)

summary

Is it very simple? Let's summarize the code building skills here:

  1. The alternative is converged to a uniform structure, and the field is privatized;
  2. Define an interface type. This interface provides a method. The parameter of the method should be the pointer type of the structure of the optional property set. Because we want to modify its internal value, we must use the pointer type;
  3. Define a function type, which should be consistent with the parameters of the methods in the interface type, and use the structure pointer with optional convergence as the parameter; (very important)
  4. Define a structure and implement the interface type in 2; (this step is not necessary, but it is a good programming style)
  5. The interface type is implemented to encapsulate the method corresponding to the optional field. The command is suggested to use With + field name.

According to the above five steps, you can set the default value of high-level play.

If you like this type of article, welcome to leave a message like!

Tags: Go Google PHP Python Attribute

Posted on Tue, 23 Jun 2020 23:30:39 -0400 by curtisdw