缘起

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

模板方法模式

  1. 模板方法模式(Template Method Pattern)又叫作模板模式,指定义一个操作中的算法的框架,
  2. 而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤,
  3. 属于行为型设计模式。
  4. 模板方法模式主要包含2个角色。
  5. 1)抽象模板(AbstractClass):抽象模板类,定义了一套算法框架/流程。
  6. 2)具体实现(ConcreteClass):具体实现类,对算法框架/流程的某些步骤进行了实现。
  7. (摘自 谭勇德 <<设计模式就该这样学>>)

_

场景

  • 某业务系统, 数据访问层需要编写大量DAO代码
  • 数据查询基本上遵循如下流程: 1-执行SQL, 2-遍历结果集, 3-将数据行映射为强类型对象
  • 现根据模板方法模式, 将步骤1和2的公用代码抽取到基类中, 子类仅需要实现结果映射方法即可

设计

  • IDao: 定义DAO对象的查询接口
  • tBaseDAO: 实现IDao接口, 根据模板方法模式, 定义了查询数据的完整流程, 并接受数据行映射函数注入
  • UserInfo: 用户信息实体
  • IUserDAO: 定义强类型的查询接口 - 用户信息查询
  • UserDAO: 继承tBaseDAO并注入数据行映射函数, 同时实现IUserDAO接口, 提供用户信息查询功能

单元测试

template_pattern_test.go, 引入sqlmock虚拟数据库连接

  1. package behavioral_patterns
  2. import (
  3. "github.com/DATA-DOG/go-sqlmock"
  4. "learning/gooop/behavioral_patterns/template"
  5. "strings"
  6. "testing"
  7. )
  8. func Test_TemplatePattern(t *testing.T) {
  9. // setup sqlmock ///////////////////////////////////////////
  10. db, mock, err := sqlmock.New()
  11. if err != nil {
  12. t.Fatalf("mock error: '%s'", err)
  13. }
  14. defer func() {
  15. _ = db.Close()
  16. }()
  17. // end setup sqlmock ///////////////////////////////////////////
  18. // test UserDAO.GetUserByID ////////////////////////////////////
  19. mock.ExpectQuery("select").WillReturnRows(
  20. mock.
  21. NewRows(strings.Split("id,name,pwd,org_id,role_id", ",")).
  22. AddRow(1, "John", "abcdefg", 11, "guest"))
  23. ud := template.NewUserDAO()
  24. e, u := ud.GetUserByID(db, 1)
  25. if e != nil {
  26. t.Error(e)
  27. } else {
  28. t.Logf("user = %v", u)
  29. }
  30. // end test UserDAO.GetUserByID ////////////////////////////////
  31. // test UserDAO.GetUsersByOrgID ///////////////////////////////
  32. mock.ExpectQuery("select").WillReturnRows(
  33. mock.
  34. NewRows(strings.Split("id,name,pwd,org_id,role_id", ",")).
  35. AddRow(1, "John", "abcdefg", 11, "guest").
  36. AddRow(2, "Mike", "aaaaaa", 11, "admin"))
  37. e,ul := ud.GetUsersByOrgID(db, 11)
  38. if e != nil {
  39. t.Error(e)
  40. } else {
  41. for i,it := range ul {
  42. t.Logf("users[%d] = %v", i, it)
  43. }
  44. }
  45. // end test UserDAO.GetUsersByOrgID ///////////////////////////
  46. }

测试输出

  1. $ go test -v template_pattern_test.go
  2. === RUN Test_TemplatePattern
  3. template_pattern_test.go:37: user = &{1 John abcdefg 11 guest}
  4. template_pattern_test.go:53: users[0] = &{1 John abcdefg 11 guest}
  5. template_pattern_test.go:53: users[1] = &{2 Mike aaaaaa 11 admin}
  6. --- PASS: Test_TemplatePattern (0.00s)
  7. PASS
  8. ok command-line-arguments 0.002s

IDao.go

定义DAO对象的查询接口

  1. package template
  2. import "database/sql"
  3. type IDao interface {
  4. QueryOne(db *sql.DB, sql string, args... interface{}) error
  5. QueryMulti(db *sql.DB, sql string, args... interface{}) error
  6. }

tBaseDAO.go

