OOP 思想在 TCC/APIX/GORM 源码中的应用

admin 2022年5月16日09:47:44评论7 views字数 20411阅读68分2秒阅读模式

动手点关注 干货不迷路 👆

名词解释

OOP

面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP 的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP 达到了软件工程的三个主要目标:重用性、灵活性和扩展性。面向对象编程的三大特点:封装性、继承性和多态性。

TCC

动态配置中心 TCC(ToutiaoConfigCenter)是提供给业务方的一套平台+SDK 的配置管理解决方案,提供的功能有权限管理、配置管理、版本管理、灰度发布、多地区多环境支持等。与百度开源的“百度分布式配置中心 BRCC”功能类似。

APIX

Golang 实现的 web 框架,可参考开源项目 Gin。

GORM

Golang 编写的热门数据库 ORM 框架。

背景

大力智能学习灯于 2019 年 10 月份上线,截止 2021 年底,台灯出货量已超过 100w 台,完成了从 0 到 1 的探索。在成立之初,很多方向的产品为了尽早拿到用户反馈,要求快速迭代,研发在代码实现上相对快糙猛,早期阶段这无可厚非,但慢慢地,自习室、系统工具、知识宇宙等应用已经变成灯上核心基建,如果还按之前的野蛮生长的方式将会为台灯的成长埋下隐患。

在这样的背景下,大力智能服务端推动 OOP 技术专项的落地,希望能够:提升团队成员自身的编码水平;统一团队内部编程风格;支撑业务快速迭代。

TCC、APIX、GORM 都是日常项目中经常会依赖到的外部包,本文从这些项目的源码出发,在学习的过程中,解读良好的代码设计在其中的应用,希望能帮忙大家更好的理解和应用 OOP 思想,写出更优秀的代码。

OOP 原则

单一职责原则(SRP)

一个类只负责一个职责(功能模块)。

开放封闭原则(OCP)

一个类、方法或模块的扩展性要保持开放,可扩展但不影响源代码(封闭式更改)

替换原则(LSP)

子类可以替换父类,并且不会导致程序错误。

接口隔离原则(ISP)

一个类对另一个类的依赖应该建立在最小的接口上。

依赖倒置原则(DIP)

高层次的模块不应该依赖于低层次的模块,它们应该依赖于抽象。

参数可选,开箱即用—函数式选项模式

解决问题:在设计一个函数时,当存在配置参数较多,同时参数可选时,函数式选项模式是一个很好的选择,它既有为不熟悉的调用者准备好的默认配置,还有为需要定制的调用者提供自由修改配置的能力,且支持未来灵活扩展属性。

TCC 在创建BConfigClient对象时使用了该模式。BConfigClient是用于发送 http 请求获取后端服务中 key 对应的 value 值,其中getoptions结构体是 BConfigClient 的配置类,包含请求的 cluster、addr、auth 等信息,小写开头,属于内部结构体,不允许外部直接创建和修改,但同时对外提供了GetOption的方法去修改getoptions中的属性,其中WithClusterWithAddrWithAuth是快捷生成GetOption的函数。

这样的方式很好地控制了哪些属性能被外部修改,哪些是不行的。当getoptions需要增加新属性时,给定一个默认值,对应增加一个新GetOption方法即可,对于历史调用方来说无感,能向前兼容式的升级,符合 OOP 中的对修改关闭,对扩展开放的开闭设计原则。

type getoptions struct {
   cluster string
   addr    string
   auth    bool
}

// GetOption represents option of get op
type GetOption func(o *getoptions)

// WithCluster sets cluster of get context
func WithCluster(cluster string) GetOption {
   return func(o *getoptions) {
      o.cluster = cluster
   }
}

// WithAddr sets addr for http request instead get from consul
func WithAddr(addr string) GetOption {
   return func(o *getoptions) {
      o.addr = addr
   }
}
// WithAuth Set the GDPR Certify On.
func WithAuth(auth bool) GetOption {
   return func(o *getoptions) {
      o.auth = auth
   }
}

NewBConfigClient方法接受一个可变长度的GetOption,意味着调用者可以不用传任何参数,开箱即用,也可以根据自己的需要灵活添加。函数内部首先初始化一个默认配置,然后循环执行GetOption方法,将用户定义的操作赋值给默认配置。

