缘起

最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
本系列笔记拟采用golang练习之

合成复用原则

  • 合成复用原则(Composite/Aggregate Reuse Principle, CARP)指尽量使用对象组合(has-a)或对象聚合(contanis-a)的方式实现代码复用,而不是用继承关系达到代码复用的目的。合成复用原则可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较小。
  • 继承,又被称为白箱复用,相当于把所有实现细节暴露给子类。组合/聚合又被称为黑箱复用,对类以外的对象是无法获取实现细节的。

_

场景

  • 某订单业务系统, 需要连接数据库对产品信息进行CRUD操作
  • 不好的设计:
    • 定义DBConnection类, 实现对数据库的连接和SQL执行
    • 定义ProductDAO类, 继承DBConnection类, 并封装对产品资料的增删改查
    • 问题: ProductDAO对DBConnection的继承仅仅是为了代码复用, 不符合合成复用原则
  • 更好的设计:
    • 定义IDBConnection接口
    • 定义MysqlConnection类, 实现对mysql数据库的连接和SQL执行
    • 定义ProductDAO类, 通过Setter方法注入IDBConnection实例

_

Product.go

产品实体类

  1. package composite_reuse
  2. type Product struct {
  3. ID int
  4. Name string
  5. Price float64
  6. }
  7. func NewProduct(id int, name string, price float64) *Product {
  8. return &Product{
  9. id, name, price,
  10. }
  11. }

_

BadDBConnection.go

BadDBConnection用于连接数据库并执行SQL语句

  1. package composite_reuse
  2. import "fmt"
  3. type BadDBConnection struct {
  4. sURL string
  5. sUID string
  6. sPWD string
  7. }
  8. func NewBadDBConnection(url string, uid string, pwd string) *BadDBConnection {
  9. return &BadDBConnection{
  10. url, uid, pwd,
  11. }
  12. }
  13. func (me *BadDBConnection) Execute(sql string, args... interface{}) (error, int) {
  14. fmt.Printf("BadDBConnection.Execute, sql=%v, args=%v\n", sql, args)
  15. return nil,0
  16. }

BadProductDAO.go

不好的设计. 直接从BadDBConnection继承, 以获取访问数据库的能力. 继承仅仅是为了代码复用, 而不是概念复用, 因此违反了合成复用原则.

  1. package composite_reuse
  2. type BadProductDAO struct {
  3. BadDBConnection
  4. }
  5. func NewBadProductDAO(url string, uid string, pwd string) *BadProductDAO {
  6. return &BadProductDAO{
  7. *NewBadDBConnection(url, uid, pwd),
  8. }
  9. }
  10. func (me *BadProductDAO) Insert(it *Product) (error, int) {
  11. return me.Execute("insert into product(id,name,price) values(?, ?, ?)", it.ID, it.Name, it.Price)
  12. }
  13. func (me *BadProductDAO) Update(it *Product) (error, int) {
  14. return me.Execute("update product set name=? price=? where id=?", it.Name, it.Price, it.ID)
  15. }
  16. func (me *BadProductDAO) Delete(id int) (error, int) {
  17. return me.Execute("delete from product where id=?", id)
  18. }

IGoodDBConnection.go

更好的设计, 将数据库连接抽象为接口, 以支持多种数据库

  1. package composite_reuse
  2. type IGoodDBConnection interface {
  3. Execute(sql string, args... interface{}) (error, int)
  4. }

GoodMysqlConnection.go

更好的设计, GoodMysqlConnection封装MYSQL数据库方言, 实现IGoodDBConnection接口

  1. package composite_reuse
  2. import "fmt"
  3. type GoodMysqlConnection struct {
  4. sURL string
  5. sUID string
  6. sPWD string
  7. }
  8. func NewGoodMysqlConnection(url string, uid string, pwd string) IGoodDBConnection {
  9. return &GoodMysqlConnection{
  10. url, uid, pwd,
  11. }
  12. }
  13. func (me *GoodMysqlConnection) Execute(sql string, args... interface{}) (error, int) {
  14. fmt.Printf("GoodMysqlConnection.Execute, sql=%v, args=%v\n", sql, args)
  15. return nil, 0
  16. }

