Расширенный игровой процесс с настройками значений по умолчанию для навыков Голанга.

Go дизайн
Расширенный игровой процесс с настройками значений по умолчанию для навыков Голанга.

Берите пищу из чужого кода! позвольте себе расти

Недавно использованныйGRPCСтоит поучиться тому, как найти место с особенно хорошим дизайном.

Когда мы ежедневно пишем методы, мы хотим установить значение по умолчанию для поля.Этот параметр не передается в сценариях, которые не требуют настройки, ноGolangно не обеспечиваетPHP,PythonВозможность этого динамического языка устанавливать значения по умолчанию для параметров метода.

Низкоуровневые игроки имеют дело со значениями по умолчанию.

Возьмем, к примеру, корзину для покупок. Например, у меня есть следующая структура корзины, в которойCartExtsЭто расширенный атрибут, у него есть собственное значение по умолчанию, и пользователь не хочет передавать этот параметр, если значение по умолчанию не изменено. Однако из-заGolangНет возможности задать значения по умолчанию в параметрах, есть всего несколько вариантов:

  1. Обеспечьте функцию инициализации, всеextВсе поля используются как параметры.Если нулевое значение типа передается, когда оно не нужно, это выставляет сложность для вызывающего;
  2. будетextЭта структура используется в качестве параметра в функции инициализации с1Опять же, сложность заключается в вызывающей стороне;
  3. Предоставляет несколько функций инициализации с внутренними настройками по умолчанию для каждого сценария.

Посмотрим, как будет работать код

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,       // 默认是普通购物车类型
 TTL:      time.Minute * 60, // 默认 60min 过期
}

// 方式一:每个扩展数据都做为参数
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,
 }
}

// 方式二:多个场景的独立初始化函数;方式二会依赖一个基础的函数
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, "")
}

Вышеприведенный код выглядит нормально, но наиболее важными факторами при разработке кода являются стабильность и изменения, которые нам нужно сделать.Открыт для расширения, закрыт для модификациии кодвысокая сплоченность. то, если это приведенный выше код, вы находитесь вCartExtsПоле добавляется или поле удаляется. Везде надо переделывать? илиCartExtsЕсли полей много, нужно ли нам писать много конструкторов для разных сценариев? Итак, кратко опишите проблемы с вышеописанным подходом.

  1. неудобное правоCartExtsПоле расширено;
  2. еслиCartExtsПолей много, а параметры конструктора очень длинные, что некрасиво и сложно в обслуживании;
  3. Вся логика построения поля избыточна вNewCart, спагетти-код не элегантен;
  4. При использованииCartExtsВ качестве параметра он предоставляет вызывающей стороне слишком много деталей.

Далее посмотримGRPCКак это сделать, изучите отличные примеры и улучшите свои навыки кодирования.

Из этого вы также можете понять, что для людей с отличными навыками кодирования код — это красота письма!

Игроки высокого уровня GRPC устанавливают значение по умолчанию

Исходный код из версии: grpc@v1.28.1. Код был урезан там, где это необходимо, чтобы выделить основную цель.


// dialOptions 详细定义在 google.golang.org/grpc/dialoptions.go
type dialOptions struct {
    // ... ...
 insecure    bool
    timeout     time.Duration
    // ... ...
}

// ClientConn 详细定义在 google.golang.org/grpc/clientconn.go
type ClientConn struct {
    // ... ...
 authority    string
 dopts        dialOptions // 这是我们关注的重点,所有可选项字段都在这里
    csMgr        *connectivityStateManager
    
    // ... ...
}

// 创建一个 grpc 链接
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(), // 默认值选项
  blockingpicker:    newPickerWrapper(),
  czData:            new(channelzData),
  firstResolveEvent: grpcsync.NewEvent(),
    }
    // ... ...

    // 修改改选为用户的默认值
 for _, opt := range opts {
  opt.apply(&cc.dopts)
    }
    // ... ...
}

Смысл вышеприведенного кода очень ясен, можно считать, чтоDialContextФункция представляет собой функцию создания ссылки grpc, которая в основном построена внутриClientConnЭта структура и как возвращаемое значение.defaultDialOptionsВозвращаемая функция является системой.doptsЗначение поля по умолчанию, если пользователь хочет настроить необязательный атрибут, он может передать переменный параметрoptsконтролировать.

После вышеперечисленных доработок мы с удивлением обнаруживаем, что этот конструктор очень красивый, несмотря ни на чтоdoptsКак увеличивать или уменьшать поля, конструктор менять не нужно;defaultDialOptionsЕго также можно изменить с общедоступного поля на частное поле, что является более связным и удобным для вызывающих абонентов.

Так как же все это работает? Давайте изучим эту идею реализации вместе.

Инкапсуляция DialOption

Прежде всего, первый технический момент здесь,DialOptionТип этого параметра. Мы оптимизируем неловкость добавления параметров конструктора при модификации необязательных полей через необязательные параметры, но для этого нам нужно обеспечить согласованность типов необязательных полей, что на практике невозможно. Поэтому они прибегли к самому высокому методу в мире программирования: если один слой не может быть реализован, добавьте еще один слой.

Через этот тип интерфейса реализована унификация разных типов полей, упрощены параметры конструктора. Взгляните на этот интерфейс.

type DialOption interface {
 apply(*dialOptions)
}

Этот интерфейс имеет метод, параметр которого*dialOptionsТип, мы также можем видеть из кода в цикле for выше, входящий&cc.dopts. Проще говоря, объект, который нужно изменить, передается.applyКонкретная логика модификации реализована внутри метода.

Ну, раз это интерфейс, то должна быть и конкретная реализация. Давайте посмотрим на реализацию.

