写golang两个月的一些心得
自从开始负责公司后台的SQL引擎优化之后,时常需要为公司其他同事提供一些SQL解析接口。
由于之前部门大部分的接口开发都使用
cpp/golang,而我这边的接口服务主要性能瓶颈都在后台的SQL解析引擎,接口所耗费的资源在整体资源中占比非常小。cpp性能虽然更快,但是在这里并不是我需要考虑的主要因素,很自然的,我选择了golang作为我的后台接口开发语言。
这里就对我这两个月的一些开发心得做一些总结。
referecnce
golang实现SOLID原则
SOLID
并不算是一个面向golang的原则,而是面向所有面向对象语言 都可以参考的原则。
虽然从严格的角度来讲,golang没有显示继承 ,但是golang的接口可以隐式继承,也可以通过struct持有另外一个匿名struct对象来实现隐式继承。
所以SOLID原则对golang来讲仍然是适用的。
SOLID分别对应于:
The Single-responsibility principle
The Open–closed principle
The Liskov substitution principle
The Interface segregation principle
The Dependency inversion principle
单一职责原则(The
Single-responsibility principle)
A module should be responsible to one, and only
one,actor
. The term actor
refers to a group
(consisting of one or more stakeholders or users) that requires a change
in the module.
对于单一职责原则 ,有一个比较难理解的术语是
actor
。按照网络上大部分博主的说法,我们可以简单的理解为会引起变化的原因 。
我们可以用下面的代码来描述,假设我们存在这样一个struct
1 2 3 4 5 6 7 type Trade struct { TradeID int Symbol string Quantity float64 Price float64 }
我们可以声明两个结构体 TradeRepository
和
TradeValidator
分别用于存储Trade,验证Trade是否合法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type TradeRepository struct {}func (tr *TradeRepository) Save(trade *Trade) error { } type TradeValidator struct {}func (tv *TradeValidator) Validate(trade *Trade) error { }
从代码功能的角度讲,我们也可以这样定义,把Save和Validate都放在一个类中。这样带来的最大问题是,当存储逻辑或者验证逻辑要修改时,都会需要修改
TradeUtil,这违背了单一职责原则。
1 2 3 4 5 6 7 8 9 10 11 type TradeUtil struct {}func (u *TradeUtil) Save(trade *Trade) error { } func (u *TradeUtil) Validate(trade *Trade) error { }
但是,我们可以进一步的思考,如果按照这样的单一原则,那是不是我每个struct都只能绑定一个方法呢?比如我现在拥有
Save
Update
Validate
三个方法,难道我需要声明三个struct并为他们绑定各自的方法吗?此时我们可以将逻辑拆分为:
第一个struct负责数据库操作
第二个struct负责Trade验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type TradeDb struct {}func (u *TradeDb) Save(trade *Trade) error { } func (u *TradeDb) Update(trade *Trade) error { } type TradeValidator struct {}func (tv *TradeValidator) Validate(trade *Trade) error { }
而具体的拆分粒度需要根据应用场景来设计,毕竟拆或者不拆都会引入一些问题:
拆分粒度过细会导致出现大量的struct,并且在这种情况下,绑定方法到struct完全丧失了意义,我们甚至可以声明一个全局的方法,而不是将方法绑定到struct。
拆分粒度过粗会导致任何外部的需求变更都会有大量的接口/方法可能受到影响。
开闭原则
software entities (classes, modules, functions, etc.) should be open
for extension, but closed for modification
开闭原则其实可以简单的描述为,在需求可能会发生变更的地方,尽可能的使用接口,继承(虽然golang只有隐式继承)来实现 。例如,我们现在有一个如下的交易流程:
验证Trade
交易Trade
存储Trade
而交易流程中,这个Trade的交易可能是买入或者卖出。如果我们不遵循开闭原则,那么我们的代码实现最开始可能是这样的:我们只需要实现买
1 2 3 4 5 6 7 8 9 10 11 12 type TradeDb struct {}func (u *TradeDb) Save(trade *Trade) error { } func DoTrade (td *Trade) { td.Validate() td.Buy() td.Save() }
随后发现需要新增一个卖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type TradeDb struct {}func (u *TradeDb) Save(trade *Trade) error { } func DoTrade (td *Trade, tp Type) { td.Validate() if tp == Buy { td.Buy() } else if tp == Sale { td.Sale() } td.Save() }
这样明显违背了开闭原则,因为我们的接口修改了,如果我们遵循开闭原则实现则可能是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type TradeProcessor interface { Process(trade *Trade) error } type TradeDb struct {}func (u *TradeDb) Save(trade *Trade) error { } type TradeBuy struct {}func (tb *TradeBuy) Process(trade *Trade) error {}type TradeSale struct {}func (tb *TradeSale) Process(trade *Trade) error {}func DoTrade (td *Trade, tp *TradeProcessor) { td.Validate() tp.Process(td) td.Save() }
在这种情况下,在流程不变的情况下,我们不需要修改 DoTrade
方法,只需要只对与 TradeProcessor
进行扩展,我们即可实现买或卖。
里式替换原则
里式替换原则表明,所有引用父类的地方必须能透明的使用其子类。也就是说,程序的正确性不应该依赖于子类的实现。
由于golang没有提供继承,只是通过接口提供了一个类似于父类-子类的继承关系。
对于里式替换原则,我们可以看看他的反例(这里是基于 Is
this a violation of the Liskov Substitution Principle?
这个例子的,就懒得翻译成 go,直接使用问题中的代码了。)
在下面的例子中,Task 可能在任意时间被 Close,而 ProjectTask
则不一样,当他的 Status == Status.Started 时会直接抛出异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Task { public Status Status { get; set; } public virtual void Close () { Status = Status.Closed; } } public class ProjectTask : Task{ public override void Close () { if (Status == Status.Started) throw new Exception ("Cannot close a started Project Task" ); base.Close(); } }
第一个优化方案是,我们新增加一个CanClose函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Task { public Status Status { get; set; } public virtual bool CanClose () { return true ; } public virtual void Close () { Status = Status.Closed; } } public class ProjectTask : Task { public override bool CanClose () { return Status != Status.Started; } public override void Close () { if (!CanClose ()) throw new Exception ("Cannot close a started Project Task" ); base.Close (); } }
第二个优化方案是,这个方案下 Close 不在是 virtual
的。这样设计相对于之前的好处是:
任何其他的类型都可以判断此任务的状态是否可以关闭;
可以提供原因。
为什么这样设计?因为当引入了一个额外的 Status
之后,我们的流程其实已经发生了改变,新的流程是:
是否可以关闭;
如果可以关闭则进入关闭流程;
而关闭的流程(在目前而言)对于所有的Task都是一样的:就是将Status设置为Closed。这样对于用户来讲,只需要实现CanClose即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Task { public Status Status { get; private set; } public virtual bool CanClose (out String reason) { reason = null; return true ; } public void Close () { String reason; if (!CanClose (out reason)) throw new Exception (reason); Status = Status.Closed; } } public class ProjectTask : Task { public override bool CanClose (out String reason) { if (Status != Status.Started) { reason = "Cannot close a started Project Task" ; return false ; } return base.CanClose (out reason); } }
接口隔离原则
Clients should not be forced to depend upon interfaces that they do
not use.
简单理解就是,应该建立单一接口,而不是臃肿庞大的接口,例如当我们进行跨国交易的时候,我们可能会希望交易包含一些汇率信息,那么我们可以这样实现:
1 2 3 4 5 6 7 type Trade interface { Process() error CalculateImpliedVolatility() error }
在这种情况下,那些不依赖于汇率的交易,也必须去实现这个计算汇率的接口 。更好的实现是,我们去定义两个不同的接口。然后再为每个不同的子类去绑定不同的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 type Trade interface { Process() error } type OptionTrade interface { CalculateImpliedVolatility() error } type FutureTrade struct { Trade } func (ft *FutureTrade) Process() error { return nil } type OptionTrade struct { Trade } func (ot *OptionTrade) Process() error { return nil } func (ot *OptionTrade) CalculateImpliedVolatility() error { return nil }
依赖倒置原则
Depend upon abstractions, [not] concretions
这个比较好理解:依赖接口,不依赖实例。假设我们已经实现了我们存储Trade的代码,这样的代码更不容易扩展,因为他依赖于
TradeMysql
或者
TradeOracle
。这是一个实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type TradeMysql struct {}func (tr *TradeMysql) Save(trade *Trade) error { } type TradeOracle struct {}func (tr *TradeOracle) Save(trade *Trade) error { } func main () { td := Trade{} tm := TradeMysql{} tm.Save(&td) to := TradeOracle{} to.Save(&td) }
我们可以使用接口来将他们解耦,我们定义了 TradeService
接口处理 Trade
。同时定义了
TradeProcessor
,依赖于TradeService
接口,而不是 TradeService 的具体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 type TradeService interface { Save(trade *Trade) error } type TradeProcessor struct { tradeService TradeService } func (tp *TradeProcessor) Process(trade *Trade) error { err := tp.tradeService.Save(trade) if err != nil { return err } return nil } type SqlServerTradeRepository struct { db *sql.DB } func (str *SqlServerTradeRepository) Save(trade *Trade) error {} type MongoDbTradeRepository struct { session *mgo.Session } func (mdtr *MongoDbTradeRepository) Save(trade *Trade) error {}