实现IDao接口, 根据模板方法模式, 定义了查询数据的完整流程, 并接受数据行映射函数注入

  1. package template
  2. import (
  3. "database/sql"
  4. "errors"
  5. )
  6. type FNBeforeQuery func() error
  7. type FNScanRow func(rows *sql.Rows) error
  8. type tBaseDAO struct {
  9. fnBeforeQuery FNBeforeQuery
  10. fnScanRow FNScanRow
  11. }
  12. func newBaseDAO(fq FNBeforeQuery, fs FNScanRow) *tBaseDAO {
  13. return &tBaseDAO{
  14. fnBeforeQuery: fq,
  15. fnScanRow: fs,
  16. }
  17. }
  18. func (me *tBaseDAO) QueryOne(db *sql.DB, sql string, args... interface{}) error {
  19. if me.fnScanRow == nil {
  20. return errors.New("tBaseDAO.fnScanRow is nil")
  21. }
  22. if me.fnBeforeQuery != nil {
  23. e := me.fnBeforeQuery()
  24. if e != nil {
  25. return e
  26. }
  27. }
  28. rows, e := db.Query(sql, args...)
  29. defer func() {
  30. if rows != nil {
  31. _ = rows.Close()
  32. }
  33. }()
  34. if e != nil {
  35. return e
  36. }
  37. if rows.Next() {
  38. return me.fnScanRow(rows)
  39. } else {
  40. return errors.New("no rows found")
  41. }
  42. }
  43. func (me *tBaseDAO) QueryMulti(db *sql.DB, sql string, args... interface{}) error {
  44. if me.fnScanRow == nil {
  45. return errors.New("tBaseDAO.fnScanRow is nil")
  46. }
  47. if me.fnBeforeQuery != nil {
  48. e := me.fnBeforeQuery()
  49. if e != nil {
  50. return e
  51. }
  52. }
  53. rows, e := db.Query(sql, args...)
  54. defer func() {
  55. if rows != nil {
  56. _ = rows.Close()
  57. }
  58. }()
  59. if e != nil {
  60. return e
  61. }
  62. for rows.Next() {
  63. e = me.fnScanRow(rows)
  64. if e != nil {
  65. return e
  66. }
  67. }
  68. return nil
  69. }

UserInfo.go

用户信息实体

  1. package template
  2. type UserInfo struct {
  3. ID int
  4. Name string
  5. Pwd string
  6. OrgID int
  7. RoleID string
  8. }
  9. func NewUserInfo() *UserInfo {
  10. return &UserInfo{
  11. 0, "", "", 0, "",
  12. }
  13. }

IUserDAO.go

定义强类型的查询接口 - 用户信息查询

  1. package template
  2. import "database/sql"
  3. type IUserDAO interface {
  4. GetUserByID(db *sql.DB, id int) (error, *UserInfo)
  5. GetUsersByOrgID(db *sql.DB, orgID int) (error, []*UserInfo)
  6. }

UserDAO.go

继承tBaseDAO并注入数据行映射函数, 同时实现IUserDAO接口, 提供用户信息查询功能

  1. package template
  2. import (
  3. "database/sql"
  4. )
  5. type UserDAO struct {
  6. tBaseDAO
  7. mItems []*UserInfo
  8. }
  9. func NewUserDAO() IUserDAO {
  10. it := &UserDAO{ *newBaseDAO(nil, nil), nil }
  11. it.fnBeforeQuery = it.BeforeQuery
  12. it.fnScanRow = it.ScanRow
  13. return it
  14. }
  15. func (me *UserDAO) BeforeQuery() error {
  16. me.mItems = make([]*UserInfo, 0)
  17. return nil
  18. }
  19. func (me *UserDAO) ScanRow(rows *sql.Rows) error {
  20. user := NewUserInfo()
  21. e := rows.Scan(&user.ID, &user.Name, &user.Pwd, &user.OrgID, &user.RoleID)
  22. if e != nil {
  23. return e
  24. }
  25. me.mItems = append(me.mItems, user)
  26. return nil
  27. }
  28. func (me *UserDAO) GetUserByID(db *sql.DB, id int) (error, *UserInfo) {
  29. e := me.QueryOne(db, "select id,name,pwd,org_id,role_id from user_info where id=?", id)
  30. if e != nil {
  31. return e, nil
  32. }
  33. return nil, me.mItems[0]
  34. }
  35. func (me *UserDAO) GetUsersByOrgID(db *sql.DB, orgID int) (error, []*UserInfo) {
  36. e := me.QueryMulti(db, "select id,name,pwd,org_id,role_id from user_info where org_id=?", orgID)
  37. if e != nil {
  38. return e, nil
  39. }
  40. return nil, me.mItems
  41. }

模板方法小结

  1. 模板方法模式的优点
  2. 1)利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
  3. 2)将不同的算法逻辑分离到不同的子类中,通过对子类的扩展增加新的行为,提高代码的可扩展性。
  4. 3)把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则。
  5. 模板方法模式的缺点
  6. 1)每一个抽象类都需要一个子类来实现,这样导致类数量增加。
  7. 2)类数量的增加,间接地增加了系统实现的复杂度。
  8. 3)由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。
  9. (摘自 谭勇德 <<设计模式就该这样学>>)

(end)