手撸golang 架构设计原则 里氏替换原则
缘起
最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
该书以java语言演绎了常见设计模式
本系列笔记拟采用golang练习之
里氏替换原则
里氏替换原则(Liskov Substitution Principle, LSP):
如果对每一个类型为T1的对象O1
都有类型为T2的对象O2
使得以T1定义的所有程序P
在所有对象O1都替换成O2时
程序P的行为没有发生变化
那么类型T2是类型T1的子类型
_
可以理解为:
所有引用父类的地方
必须能透明地使用其子类对象
子类对象能够替换父类对象
而保持程序功能不变
_
里氏替换原则的优点:
(1)约束继承泛滥,是开闭原则的一种体现
(2)加强程序的健壮性,同时变更时可以做到非常好的兼容性
_
场景
- 某线上动物园系统, 定义了鸟类接口IBird和NormalBird类
- IBird接口定义了鸣叫 - Tweet(), 和飞翔 - Fly()方法
- 现需要增加一种"鸟类" - 鸵鸟: 鸵鸟只会跑 - Run(), 不会飞 - Fly()
不好的设计:
- 新增鸵鸟类 - OstrichBird, 从NormalBird继承
- 覆盖Fly方法, 并抛出错误
- 添加Run方法
- 调用方需要修改: 判断是否OstrichBird, 是则需要特别对待
- 存在问题: OstrichBird跟NormalBird已经有较大差异, 强行继承造成很多异味
更好的设计:
- IBird接口保留鸣叫 - Tweet()方法
- NormalBird实现IBird接口, 移除Fly方法
- 新增IFlyableBird, 继承IBird接口, 并添加Fly()方法
- 新增FlyableBird, 继承NormalBird, 并实现IFlyableBird接口
- 新增IRunnableBird, 继承IBird接口, 并添加Run()方法
- 新增OstrichBird, 继承NormalBird, 并实现IRunnableBird
- 调用方判断是IFlyableBird, 还是IRunnableBird
IBadBird.go
不好的设计, 该接口未考虑某些鸟类是不能Fly的
package liskov_substitution
type IBadBird interface {
ID() int
Name() string
Tweet() error
Fly() error
}
BadNormalBird.go
BadNormalBird实现了IBadBird接口
package liskov_substitution
import "fmt"
type BadNormalBird struct {
iID int
sName string
}
func NewBadNormalBird(id int, name string) IBadBird {
return &BadNormalBird{
id,
name,
}
}
func (me *BadNormalBird) ID() int {
return me.iID
}
func (me *BadNormalBird) Name() string {
return me.sName
}
func (me *BadNormalBird) Tweet() error {
fmt.Printf("BadNormalBird.Tweet, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
func (me *BadNormalBird) Fly() error {
fmt.Printf("BadNormalBird.Fly, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
BadOstrichBird.go
不好的设计.
BadOstrichBird通过继承BadNormalBird实现了IBadBird接口. 由于不支持Fly, 因此Fly方法抛出了错误. 额外添加了IBadBird未考虑到的Run方法. 该方法的调用要求调用方必须判断具体类型, 导致严重耦合.
package liskov_substitution
import (
"errors"
"fmt"
)
type BadOstrichBird struct {
BadNormalBird
}
func NewBadOstrichBird(id int, name string) IBadBird {
return &BadOstrichBird{
*(NewBadNormalBird(id, name).(*BadNormalBird)),
}
}
func (me *BadOstrichBird) Fly() error {
return errors.New(fmt.Sprintf("BadOstrichBird.Fly, cannot fly, id=%v, name=%v\n", me.ID(), me.Name()))
}
func (me *BadOstrichBird) Run() error {
fmt.Printf("BadOstrichBird.Run, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
IGoodBird.go
更好的设计.
IGoodBird仅定义了最基本的方法集, 通过子接口IFlyableBird添加Fly方法, 通过子接口IRunnableBird添加Run方法
package liskov_substitution
type IGoodBird interface {
ID() int
Name() string
Tweet() error
}
type IFlyableBird interface {
IGoodBird
Fly() error
}
type IRunnableBird interface {
IGoodBird
Run() error
}
GoodNormalBird.go
GoodNormalBird提供对IGoodBird的基础实现
package liskov_substitution
import "fmt"
type GoodNormalBird struct {
iID int
sName string
}
func NewGoodNormalBird(id int, name string) *GoodNormalBird {
return &GoodNormalBird{
id,
name,
}
}
func (me *GoodNormalBird) ID() int {
return me.iID
}
func (me *GoodNormalBird) Name() string {
return me.sName
}
func (me *GoodNormalBird) Tweet() error {
fmt.Printf("GoodNormalBird.Tweet, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
GoodFlyableBird.go
GoodFlyableBird通过聚合GoodNormalBird实现IGoodBird接口, 通过提供Fly方法实现IFlyableBird子接口
package liskov_substitution
import "fmt"
type GoodFlyableBird struct {
GoodNormalBird
}
func NewGoodFlyableBird(id int, name string) IGoodBird {
return &GoodFlyableBird{
*NewGoodNormalBird(id, name),
}
}
func (me *GoodFlyableBird) Fly() error {
fmt.Printf("GoodFlyableBird.Fly, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
GoodOstrichBird.go
GoodOstrichBird通过聚合GoodNormalBird实现IGoodBird接口, 通过提供Run方法实现IRunnableBird子接口
package liskov_substitution
import (
"fmt"
)
type GoodOstrichBird struct {
GoodNormalBird
}
func NewGoodOstrichBird(id int, name string) IGoodBird {
return &GoodOstrichBird{
*NewGoodNormalBird(id, name),
}
}
func (me *GoodOstrichBird) Run() error {
fmt.Printf("GoodOstrichBird.Run, id=%v, name=%v\n", me.ID(), me.Name())
return nil
}
liskov_substitution_test.go
单元测试
package main
import "testing"
import (lsp "learning/gooop/principles/liskov_substitution")
func Test_LSP(t *testing.T) {
fnCallAndLog := func(fn func() error) {
e := fn()
if e != nil {
t.Logf("error = %s", e.Error())
}
}
// start testing bad /////////////////////////////////////////////////
bb := lsp.NewBadNormalBird(1, "普鸟")
fnCallAndLog(bb.Tweet)
fnCallAndLog(bb.Fly)
bo := lsp.NewBadOstrichBird(2, "鸵鸟")
fnCallAndLog(bo.Tweet)
fnCallAndLog(bo.Fly)
if it, ok := bo.(*lsp.BadOstrichBird);ok {
fnCallAndLog(it.Run)
}
// end testing bad /////////////////////////////////////////////////
// start testing good /////////////////////////////////////////////////
fnTestGoodBird := func(gb lsp.IGoodBird) {
fnCallAndLog(gb.Tweet)
if it, ok := gb.(lsp.IFlyableBird);ok {
fnCallAndLog(it.Fly)
}
if it, ok := gb.(lsp.IRunnableBird);ok {
fnCallAndLog(it.Run)
}
}
fnTestGoodBird(lsp.NewGoodFlyableBird(11, "飞鸟"))
fnTestGoodBird(lsp.NewGoodOstrichBird(12, "鸵鸟"))
// end testing good /////////////////////////////////////////////////
}
测试输出
$ go test -v liskov_substitution_test.go
=== RUN Test_LSP
BadNormalBird.Tweet, id=1, name=普鸟
BadNormalBird.Fly, id=1, name=普鸟
BadNormalBird.Tweet, id=2, name=鸵鸟
liskov_substitution_test.go:10: error = BadOstrichBird.Fly, cannot fly, id=2, name=鸵鸟
BadOstrichBird.Run, id=2, name=鸵鸟
GoodNormalBird.Tweet, id=11, name=飞鸟
GoodFlyableBird.Fly, id=11, name=飞鸟
GoodNormalBird.Tweet, id=12, name=鸵鸟
GoodOstrichBird.Run, id=12, name=鸵鸟
--- PASS: Test_LSP (0.00s)
PASS
ok command-line-arguments 0.002s
有疑问加站长微信联系(非本文作者)