美文网首页程序员设计模式
手撸golang 架构设计原则 里氏替换原则

手撸golang 架构设计原则 里氏替换原则

作者: 老罗话编程 | 来源:发表于2021-01-28 12:05 被阅读0次

    手撸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
    
    

    相关文章

      网友评论

        本文标题:手撸golang 架构设计原则 里氏替换原则

        本文链接:https://www.haomeiwen.com/subject/timttltx.html