// NewBConfigClient creates instance of BConfigClient
func NewBConfigClient(opts ...GetOption) *BConfigClient {
   oo := getoptions{cluster: defaultCluster}
   for _, op := range opts {
      op(&oo)
   }
   c := &BConfigClient{oo: oo}
   ......
   return c
}

通过组合扩展功能—装饰模式

解决问题:当已有类功能不够便捷时,通过组合的方式实现对已有类的功能扩展,实现了对已有代码的黑盒复用。

TCC 使用了装饰模式扩展了原来已有的ClientV2的能力。

在下面的DemotionClient结构体中组合了ClientV2的引用,对外提供了GetIntGetBool两个方法,包掉了对原始 string 类型的转换,对外提供了更为便捷的方法。

// Get 获取key对应的value.
func (c *ClientV2) Get(ctx context.Context, key string) (string, error)
type DemotionClient struct {
   *ClientV2
}

func NewDemotionClient(serviceName string, config *ConfigV2) (*DemotionClient, error) {
   clientV2, err := NewClientV2(serviceName, config)
   if err != nil {
      return nil, err
   }
   client := &DemotionClient{clientV2}
   return client, nil
}

// GetInt parse value to int
func (d *DemotionClient) GetInt(ctx context.Context, key string) (int, error) {
   value, err := d.Get(ctx, key)
   if err != nil {
      return 0, err
   }
   ret, err := strconv.Atoi(value)
   if err != nil {
      return 0, fmt.Errorf("GetInt Error: Key = %s; value = %s is not int", key, value)
   }
   return ret, nil
}

// GetBool parse value to bool:
//     if value=="0" return false;
//     if value=="1" return true;
//     if value!="0" && value!="1" return error;
func (d *DemotionClient) GetBool(ctx context.Context, key string) (bool, error) {
   ......
   // 类似GetInt方法
}

由于 Golang 语言对嵌入类型的支持,DemotionClient在扩展能力的同时,ClientV2的原本方法也能正常调用,这样语法糖的设计让组合操作达到了继承的效果,且符合 OOP 中替换原则。

与 Java 语言对比,如下面的例子,类 A 和类 B 实现了IHi的接口,类 C 组合了接口IHi, 如果需要暴露IHi的方法,则类 C 需要添加一个代理方法,这样 java 语言的组合在代码量上会多于继承方式,而 Golang 中无需额外代码即可提供支持。

public interface IHi {
        public void hi();
    }

    public class A implements IHi {
        @Override
        public void hi() {
            System.out.println("Hi, I am A.");
        }
    }

    public class B implements IHi {
        @Override
        public void hi() {
            System.out.println("Hi, I am B.");
        }
    }

    public class C {
        IHello h;

        public void hi() {
            h.hi();
        }
    }

    public static void main(String args[]) {
        C c = new C();
        c.h = new A();
        c.hi();
        c.h = new B();
        c.hi();
    }

隐藏复杂对象构造过程—工厂模式

解决问题:将对象复杂的构造逻辑隐藏在内部,调用者不用关心细节,同时集中变化。

TCC 创建LogCounnter时使用了工厂模式,该类作用是根据错误日志出现的频率判断是否需要打印日志,如果在指定的时间里,错误日志的触发超过指定次数,则需要记录日志。

NewLogCounter方法通过入参 LogMode 枚举类型即可生成不同规格配置的LogCounnter,可以无需再去理解 TriggerLogCount、TriggerLogDuration、Enable 的含义。

type LogMode string

const (
   LowMode       LogMode = "low"
   MediumMode    LogMode = "medium"
   HighMode      LogMode = "high"
   AlwaysMode    LogMode = "always"
   ForbiddenMode LogMode = "forbidden"
)

// In TriggerLogDuration, if error times < TriggerLogCount pass, else print error log.
type LogCounter struct {
   FirstLogTime       time.Time
   LogCount           int
   mu                 sync.RWMutex
   TriggerLogCount    int
   TriggerLogDuration time.Duration
   Enable             bool // If Enable is true, start the rule.
}

