缘起

最近复习设计模式
拜读谭勇德的<<设计模式就该这样学>>
该书以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的

  1. package liskov_substitution
  2. type IBadBird interface {
  3. ID() int
  4. Name() string
  5. Tweet() error
  6. Fly() error
  7. }

BadNormalBird.go

BadNormalBird实现了IBadBird接口

  1. package liskov_substitution
  2. import "fmt"
  3. type BadNormalBird struct {
  4. iID int
  5. sName string
  6. }
  7. func NewBadNormalBird(id int, name string) IBadBird {
  8. return &BadNormalBird{
  9. id,
  10. name,
  11. }
  12. }
  13. func (me *BadNormalBird) ID() int {
  14. return me.iID
  15. }
  16. func (me *BadNormalBird) Name() string {
  17. return me.sName
  18. }
  19. func (me *BadNormalBird) Tweet() error {
  20. fmt.Printf("BadNormalBird.Tweet, id=%v, name=%v\n", me.ID(), me.Name())
  21. return nil
  22. }
  23. func (me *BadNormalBird) Fly() error {
  24. fmt.Printf("BadNormalBird.Fly, id=%v, name=%v\n", me.ID(), me.Name())
  25. return nil
  26. }

BadOstrichBird.go

不好的设计.
BadOstrichBird通过继承BadNormalBird实现了IBadBird接口. 由于不支持Fly, 因此Fly方法抛出了错误. 额外添加了IBadBird未考虑到的Run方法. 该方法的调用要求调用方必须判断具体类型, 导致严重耦合.

  1. package liskov_substitution
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. type BadOstrichBird struct {
  7. BadNormalBird
  8. }
  9. func NewBadOstrichBird(id int, name string) IBadBird {
  10. return &BadOstrichBird{
  11. *(NewBadNormalBird(id, name).(*BadNormalBird)),
  12. }
  13. }
  14. func (me *BadOstrichBird) Fly() error {
  15. return errors.New(fmt.Sprintf("BadOstrichBird.Fly, cannot fly, id=%v, name=%v\n", me.ID(), me.Name()))
  16. }
  17. func (me *BadOstrichBird) Run() error {
  18. fmt.Printf("BadOstrichBird.Run, id=%v, name=%v\n", me.ID(), me.Name())
  19. return nil
  20. }

IGoodBird.go

更好的设计.
IGoodBird仅定义了最基本的方法集, 通过子接口IFlyableBird添加Fly方法, 通过子接口IRunnableBird添加Run方法

  1. package liskov_substitution
  2. type IGoodBird interface {
  3. ID() int
  4. Name() string
  5. Tweet() error
  6. }
  7. type IFlyableBird interface {
  8. IGoodBird
  9. Fly() error
  10. }
  11. type IRunnableBird interface {
  12. IGoodBird
  13. Run() error
  14. }

GoodNormalBird.go

GoodNormalBird提供对IGoodBird的基础实现

  1. package liskov_substitution
  2. import "fmt"
  3. type GoodNormalBird struct {
  4. iID int
  5. sName string
  6. }
  7. func NewGoodNormalBird(id int, name string) *GoodNormalBird {
  8. return &GoodNormalBird{
  9. id,
  10. name,
  11. }
  12. }
  13. func (me *GoodNormalBird) ID() int {
  14. return me.iID
  15. }
  16. func (me *GoodNormalBird) Name() string {
  17. return me.sName
  18. }
  19. func (me *GoodNormalBird) Tweet() error {
  20. fmt.Printf("GoodNormalBird.Tweet, id=%v, name=%v\n", me.ID(), me.Name())
  21. return nil
  22. }

GoodFlyableBird.go

GoodFlyableBird通过聚合GoodNormalBird实现IGoodBird接口, 通过提供Fly方法实现IFlyableBird子接口

  1. package liskov_substitution
  2. import "fmt"
  3. type GoodFlyableBird struct {
  4. GoodNormalBird
  5. }
  6. func NewGoodFlyableBird(id int, name string) IGoodBird {
  7. return &GoodFlyableBird{
  8. *NewGoodNormalBird(id, name),
  9. }
  10. }
  11. func (me *GoodFlyableBird) Fly() error {
  12. fmt.Printf("GoodFlyableBird.Fly, id=%v, name=%v\n", me.ID(), me.Name())
  13. return nil
  14. }

GoodOstrichBird.go

GoodOstrichBird通过聚合GoodNormalBird实现IGoodBird接口, 通过提供Run方法实现IRunnableBird子接口

  1. package liskov_substitution
  2. import (
  3. "fmt"
  4. )
  5. type GoodOstrichBird struct {
  6. GoodNormalBird
  7. }
  8. func NewGoodOstrichBird(id int, name string) IGoodBird {
  9. return &GoodOstrichBird{
  10. *NewGoodNormalBird(id, name),
  11. }
  12. }
  13. func (me *GoodOstrichBird) Run() error {
  14. fmt.Printf("GoodOstrichBird.Run, id=%v, name=%v\n", me.ID(), me.Name())
  15. return nil
  16. }

liskov_substitution_test.go

单元测试

  1. package main
  2. import "testing"
  3. import (lsp "learning/gooop/principles/liskov_substitution")
  4. func Test_LSP(t *testing.T) {
  5. fnCallAndLog := func(fn func() error) {
  6. e := fn()
  7. if e != nil {
  8. t.Logf("error = %s", e.Error())
  9. }
  10. }
  11. // start testing bad /////////////////////////////////////////////////
  12. bb := lsp.NewBadNormalBird(1, "普鸟")
  13. fnCallAndLog(bb.Tweet)
  14. fnCallAndLog(bb.Fly)
  15. bo := lsp.NewBadOstrichBird(2, "鸵鸟")
  16. fnCallAndLog(bo.Tweet)
  17. fnCallAndLog(bo.Fly)
  18. if it, ok := bo.(*lsp.BadOstrichBird);ok {
  19. fnCallAndLog(it.Run)
  20. }
  21. // end testing bad /////////////////////////////////////////////////
  22. // start testing good /////////////////////////////////////////////////
  23. fnTestGoodBird := func(gb lsp.IGoodBird) {
  24. fnCallAndLog(gb.Tweet)
  25. if it, ok := gb.(lsp.IFlyableBird);ok {
  26. fnCallAndLog(it.Fly)
  27. }
  28. if it, ok := gb.(lsp.IRunnableBird);ok {
  29. fnCallAndLog(it.Run)
  30. }
  31. }
  32. fnTestGoodBird(lsp.NewGoodFlyableBird(11, "飞鸟"))
  33. fnTestGoodBird(lsp.NewGoodOstrichBird(12, "鸵鸟"))
  34. // end testing good /////////////////////////////////////////////////
  35. }

测试输出

  1. $ go test -v liskov_substitution_test.go
  2. === RUN Test_LSP
  3. BadNormalBird.Tweet, id=1, name=普鸟
  4. BadNormalBird.Fly, id=1, name=普鸟
  5. BadNormalBird.Tweet, id=2, name=鸵鸟
  6. liskov_substitution_test.go:10: error = BadOstrichBird.Fly, cannot fly, id=2, name=鸵鸟
  7. BadOstrichBird.Run, id=2, name=鸵鸟
  8. GoodNormalBird.Tweet, id=11, name=飞鸟
  9. GoodFlyableBird.Fly, id=11, name=飞鸟
  10. GoodNormalBird.Tweet, id=12, name=鸵鸟
  11. GoodOstrichBird.Run, id=12, name=鸵鸟
  12. --- PASS: Test_LSP (0.00s)
  13. PASS
  14. ok command-line-arguments 0.002s