乐趣区

关于golang:手撸golang-架构设计原则-里氏替换原则

手撸 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
退出移动版