手撸golang 行为型设计模式 模板方法模式
缘起
最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
本系列笔记拟采用golang练习之
模板方法模式
模板方法模式(Template Method Pattern)又叫作模板模式,指定义一个操作中的算法的框架,
而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤,
属于行为型设计模式。
模板方法模式主要包含2个角色。
(1)抽象模板(AbstractClass):抽象模板类,定义了一套算法框架/流程。
(2)具体实现(ConcreteClass):具体实现类,对算法框架/流程的某些步骤进行了实现。
(摘自 谭勇德 <<设计模式就该这样学>>)
_
场景
- 某业务系统, 数据访问层需要编写大量DAO代码
- 数据查询基本上遵循如下流程: 1-执行SQL, 2-遍历结果集, 3-将数据行映射为强类型对象
- 现根据模板方法模式, 将步骤1和2的公用代码抽取到基类中, 子类仅需要实现结果映射方法即可
设计
- IDao: 定义DAO对象的查询接口
- tBaseDAO: 实现IDao接口, 根据模板方法模式, 定义了查询数据的完整流程, 并接受数据行映射函数注入
- UserInfo: 用户信息实体
- IUserDAO: 定义强类型的查询接口 - 用户信息查询
- UserDAO: 继承tBaseDAO并注入数据行映射函数, 同时实现IUserDAO接口, 提供用户信息查询功能
单元测试
template_pattern_test.go, 引入sqlmock虚拟数据库连接
package behavioral_patterns
import (
"github.com/DATA-DOG/go-sqlmock"
"learning/gooop/behavioral_patterns/template"
"strings"
"testing"
)
func Test_TemplatePattern(t *testing.T) {
// setup sqlmock ///////////////////////////////////////////
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("mock error: '%s'", err)
}
defer func() {
_ = db.Close()
}()
// end setup sqlmock ///////////////////////////////////////////
// test UserDAO.GetUserByID ////////////////////////////////////
mock.ExpectQuery("select").WillReturnRows(
mock.
NewRows(strings.Split("id,name,pwd,org_id,role_id", ",")).
AddRow(1, "John", "abcdefg", 11, "guest"))
ud := template.NewUserDAO()
e, u := ud.GetUserByID(db, 1)
if e != nil {
t.Error(e)
} else {
t.Logf("user = %v", u)
}
// end test UserDAO.GetUserByID ////////////////////////////////
// test UserDAO.GetUsersByOrgID ///////////////////////////////
mock.ExpectQuery("select").WillReturnRows(
mock.
NewRows(strings.Split("id,name,pwd,org_id,role_id", ",")).
AddRow(1, "John", "abcdefg", 11, "guest").
AddRow(2, "Mike", "aaaaaa", 11, "admin"))
e,ul := ud.GetUsersByOrgID(db, 11)
if e != nil {
t.Error(e)
} else {
for i,it := range ul {
t.Logf("users[%d] = %v", i, it)
}
}
// end test UserDAO.GetUsersByOrgID ///////////////////////////
}
测试输出
$ go test -v template_pattern_test.go
=== RUN Test_TemplatePattern
template_pattern_test.go:37: user = &{1 John abcdefg 11 guest}
template_pattern_test.go:53: users[0] = &{1 John abcdefg 11 guest}
template_pattern_test.go:53: users[1] = &{2 Mike aaaaaa 11 admin}
--- PASS: Test_TemplatePattern (0.00s)
PASS
ok command-line-arguments 0.002s
IDao.go
定义DAO对象的查询接口
package template
import "database/sql"
type IDao interface {
QueryOne(db *sql.DB, sql string, args... interface{}) error
QueryMulti(db *sql.DB, sql string, args... interface{}) error
}
tBaseDAO.go
实现IDao接口, 根据模板方法模式, 定义了查询数据的完整流程, 并接受数据行映射函数注入
package template
import (
"database/sql"
"errors"
)
type FNBeforeQuery func() error
type FNScanRow func(rows *sql.Rows) error
type tBaseDAO struct {
fnBeforeQuery FNBeforeQuery
fnScanRow FNScanRow
}
func newBaseDAO(fq FNBeforeQuery, fs FNScanRow) *tBaseDAO {
return &tBaseDAO{
fnBeforeQuery: fq,
fnScanRow: fs,
}
}
func (me *tBaseDAO) QueryOne(db *sql.DB, sql string, args... interface{}) error {
if me.fnScanRow == nil {
return errors.New("tBaseDAO.fnScanRow is nil")
}
if me.fnBeforeQuery != nil {
e := me.fnBeforeQuery()
if e != nil {
return e
}
}
rows, e := db.Query(sql, args...)
defer func() {
if rows != nil {
_ = rows.Close()
}
}()
if e != nil {
return e
}
if rows.Next() {
return me.fnScanRow(rows)
} else {
return errors.New("no rows found")
}
}
func (me *tBaseDAO) QueryMulti(db *sql.DB, sql string, args... interface{}) error {
if me.fnScanRow == nil {
return errors.New("tBaseDAO.fnScanRow is nil")
}
if me.fnBeforeQuery != nil {
e := me.fnBeforeQuery()
if e != nil {
return e
}
}
rows, e := db.Query(sql, args...)
defer func() {
if rows != nil {
_ = rows.Close()
}
}()
if e != nil {
return e
}
for rows.Next() {
e = me.fnScanRow(rows)
if e != nil {
return e
}
}
return nil
}
UserInfo.go
用户信息实体
package template
type UserInfo struct {
ID int
Name string
Pwd string
OrgID int
RoleID string
}
func NewUserInfo() *UserInfo {
return &UserInfo{
0, "", "", 0, "",
}
}
IUserDAO.go
定义强类型的查询接口 - 用户信息查询
package template
import "database/sql"
type IUserDAO interface {
GetUserByID(db *sql.DB, id int) (error, *UserInfo)
GetUsersByOrgID(db *sql.DB, orgID int) (error, []*UserInfo)
}
UserDAO.go
继承tBaseDAO并注入数据行映射函数, 同时实现IUserDAO接口, 提供用户信息查询功能
package template
import (
"database/sql"
)
type UserDAO struct {
tBaseDAO
mItems []*UserInfo
}
func NewUserDAO() IUserDAO {
it := &UserDAO{ *newBaseDAO(nil, nil), nil }
it.fnBeforeQuery = it.BeforeQuery
it.fnScanRow = it.ScanRow
return it
}
func (me *UserDAO) BeforeQuery() error {
me.mItems = make([]*UserInfo, 0)
return nil
}
func (me *UserDAO) ScanRow(rows *sql.Rows) error {
user := NewUserInfo()
e := rows.Scan(&user.ID, &user.Name, &user.Pwd, &user.OrgID, &user.RoleID)
if e != nil {
return e
}
me.mItems = append(me.mItems, user)
return nil
}
func (me *UserDAO) GetUserByID(db *sql.DB, id int) (error, *UserInfo) {
e := me.QueryOne(db, "select id,name,pwd,org_id,role_id from user_info where id=?", id)
if e != nil {
return e, nil
}
return nil, me.mItems[0]
}
func (me *UserDAO) GetUsersByOrgID(db *sql.DB, orgID int) (error, []*UserInfo) {
e := me.QueryMulti(db, "select id,name,pwd,org_id,role_id from user_info where org_id=?", orgID)
if e != nil {
return e, nil
}
return nil, me.mItems
}
模板方法小结
模板方法模式的优点
(1)利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
(2)将不同的算法逻辑分离到不同的子类中,通过对子类的扩展增加新的行为,提高代码的可扩展性。
(3)把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则。
模板方法模式的缺点
(1)每一个抽象类都需要一个子类来实现,这样导致类数量增加。
(2)类数量的增加,间接地增加了系统实现的复杂度。
(3)由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。
(摘自 谭勇德 <<设计模式就该这样学>>)
(end)