3

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

 3 years ago
source link: https://studygolang.com/articles/33105
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK