Learn from other people’s code! Let yourself grow

Recently, I used GRPC to find a very good design, which is worth learning from.

Golang does not provide the ability to set default values for dynamic languages such as PHP and Python.

Low level players deal with default values

Take a shopping cart. For example, I have the following structure of a shopping cart, and CartExts is an extended attribute, which has its own default value. Users hope that this parameter will not be transmitted if the default value is not changed. But since Golang can’t set default values in the parameters, there are only a few options:

  1. Provide an initialization function, all of themextFields are used as arguments. Passing a zero value of that type when not needed exposes the complexity to the caller.
  2. willextThis structure is used as a parameter in the initialization function, and1Again, complexity lies in the caller;
  3. Provides multiple initialization functions with internal default Settings for each scenario.

Now what does the code 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, // The default is normal shopping cart type  TTL: time.Minute * 60.// The default expiration time is 60 minutes }  // Method 1: Each extension 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,  } }  // Separate initialization functions for multiple scenes; Method two will rely on an underlying 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, "") }  Copy the code

The above code looks fine, but the most important consideration in our code design is stability and change. We need to be open to extension, close to modification, and highly cohesive. So in the above code, you added a field or reduced a field in the CartExts. Does everything need to be changed? Or in CartExts, if there are a lot of fields, do we have to write multiple constructors for different scenes? So here’s a quick overview of the problems with the above approach.

  1. Is not convenient toCartExtsField for extension;
  2. ifCartExtsVery many fields, constructor arguments are long, ugly, and difficult to maintain;
  3. All field constructs are logically redundant inNewCartIn, the noodle code is not elegant;
  4. If theCartExtsAs a parameter, it exposes too much detail to the caller.

Next, let’s take a look at how GRPC does it, learning excellent examples and improving my code ability.

From this you can also realize the code base niu Forced people, code is to write the United States!

Default value for advanced players of GRPC

Source: [email protected] version. Necessary cuts were made to the code to highlight the main goals.


/ / dialOptions definition in google.golang.org/grpc/dialoptions.go in detail
type dialOptions struct {
    / /... .
 insecure    bool
 timeout time.Duration  / /... . }  / / ClientConn definition in google.golang.org/grpc/clientconn.go in detail 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(),  }  / /... .   // Change to the default value of the user  for _, opt := range opts {  opt.apply(&cc.dopts)  }  / /... . } Copy the code

The DialContext function is a GRPC link creation function that builds ClientConn as the return value. DefaultDialOptions returns the default value of the DOPTS field provided by the system. If you want to customize the optional properties, you can use the opTS variable to control.

After the above improvements, we were surprised to find that the constructor is so elegant that no matter how dopts fields are added or decreased, the constructor does not need to be changed; DefaultDialOptions can also be changed from a public field to a private field, making it more cohesive and caller-friendly.

So how does this work? Let’s learn the implementation idea together.

DialOption encapsulation

First of all, the first technical point here is the DialOption parameter type. We’ve optimized optional field modifications by adding constructor arguments, but to do so requires ensuring that the optional fields are of the same type, which is practically impossible. So it’s the highest level of programming, and if it doesn’t work, it adds another layer.

With this interface type, we can unify the different field types and simplify constructor entry. Take a look at this interface.

type DialOption interface {
 apply(*dialOptions)
}
Copy the code

This interface has a method whose arguments are of type *dialOptions, as we can also see from the code in the for loop above, passing in &cc.dopts. Basically, you pass in the object that you want to modify. The apply method implements the specific change logic internally.

So, since this is an interface, there must be a concrete implementation. Let’s look at the implementation.

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

func (EmptyDialOption) apply(*dialOptions) {}

// What are the most used parts 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,  } } Copy the code

We focus on the implementation funcDialOption. This is an advanced usage that shows that functions are first-class citizens in Golang. It has a constructor and implements the DialOption interface.

The newFuncDialOption constructor takes a function as a unique argument and saves the passed function on field F of funcDialOption. The parameter type of the function is *dialOptions, which is the same as the parameters of the apply method. This is the second important point of the design.

Now it’s time to look at the implementation of the Apply method. It is very simple, and is essentially the method passed in when the constructor funcDialOption is called. You can think of it as acting as an agent. Drop the object to be modified by apply into the f method. So all the important logic is implemented by the parameter method we pass to the function newFuncDialOption.

Now let’s see where the newFuncDialOption constructor is called inside the GRPC.

NewFuncDialOption calls

Since *funcDialOption returned by newFuncDialOption implements the DialOption interface, pay attention to where it is called, We can find the parameters that our original GRPC.DialContext constructor opts can pass in.

This method is called in so many places that we’ll just focus on the methods corresponding to the two fields listed in the article: Insecure and timeout.


/ / the following methods defined in google.golang.org/grpc/dialoptions.go in detail
// Enable insecure transmission
func WithInsecure(a) DialOption {
 return newFuncDialOption(func(o *dialOptions) {
 o.insecure = true  }) }  / / set the timeout func WithTimeout(d time.Duration) DialOption {  return newFuncDialOption(func(o *dialOptions) {  o.timeout = d  }) } Copy the code

Take a look at the design here:

  1. First, for each field, provide a method to set its corresponding value. Because each method returns a type ofDialOption, thus ensuring thatgrpc.DialContextMethods can take optional arguments because the types are consistent;
  2. The true type returned is*funcDialOptionBut it implements the interfaceDialOption, which increases scalability.

GRPC. DialContext calls

Finished the above program construction, now we stand in the use of the Angle, feel this infinite amorous feelings.


opts := []grpc.DialOption{
    grpc.WithTimeout(1000),
    grpc.WithInsecure(),
}
 conn, err := grpc.DialContext(context.Background(), target, opts...) / /... . Copy the code

Of course, the focus here is the slice of OPTS, whose element is the object that implements the DialOption interface. Both methods are wrapped as *funcDialOption objects that implement the DialOption interface, so the return value of these calls is the slice element.

Now we can go inside the grpc.DialContext method and see how it is called internally. Iterate through the OPTS, then call the Apply method in turn to complete the setup.

// Change to the default value of the user
for _, opt := range opts {
    opt.apply(&cc.dopts)
}
Copy the code

After such layers of packaging, although increased a lot of code, but can obviously feel the beauty of the whole code, scalability has been improved. Now, how can we improve our own demo?

Improve the DEMO code

First, we need to transform the structure into CartExts, and we need to design a package type to wrap all the extended fields, and make this package type an optional parameter of the constructor.


const (
 CommonCart = "common"
 BuyNowCart = "buyNow"
)
 type cartExts struct {  CartType string  TTL time.Duration }  type CartExt interface {  apply(*cartExts) }  // Add a new type to mark this function. Related techniques are described below type tempFunc func(*cartExts)  / / implementationCartExtinterfacetype funcCartExt struct {  f tempFunc }  // Implement the 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, // The default is normal shopping cart type  TTL: time.Minute * 60.// The default expiration time is 60 minutes }  func NewCart(userID string, Sku int64, exts ... CartExt) *DemoCart {  c := &DemoCart{  UserID: userID,  Sku: Sku,  Ext: DefaultExt, // Set the default value  }   // Walk through the Settings  for _, ext := range exts {  ext.apply(&c.Ext)  }   return c }  Copy the code

After all this, does our code look a lot like GRPC code? The last step is to wrap a function for each field of the 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  }) }  Copy the code

For the user, just do the following:

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

NewCart("dayu".888, exts...) Copy the code

conclusion

Is it very simple? Let’s summarize the code construction techniques here:

  1. Converging the options into a unified structure; And privatize the field;
  2. Define an interface type that provides a method whose argument should be a pointer type to the structure of the optional property collection, because we need to modify its internal value.
  3. Define a function type that takes the optional convergent structure pointer as arguments to the methods in the interface type. (Very important)
  4. Define a structure and implement2Interface type in; (This step is not necessary, but it is good programming style.)
  5. The method corresponding to the optional field is encapsulated by the type of interface implemented. You are advised to use the With + field name.

By following the five steps above, you can achieve advanced gameplay with default Settings.

If you like this type of article, please leave a comment and like it!

My official account is dayuTalk

GitHub:github.com/helei112g