动手点关注 干货不迷路 👆
名词解释
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
中的属性,其中WithCluster
、WithAddr
、WithAuth
是快捷生成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
的引用,对外提供了GetInt
和GetBool
两个方法,包掉了对原始 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
publi