Берите пищу из чужого кода! позвольте себе расти
Недавно использованныйGRPC
Стоит поучиться тому, как найти место с особенно хорошим дизайном.
Когда мы ежедневно пишем методы, мы хотим установить значение по умолчанию для поля.Этот параметр не передается в сценариях, которые не требуют настройки, ноGolang
но не обеспечиваетPHP
,Python
Возможность этого динамического языка устанавливать значения по умолчанию для параметров метода.
Низкоуровневые игроки имеют дело со значениями по умолчанию.
Возьмем, к примеру, корзину для покупок. Например, у меня есть следующая структура корзины, в которойCartExts
Это расширенный атрибут, у него есть собственное значение по умолчанию, и пользователь не хочет передавать этот параметр, если значение по умолчанию не изменено. Однако из-заGolang
Нет возможности задать значения по умолчанию в параметрах, есть всего несколько вариантов:
- Обеспечьте функцию инициализации, все
ext
Все поля используются как параметры.Если нулевое значение типа передается, когда оно не нужно, это выставляет сложность для вызывающего; - будет
ext
Эта структура используется в качестве параметра в функции инициализации с1
Опять же, сложность заключается в вызывающей стороне; - Предоставляет несколько функций инициализации с внутренними настройками по умолчанию для каждого сценария.
Посмотрим, как будет работать код
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
Если полей много, нужно ли нам писать много конструкторов для разных сценариев? Итак, кратко опишите проблемы с вышеописанным подходом.
- неудобное право
CartExts
Поле расширено; - если
CartExts
Полей много, а параметры конструктора очень длинные, что некрасиво и сложно в обслуживании; - Вся логика построения поля избыточна в
NewCart
, спагетти-код не элегантен; - При использовании
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
начальство. Давайте посмотрим на тип параметра этой функции параметра:*dialOptions
,иapply
Параметры метода согласованы, что является вторым важным моментом конструкции.
время смотреть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
})
}
Приходите и испытайте изысканный дизайн здесь:
- Во-первых, для каждого поля предоставьте метод для установки соответствующего значения. Поскольку тип, возвращаемый каждым методом,
DialOption
, таким образом гарантируя, чтоgrpc.DialContext
Методы могут использовать необязательные параметры, поскольку типы одинаковы; - Возвращаемый истинный тип
*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...)
Суммировать
Это очень просто? Давайте подытожим навыки построения этого кода вместе:
- Объединение опций в единую структуру и приватизация поля;
- Определите тип интерфейса, этот интерфейс предоставляет метод, параметр метода должен быть типом указателя структуры дополнительного набора атрибутов, потому что мы хотим изменить его внутреннее значение, поэтому тип указателя должен быть;
- Определите тип функции, функция должна иметь те же параметры, что и метод в типе интерфейса, и использовать необязательный указатель конвергентной структуры в качестве параметра; (очень важно)
- определить структуру и внедрить
2
тип интерфейса в ; (этот шаг не обязателен, но это хороший стиль программирования) - Используйте тип, реализующий интерфейс, для инкапсуляции метода, соответствующего необязательному полю, рекомендуется использовать командуС + именем поляПуть.
Согласно приведенному выше пятишаговому Дафа, вы можете достичь высокого уровня игрового процесса, установив значения по умолчанию.
Если вам нравится этот тип статьи, пожалуйста, оставьте комментарий и лайк!
Личный общедоступный номер: dayuTalk
Гитхаб:github.com/helei112g