手擼golang 架構設計原則 里氏替換原則

手擼golang 架構設計原則 里氏替換原則

緣起

最近複習設計模式
拜讀譚勇德的<<設計模式就該這樣學>>
該書以java語言演繹了常見設計模式
本系列筆記擬採用golang練習之java

里氏替換原則

里氏替換原則(Liskov Substitution Principle, LSP):
若是對每個類型爲T1的對象O1
都有類型爲T2的對象O2
使得以T1定義的全部程序P
在全部對象O1都替換成O2時
程序P的行爲沒有發生變化
那麼類型T2是類型T1的子類型
_
能夠理解爲:
全部引用父類的地方
必須能透明地使用其子類對象
子類對象可以替換父類對象
而保持程序功能不變
_
里氏替換原則的優勢:
(1)約束繼承氾濫,是開閉原則的一種體現
(2)增強程序的健壯性,同時變動時能夠作到很是好的兼容性
_golang

場景

  • 某線上動物園系統, 定義了鳥類接口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的oop

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子接口code

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
相關文章
相關標籤/搜索