func NewLogCounter(logMode LogMode, triggerLogCount int, triggerLogDuration time.Duration) *LogCounter {
   logCounter := &LogCounter{}
   switch logMode {
   case AlwaysMode:
      logCounter.Enable = false
   case LowMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 5
      logCounter.TriggerLogDuration = 60 * time.Second
   case MediumMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 5
      logCounter.TriggerLogDuration = 300 * time.Second
   case HighMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 3
      logCounter.TriggerLogDuration = 300 * time.Second
   case ForbiddenMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 0
   }
   if triggerLogCount > 0 {
      logCounter.Enable = true
      logCounter.TriggerLogCount = triggerLogCount
      logCounter.TriggerLogDuration = triggerLogDuration
   }
   return logCounter
}

func (r *LogCounter) CheckPrintLog() bool
func (r *LogCounter) CheckDiffTime(lastErrorTime, newErrorTime time.Time) bool

识别变化隔离变化,简单工厂是一个显而易见的实现方式。它符合了 DRY 原则(Don't Repeat Yourself!),创建逻辑存放在单一的位置,即使它变化,也只需要修改一处就可以了。DRY 很简单,但却是确保我们代码容易维护和复用的关键。DRY 原则同时还提醒我们:对系统职能进行良好的分割,职责清晰的界限一定程度上保证了代码的单一性。[引用自 https://blog.51cto.com/weijie/82767]

一步步构建复杂对象—建造者模式

解决问题:使用多个简单的对象一步一步构建成一个复杂的对象。

APIX 在创建请求的匹配函数Matcher时使用了建造者模式。

APIX 中提供了指定对哪些 request 生效的中间件,定义和使用方式如下,CondHandlersChain结构体中定义了匹配函数Matcher和命中后执行的处理函数HandlersChain

以“对路径前缀为`/wechat` 的请求开启微信认证中间件”为例子,Matcher 函数不用开发者从头实现一个,只需要初始化 SimpleMatcherBuilder 对象,设置请求前缀后,直接 Build 出来即可,它将复杂的匹配逻辑隐藏在内部,非常好用。

// Conditional handlers chain
type CondHandlersChain struct {
   // 匹配函数
   Matcher func(method, path string) bool
   // 命中匹配后,执行的处理函数
   Chain   HandlersChain
}

// 对路径前缀为 `/wechat` 的请求开启微信认证中间件
mw1 := apix.CondHandlersChain{
   Matcher: new(apix.SimpleMatcherBuilder).PrefixPath("/wechat").Build(),
   Chain:   apix.HandlersChain{wxsession.NewMiddleware()},
}

// 注册中间件
e.CondUse(mw1)

SimpleMatcherBuilder是一个建造者,它实现了MatcherBuilder接口,该类支持 method、pathPrefix 和 paths 三种匹配方式,业务方通过Method()PrefixPath()FullPath()三个方法的组合调用即可构造出期望的匹配函数。

type MatcherBuilder interface {
   Build() func(method, path string) bool
}

var _ MatcherBuilder = (*SimpleMatcherBuilder)(nil)

// SimpleMatcherBuilder build a matcher for CondHandlersChain.
// An `AND` logic will be applied to all fields(if provided).
type SimpleMatcherBuilder struct {
   method     string
   pathPrefix string
   paths      []string
}

func (m *SimpleMatcherBuilder) Method(method string) *SimpleMatcherBuilder {
   m.method = method
   return m
}

func (m *SimpleMatcherBuilder) PrefixPath(path string) *SimpleMatcherBuilder {
   m.pathPrefix = path
   return m
}

func (m *SimpleMatcherBuilder) FullPath(path ...string) *SimpleMatcherBuilder {
   m.paths = append(m.paths, path...)
   return m
}

func (m *SimpleMatcherBuilder) Build() func(method, path string) bool {
   method, prefix := m.method, m.pathPrefix
   paths := make(map[string]struct{}, len(m.paths))
   for _, p := range m.paths {
      paths

 = struct{}{}
   }

   return func(m, p string) bool {
      if method != "" && m != method {
         return false
      }
      if prefix != "" && !strings.HasPrefix(p, prefix) {
         return false
      }

      if len(paths) == 0 {
         return true
      }

      _, ok := paths


      return ok
   }
}

var _ MatcherBuilder = (AndMBuilder)(nil)
var _ MatcherBuilder = (OrMBuilder)(nil)
var _ MatcherBuilder = (*NotMBuilder)(nil)
var _ MatcherBuilder = (*ExcludePathBuilder)(nil)
......

除此之外,ExcludePathBuilderAndMBuilderOrMBuilder*NotMBuilder也实现了MatcherBuilder接口,某些对象内部又嵌套了对MatcherBuilder的调用,达到了多条件组合起来匹配的目的,非常灵活。

// 路径以 `/api/v2` 开头的请求中,除了 `/api/v2/legacy` 外,都开启中间件
mb1 := new(apix.SimpleMatcherBuilder).PrefixPath("/api/v2")
mb2 := new(apix.ExcludePathBuilder).FullPath("/api/v2/legacy")
mw3 := apix.CondHandlersChain{
    Matcher: apix.AndMBuilder{mb1, mb2}.Build(),
    Chain: apix.HandlersChain{...},
}
e.CondUse(mw1, mw2)

工厂方法模式 VS 建造者模式

工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。

举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。[引用自 https://www.cnblogs.com/ChinaHook/p/7471470.html]

Web 中间件—责任链模式

解决问题:当业务处理流程很长时,可将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到没有对象处理它为止。

APIX 应用了责任链模式来实现中间件的功能,类似的逻辑可参考文章“Gin 中间件的编写和使用”。

首先要定义中间件接口,即下文中的HandlerFunc,然后定义HandlersChain将一组处理函数组合成一个处理链条,最后将HandlersChain插入Context中。

开始执行时,是调用ContextNext函数,遍历每个HandlerFunc,然后将Context自身的引用传入,index是记录当前执行到第几个中间件,当过程中出现不满足继续进行的条件时,可以调用Abort()来终止流程。

// 定义中间件的接口
type HandlerFunc func(*Context)

// 将一组处理函数组合成一个处理链条
type HandlersChain []HandlerFunc

// 处理的上下文
type Context struct {
    // ...

    // handlers 是一个包含执行函数的数组
    // type HandlersChain []HandlerFunc
        handlers HandlersChain
    // index 表示当前执行到哪个位置了
        index    int8

    // ...
}

// Next 会按照顺序将一个个中间件执行完毕
// 并且 Next 也可以在中间件中进行调用,达到请求前以及请求后的处理
func (c *Context) Next() {
   c.index++
   for c.index < int8(len(c.handlers)) {
      if handler := c.handlers[c.index]; handler != nil {
         handler(c)
      }
      c.index++
   }
}

// 停止中间件的循环, 通过将索引后移到abortIndex实现。
func (c *Context) Abort() {
   if c.IsDebugging() && c.index < int8(len(c.handlers)) {
      handler := c.handlers[c.index]
      handlerName := nameOfFunction(handler)
      c.SetHeader("X-APIX-Aborted", handlerName)
   }

   c.index = abortIndex
}

下面是一个检查用户是否登录的中间件实现,业务方也可以实现自己的中间件插入到请求处理中,非常灵活。

// RequireLogin 检查用户是否登陆成功。如果不是,终止请求。
func RequireLogin(c *apix.Context) {
   if c.Header(agwconsts.Key_LoaderSessionError) == "1" {
      hsuite.AbortWithBizCode(c, bizstat.APIErrRPCFailed)
      return
   }

   if c.UserId() == 0 {
      hsuite.AbortWithBizCode(c, bizstat.APIErrSessionExpired)
      return
   }
}

在服务启动时,注册中间件。

func main() {
   e := apiservice.Default(
      hsuite.WithBizCodeErrs(consts.BizCodeErrs...), // user-defined error code
   )
   // 可通过 e.Use(), e.CondUse() 注册中间件
   e.Use(devicesession.AGWSessionSuccess, devicesession.NewHWSessionMiddleware(), middleware.Tracing)
   ......
   apiservice.Run()
}

什么是洋葱模型

请求进来时,一层一层的通过中间件执行Next函数进入到你设置的下一个中间件中,并且可以通过Context对象一直向下传递下去,当到达最后一个中间件的时候,又向上返回到最初的地方。

该模型常用于记录请求耗时、埋点等场景。

OOP 思想在 TCC/APIX/GORM 源码中的应用

在“Go 语言动手写 Web 框架”[https://geektutu.com/post/gee-day5.html]这篇文章中为我们举了一个浅显易懂的例子。

假设我们应用了中间件 A 和 B,和路由映射的 Handler。c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),接下来的流程是这样的:

func A(c *Context) {
    part1
    c.Next()
    part2
}
func B(c *Context) {
    part3
    c.Next()
    part4
}
- c.index++,c.index 变为 0
- 0 < 3,调用 c.handlers[0],即 A
- 执行 part1,调用 c.Next()
- c.index++,c.index 变为 1
- 1 < 3,调用 c.handlers[1],即 B
- 执行 part3,调用 c.Next()
- c.index++,c.index 变为 2
- 2 < 3,调用 c.handlers[2],即Handler
- Handler 调用完毕,返回到 B 中的 part4,执行 part4
- part4 执行完毕,返回到 A 中的 part2,执行 part2
- part2 执行完毕,结束。

最终的调用顺序是part1 -> part3 -> Handler -> part 4 -> part2

依赖注入,控制反转—观察者模式

解决问题:解耦观察者和被观察者,尤其是存在多个观察者的场景。

TCC 使用了观察者模式实现了当某 key 的 value 发生变更时执行回调的逻辑。

TccClient对外提供AddListener方法,允许业务注册对某 key 变更的监听,同时开启定时轮询,如果 key 的值与上次不同就回调业务的 callback 方法。

这里的观察者是调用 AddListener 的发起者,被观察者是 TCC 的 key。Callback可以看作只有一个函数的接口,TccClient的通知回调不依赖于具体的实现,而是依赖于抽象,同时Callback对象不是在内部构建的,而是在运行时传入的,让被观察者不再依赖观察者,通过依赖注入达到控制反转的目的。

// Callback for listener,外部监听者需要实现该方法传入,用于回调
type Callback func(value string, err error)

// 一个监听者实体
type listener struct {
   key             string
   callback        Callback
   lastVersionCode string
   lastValue       string
   lastErr         error
}

// 检测监听的key是否有发生变化,如果有,则回调callback函数
func (l *listener) update(value, versionCode string, err error) {
   if versionCode == l.lastVersionCode && err == l.lastErr {
      return
   }
   if value == l.lastValue && err == l.lastErr {
      // version_code updated, but value not updated
      l.lastVersionCode = versionCode
      return
   }
   defer func() {
      if r := recover(); r != nil {
         logs.Errorf("[TCC] listener callback panic, key: %s, %v", l.key, r)
      }
   }()
   l.callback(value, err)
   l.lastVersionCode = versionCode
   l.lastValue = value
   l.lastErr = err
}

// AddListener add listener of key, if key's value updated, callback will be called
func (c *ClientV2) AddListener(key string, callback Callback, opts ...ListenOption) error {
   listenOps := listenOptions{}
   for _, op := range opts {
      op(&listenOps)
   }

   listener := listener{
      key:      key,
      callback: callback,
   }
   if listenOps.curValue == nil {
      listener.update(c.getWithCache(context.Background(), key))
   } else {
      listener.lastValue = *listenOps.curValue
   }

   c.listenerMu.Lock()
   defer c.listenerMu.Unlock()
   if _, ok := c.listeners[key]; ok {
      return fmt.Errorf("[TCC] listener already exist, key: %s", key)
   }
   c.listeners[key] = &listener
   // 一个client启动一个监听者
   if !c.listening {
      go c.listen()
      c.listening = true
   }
   return nil
}

// 轮询监听
func (c *ClientV2) listen() {
   for {
      time.Sleep(c.listenInterval)
      listeners := c.getListeners()
      for key := range listeners {
         listeners[key].update(c.getWithCache(context.Background(), key))
      }
   }
}

// 加锁防止多线程同时修改listeners,同时拷贝一份map在循环监听时使用。
func (c *ClientV2) getListeners() map[string]*listener {
   c.listenerMu.Lock()
   defer c.listenerMu.Unlock()
   listeners := make(map[string]*listener, len(c.listeners))
   for key := range c.listeners {
      listeners[key] = c.listeners[key]
   }
   return listeners
}

什么是控制反转(Ioc—Inversion of Control)

控制反转不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序,传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

[引用自 https://blog.csdn.net/bestone0213/article/details/47424255]

什么是依赖注入(DI—Dependency Injection)

组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

[引用自 https://blog.csdn.net/bestone0213/article/details/47424255]

控制反转和依赖注入是同一个概念的不同角度描述。简而言之,当依赖的外部组件时,不要直接从内部 new,而是从外部传入。

替代 IF—策略模式

解决场景:支持不同策略的灵活切换,避免多层控制语句的不优雅实现,避免出现如下场景:

if xxx {
  // do something
else if xxx {
  // do something
else if xxx {
  // do something
else if xxx {
  // do something
else {
}

通常的做法是定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法。

在 GORM 的 clause/clause.go 中使用到策略模式实现 SQL 的拼装。

现实业务中 SQL 语句千变万化,GORM 将 SQL 的拼接过程,拆分成了一个个小的子句,这些子句统一实现clause.Interface这个接口,然后各自在Build方法中实现自己的构造逻辑。

以最简单的分页查询为例,在使用 db 链式调用构建 SQL 时,对LimitOffsetOrder的函数调用最终转化成了Limit子句和OrderBy子句,两者都实现了clause.Interface接口。

db.WithContext(ctx).
    Model(&Course{}).
    Order("course_id DESC").
    Limit(0).
    Offset(100)
// Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
   tx = db.getInstance()
   tx.Statement.AddClause(clause.Limit{Limit: limit})
   return
}

// Offset specify the number of records to skip before starting to return the records
func (db *DB) Offset(offset int) (tx *DB) {
   tx = db.getInstance()
   tx.Statement.AddClause(clause.Limit{Offset: offset})
   return
}


// Order specify order when retrieve records from database
//     db.Order("name DESC")
//     db.Order(clause.OrderByColumn{Column: clause.Column{Name: "name"}, Desc: true})
func (db *DB) Order(value interface{}) (tx *DB) {
   tx = db.getInstance()

   switch v := value.(type) {
   case clause.OrderByColumn:
      tx.Statement.AddClause(clause.OrderBy{
         Columns: []clause.OrderByColumn{v},
      })
   case string:
      if v != "" {
         tx.Statement.AddClause(clause.OrderBy{
            Columns: []clause.OrderByColumn{{
               Column: clause.Column{Name: v, Raw: true},
            }},
         })
      }
   }
   return
}

Clause 的接口定义:

// Interface clause interface
type Interface interface {
   Name() string
   Build(Builder)
   MergeClause(*Clause)
}

Limit Clause 的定义:


// Limit limit clause
type Limit struct {
   Limit  int
   Offset int
}

// Build build where clause
func (limit Limit) Build(builder Builder) {
   if limit.Limit > 0 {
      builder.WriteString("LIMIT ")
      builder.WriteString(strconv.Itoa(limit.Limit))
   }
   if limit.Offset > 0 {
      if limit.Limit > 0 {
         builder.WriteString(" ")
      }
      builder.WriteString("OFFSET ")
      builder.WriteString(strconv.Itoa(limit.Offset))
   }
}

// Name where clause name
func (limit Limit) Name() string {......}
// MergeClause merge limit by clause
func (limit Limit) MergeClause(clause *Clause) {......}

OrderBy Clause 的定义:

type OrderByColumn struct {
   Column  Column
   Desc    bool
   Reorder bool
}

type OrderBy struct {
   Columns    []OrderByColumn
   Expression Expression
}

// Build build where clause
func (orderBy OrderBy) Build(builder Builder) {
   if orderBy.Expression != nil {
      orderBy.Expression.Build(builder)
   } else {
      for idx, column := range orderBy.Columns {
         if idx > 0 {
            builder.WriteByte(',')
         }

         builder.WriteQuoted(column.Column)
         if column.Desc {
            builder.WriteString(" DESC")
         }
      }
   }
}

// Name where clause name
func (limit Limit) Name() string {......}
// MergeClause merge order by clause
func (limit Limit) MergeClause(clause *Clause) {......}

下面的截图中列举了实现clause.Interface接口的所有类,以后 SQL 支持新子句时,创建一个类实现clause.Interface接口,并在函数调用的地方实例化该类,其余执行的代码皆可不变,符合 OOP 中的开闭原则和依赖倒置原则。

OOP 思想在 TCC/APIX/GORM 源码中的应用

Lazy 加载,线程安全—单例模式

解决场景:变量只想初始化一次。

APIX 在埋点中间件中通过单例模式实现了对变量延迟且线程安全地赋值。

Metrics()用来生成 Metric 埋点中间件,在加载的过程,由于 APIX 的路由表还未注册完毕,所以需要把两个变量 metricMap 和 pathMap 的初始化放在中间件的执行过程中,但服务器启动后,这两个变量的值是固定的,没必要反复初始化,其次大量请求过来时,中间件的逻辑会并发执行,存在线程不安全的问题。

故在实现的过程中用到了sync.Once对象,只要声明类型的 once 变量,就可以直接使用它的 Do 方法,Do 方法的参数是一个无参数,无返回的函数。

func Metrics() HandlerFunc {
   ......
   metricMap := make(map[string]m3.Metric) // key: handler name
   pathMap := make(map[string][]string)    // key: handler name, value: paths
   once := sync.Once{}                     // protect maps init

   return func(c *Context) {
      // why init in handler chain not earlier: routes haven't been registered into the engine when the middleware func created.
      once.Do(func() {
         for _, r := range c.engine.Routes() {
            metricMap[r.Handler] = cli.NewMetric(r.Handler+".calledby", tagMethod, tagURI, tagErrCode, tagFromCluster, tagToCluster, tagFrom, tagEnv)
            pathMap[r.Handler] = append(pathMap[r.Handler], r.Path)
         }
      })

      c.Next()
      ......
   }
}

Sync.Once

sync.Once的源码很短,它通过对一个标识值,原子性的修改和加载,来减少锁竞争的。

type Once struct {
        done uint32
        m    Mutex
}

func (o *Once) Do(f func()) {
    // 加载标识值,判断是否已被执行过
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) { // 还没执行过函数
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 { // double check 是否已被执行过函数
        defer atomic.StoreUint32(&o.done, 1// 修改标识值
        f() // 执行函数
    }
}

它有两个特性,一是不管调用 Do 方法多少次,里面的函数只会执行一次;二是如果开始有两个并发调用,可以保证第二个调用不会立即返回,会在获取锁的时候阻塞,等第一个调用执行完毕之后,第二个调用进行二次校验之后就直接返回了。

Sync.Once 有个问题,Do 的过程并不关注 f 函数执行的结果是成功还是失败,当 f()执行失败时,由于本身的机制,没有机会再次初始化了。如果你需要二次初始化,可以看看下面传送门中关于“sync.Once 重试”的文章。

传送门

  • 百度分布式配置中心 BRCC:https://cloud.tencent.com/developer/article/1798554
  • Gin github 地址:https://github.com/gin-gonic/gin
  • GORM 官网:https://gorm.io/zh_CN/docs/
  • Gin 中间件的编写和使用:https://www.moemona.com/2020/10/804/
  • Go 语言动手写 Web 框架:https://geektutu.com/post/gee-day5.html
  • sync.Once 重试:https://studygolang.com/articles/31348

参考和引用文献

  • https://blog.51cto.com/weijie/82767
  • https://www.jianshu.com/p/150523db21a9
  • https://geektutu.com/post/gee-day5.html
  • https://www.musicpoet.top/20200409/7-major-design-principles-of-oop.html
  • https://www.cnblogs.com/ChinaHook/p/7471470.html
  • https://blog.csdn.net/bestone0213/article/details/47424255

加入我们

大力智能是大力教育旗下的智能学习平台,针对书桌场景,革新产品形态和技术,践行科学的教育理念,推动家庭学习的数字化和智能化。通过持续创新,服务教育生态参与者,实现“创新教育,成就每一个人”的目标。

加入我们研发团队,在这里你有机会接触到人工智能、大数据、高可用与高性能服务、软硬件一体化、跨端研发等丰富的技术栈。同时也有机会使用这些技术加速字节大力教育的创新与探索。让技术激发人的潜能,也释放你的潜能。

在招岗位:服务端开发工程师、前端开发工程师、客户端开发工程师(Android/iOS)

岗位地点:上海

岗位投递链接:

OOP 思想在 TCC/APIX/GORM 源码中的应用

欢迎优秀的你,一起来做优秀的事~

原文始发于微信公众号(字节跳动技术团队):OOP 思想在 TCC/APIX/GORM 源码中的应用

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月16日09:47:44
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   OOP 思想在 TCC/APIX/GORM 源码中的应用http://cn-sec.com/archives/998342.html

发表评论

匿名网友 填写信息