GoodProductDAO.go

更好的设计, 通过Setter方法注入数据库方言实例(遵循了合成复用原则), 实现产品的CRUD

  1. package composite_reuse
  2. type GoodProductDAO struct {
  3. mDBConnection IGoodDBConnection
  4. }
  5. func NewGoodProductDAO() *GoodProductDAO {
  6. return &GoodProductDAO{}
  7. }
  8. func (me *GoodProductDAO) SetDBConnection(it IGoodDBConnection) {
  9. me.mDBConnection = it
  10. }
  11. func (me *GoodProductDAO) Insert(it *Product) (error, int) {
  12. return me.mDBConnection.Execute("insert into product(id,name,price) values(?, ?, ?)", it.ID, it.Name, it.Price)
  13. }
  14. func (me *GoodProductDAO) Update(it *Product) (error, int) {
  15. return me.mDBConnection.Execute("update product set name=? price=? where id=?", it.Name, it.Price, it.ID)
  16. }
  17. func (me *GoodProductDAO) Delete(id int) (error, int) {
  18. return me.mDBConnection.Execute("delete from product where id=?", id)
  19. }

composite_reuse_test.go

单元测试

  1. package main
  2. import "testing"
  3. import (carp "learning/gooop/principles/composite_reuse")
  4. func Test_CARP(t *testing.T) {
  5. p := carp.NewProduct(1, "手机", 1000)
  6. fnCallAndLog := func(fn func() (error, int)) {
  7. e,rows := fn()
  8. if e != nil {
  9. t.Errorf("error = %s", e.Error())
  10. } else {
  11. t.Logf("affected rows = %v", rows)
  12. }
  13. }
  14. // begin testing bad //////////////////////////////////////////////////////////////
  15. bd := carp.NewBadProductDAO("database connection url", "sa", "123")
  16. fnCallAndLog(func() (error, int) {
  17. return bd.Insert(p)
  18. })
  19. fnCallAndLog(func() (error, int) {
  20. return bd.Update(p)
  21. })
  22. fnCallAndLog(func() (error, int) {
  23. return bd.Delete(p.ID)
  24. })
  25. // end testing bad //////////////////////////////////////////////////////////////
  26. // begin testing good //////////////////////////////////////////////////////////////
  27. con := carp.NewGoodMysqlConnection("database connection url", "sa", "123")
  28. gd := carp.NewGoodProductDAO()
  29. gd.SetDBConnection(con)
  30. fnCallAndLog(func() (error, int) {
  31. return gd.Insert(p)
  32. })
  33. fnCallAndLog(func() (error, int) {
  34. return gd.Update(p)
  35. })
  36. fnCallAndLog(func() (error, int) {
  37. return gd.Delete(p.ID)
  38. })
  39. // end testing good //////////////////////////////////////////////////////////////
  40. }

测试输出

  1. $ go test -v composite_reuse_test.go
  2. === RUN Test_CARP
  3. BadDBConnection.Execute, sql=insert into product(id,name,price) values(?, ?, ?), args=[1 手机 1000]
  4. composite_reuse_test.go:13: affected rows = 0
  5. BadDBConnection.Execute, sql=update product set name=? price=? where id=?, args=[手机 1000 1]
  6. composite_reuse_test.go:13: affected rows = 0
  7. BadDBConnection.Execute, sql=delete from product where id=?, args=[1]
  8. composite_reuse_test.go:13: affected rows = 0
  9. GoodMysqlConnection.Execute, sql=insert into product(id,name,price) values(?, ?, ?), args=[1 手机 1000]
  10. composite_reuse_test.go:13: affected rows = 0
  11. GoodMysqlConnection.Execute, sql=update product set name=? price=? where id=?, args=[手机 1000 1]
  12. composite_reuse_test.go:13: affected rows = 0
  13. GoodMysqlConnection.Execute, sql=delete from product where id=?, args=[1]
  14. composite_reuse_test.go:13: affected rows = 0
  15. --- PASS: Test_CARP (0.00s)
  16. PASS
  17. ok command-line-arguments 0.004s