// 空实现,什么也不做
type EmptyDialOption struct{}

func (EmptyDialOption) apply(*dialOptions) {}

// 用到最多的地方,重点讲
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,
 }
}

мы фокусируемся наfuncDialOptionЭта реализация. Это расширенное использование, отражающее, что функция в Golangгражданин первого класса. Он имеет конструктор и реализуетDialOptionинтерфейс.

newFuncDialOptionКонструктор принимает функцию в качестве единственного параметра, а затем сохраняет переданную функцию вfuncDialOptionполеfначальство. Давайте посмотрим на тип параметра этой функции параметра:*dialOptionsapplyПараметры метода согласованы, что является вторым важным моментом конструкции.

время смотретьapplyметод реализован. Это очень просто, на самом деле это просто вызов конструктораfuncDialOptionметод передан. Его можно понимать как эквивалент агента. ПучокapplyОбъект, который нужно изменить, помещается вfЭтот метод в этом методе. Так важна логика нас в свою очередьnewFuncDialOptionРеализован параметрический метод этой функции.

Проверьте это сейчасgrpcГде внутренние звонки?newFuncDialOptionэтот конструктор.

Вызов newFuncDialOption

так какnewFuncDialOptionвозвращение*funcDialOptionДостигнутоDialOptionинтерфейс, поэтому обратите внимание на то, где он называется, и вы можете найти наш оригинальныйgrpc.DialContextКонструкторoptsПараметры, которые можно передать.

Есть много мест, где этот метод вызывается, мы сосредоточимся только на методах, соответствующих двум полям, перечисленным в статье:insecureиtimeout.


// 以下方法详细定义在 google.golang.org/grpc/dialoptions.go
// 开启不安全传输
func WithInsecure() DialOption {
 return newFuncDialOption(func(o *dialOptions) {
  o.insecure = true
 })
}

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

Приходите и испытайте изысканный дизайн здесь:

  1. Во-первых, для каждого поля предоставьте метод для установки соответствующего значения. Поскольку тип, возвращаемый каждым методом,DialOption, таким образом гарантируя, чтоgrpc.DialContextМетоды могут использовать необязательные параметры, поскольку типы одинаковы;
  2. Возвращаемый истинный тип*funcDialOption, но он реализует интерфейсDialOption, что увеличивает масштабируемость.

Вызов grpc.DialContext

После завершения описанной выше конструкции программы, теперь давайте встанем в перспективу использования и почувствуем бесконечный стиль.


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

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

Конечно, тут делоoptsЭтот слайс, его элементы реализованыDialOptionобъект интерфейса. Вышеупомянутые два метода упакованы*funcDialOptionобъект, реализующийDialOptionинтерфейса, поэтому возвращаемое значение этих вызовов функций является элементом этого слайса.

Теперь мы можем пойти вgrpc.DialContextВнутри этого метода посмотрите, как он вызывается внутри. траверсopts, а затем позвонитеapplyспособ завершения настройки.

// 修改改选为用户的默认值
for _, opt := range opts {
    opt.apply(&cc.dopts)
}

После такой послойной упаковки, хотя и было добавлено много кода, очевидно, что красота и масштабируемость всего кода улучшились. Далее, давайте посмотрим, как можно улучшить нашу собственную демонстрацию?

Улучшить ДЕМО-код

Во-первых, нам нужно изменить структуру,CartExtsстатьcartExts, и вам нужно разработать тип оболочки для переноса всех полей расширения и использовать этот тип оболочки в качестве необязательного параметра конструктора.


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

type cartExts struct {
 CartType string
 TTL      time.Duration
}

type CartExt interface {
 apply(*cartExts)
}

// 这里新增了类型,标记这个函数。相关技巧后面介绍
type tempFunc func(*cartExts)

// 实现 CartExt 接口
type funcCartExt struct {
 f tempFunc
}

// 实现的接口
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,       // 默认是普通购物车类型
 TTL:      time.Minute * 60, // 默认 60min 过期
}

func NewCart(userID string, Sku int64, exts ...CartExt) *DemoCart {
 c := &DemoCart{
  UserID: userID,
  Sku:    Sku,
  Ext:    DefaultExt, // 设置默认值
    }
    
    // 遍历进行设置
 for _, ext := range exts {
  ext.apply(&c.Ext)
 }

 return c
}

После всех этих метаний наш код очень похож на код grpc? Остался еще последний шаг,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
 })
}

Для пользователей просто сделайте следующее:

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

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

Суммировать

Это очень просто? Давайте подытожим навыки построения этого кода вместе:

  1. Объединение опций в единую структуру и приватизация поля;
  2. Определите тип интерфейса, этот интерфейс предоставляет метод, параметр метода должен быть типом указателя структуры дополнительного набора атрибутов, потому что мы хотим изменить его внутреннее значение, поэтому тип указателя должен быть;
  3. Определите тип функции, функция должна иметь те же параметры, что и метод в типе интерфейса, и использовать необязательный указатель конвергентной структуры в качестве параметра; (очень важно)
  4. определить структуру и внедрить2тип интерфейса в ; (этот шаг не обязателен, но это хороший стиль программирования)
  5. Используйте тип, реализующий интерфейс, для инкапсуляции метода, соответствующего необязательному полю, рекомендуется использовать командуС + именем поляПуть.

Согласно приведенному выше пятишаговому Дафа, вы можете достичь высокого уровня игрового процесса, установив значения по умолчанию.

Если вам нравится этот тип статьи, пожалуйста, оставьте комментарий и лайк!

Личный общедоступный номер: dayuTalk

Гитхаб:github.com/helei112g