Skip to content

Open-Closed Principle 开放封闭准则的golang实例

author: dameng(Makefile君)

概念引导

S.O.L.I.D 原则中的 O 即指明的是开放封闭准则, 其倡导软件实体应该是可以扩展的,但不接受修改.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

场景搭建

为了方便讲解,这里以一个现实中的例子来讲解,假设我们的主人公小明刚刚找了一份新工作,顺带想租个离公司近一点的房子, 与是他登陆“某家”网进行搜寻,发现其可以按距离,也可以按价位进行搜索。

我们这里对房屋进行一些抽象,如果我们的用户关心的只有距离和价位置,那么可以进行如下的抽象。

type Estate struct {
    name string
    distance float64
    price int64
}

除了举例和价为之外,我们增加了一个name属性,用来标名房屋具体的不同。

而查询的条件有两个: 距离,价位.

我们需要能分别按照距离和加为进行查找,这里也进行一些抽象如下

type Filter struct {

}

func (f *Filter) FilterByPrice(estates []Estate, low int64, high int64) []*Estate {
    result := make([]*Estate, 0)
    for i,v := range estates {
        if low <= v.price && v.price <= high {
            result = append(result, &estates[i])
        }
    }
    return result
}

func (f *Filter) FilterByDistance(estates []Estate, distance float64) []*Estate {
    result := make([]*Estate, 0)
    for i,v := range estates {
        if v.distance <= distance {
            result = append(result, &estates[i])
        }
    }
    return result
}

可以看到,Filter结构体本身并不包含额外的属性,只是增加了两个查询的方法。

真正调用时,我们需要先构建一些房屋的数据,比如

houses := []Estate{ 
    {"福山一村#1", 3.0, 5500}, 
    {"由由一村#1", 4.0, 4000},
    {"高境一村#1", 7.0, 3500} }

在查找时,我们需要声明一个Filter的实例以便调用其方法

f := Filter{}

按价格查找

for _,v := range f.FilterByPrice(houses, 2500, 4500) {
    fmt.Println(v.name)
}

按距离查找

for _,v := range f.FilterByDistance(houses, 5.0) {
  fmt.Println(v.name)
}

需求变更

这个时候一个很自然就能引生出来的需求就是同时按价格和距离进行查找

直观的做法是增加一个FilterByPriceAndDistance, 完成相应条件匹配的搜寻
比如可以按照如下进行实现

func (f *Filter) FilterByPriceAndDistance(estates []Estate, low int64, high int64, distance float64) []*Estate {
    result := make([]*Estate, 0)
    for i,v := range estates {
        if low <= v.price && v.price <= high && v.distance <= distance {
            result = append(result, &estates[i])
        }
    }
    return result
}

但是这是不符合Open-Closed准则, 因为这是一种“侵入式”的"修改"(调用时函数名称等需要修改),
这种方式实现简单,但是整体缺乏规范性,同时每当有一种新的需求来的时候,还要手工增加额外的代码,扩展不够方便。

接下来,我们尝试实践使用OC原则对其进行改造

OC抽象Spec

为了避免这种累加式的修改,我们可以抽离出一层中间概念,用来统一描述查询条件。
这里暂定其结构体名称为Spec

type Spec interface {
    IsSatisfied(e *Estate) bool
}

这里对于price和distance分别做两个接口的实现

type DistanceSpec struct {
    distance float64
}

type PriceSpec struct {
    low int64
    high int64
}

而其IsSatisfied方法的实现为

func (d DistanceSpec) IsSatisfied(e *Estate) bool {
    return e.distance <= d.distance
}

func (p PriceSpec) IsSatisfied(e *Estate) bool {
    return e.price >= p.low && e.price <= p.high
}

我们在调用Filter的方法时可以做如下的改造

func (f *Filter) Filter(estates []Estate, spec Spec) []*Estate {
    result := make([]*Estate, 0)
    for i,v := range estates {
        if  spec.IsSatisfied(&v) {
            result = append(result, &estates[i])
        }
    }
    return result
}

可以看到,如果要新增一个查询条件的话,我们仍然需要做一些修改,
但这里已经不再是对Filter方法的修改,而是编写一个Spec来完成扩展。
这是符合OC原则的。另外,还有一个好处,当多个查询条件产生时,时按或还是按与进行条件判断也是一个问题。
相比之前的代码,抽象出Spec接口后可以更好的实现这一点。

type AndSpec struct {
    first,second Spec
}

func (a AndSpec) IsSatisfied(e *Estate) bool {
    return a.first.IsSatisfied(e) && a.second.IsSatisfied(e)
}

相应的,其调用方式为

f := Filter{}
priceSpec := PriceSpec{2500, 4500}
distanceSpec := DistanceSpec{5.0}
andSpec := AndSpec{priceSpec,distanceSpec}
for _,v := range f.Filter(houses, andSpec) {
  fmt.Println(v.name)
}

完整的代码如下

package main

import (
    "fmt"
)

type Estate struct {
    name string
    distance float64
    price int64
}


type Filter struct {

}

type Spec interface {
    IsSatisfied(e *Estate) bool
}

type DistanceSpec struct {
    distance float64
}

type PriceSpec struct {
    low int64
    high int64
}

type AndSpec struct {
    first,second Spec
}

func (d DistanceSpec) IsSatisfied(e *Estate) bool {
    return e.distance <= d.distance
}

func (p PriceSpec) IsSatisfied(e *Estate) bool {
    return e.price >= p.low && e.price <= p.high
}

func (a AndSpec) IsSatisfied(e *Estate) bool {
    return a.first.IsSatisfied(e) && a.second.IsSatisfied(e)
}

func (f *Filter) Filter(estates []Estate, spec Spec) []*Estate {
    result := make([]*Estate, 0)
    for i,v := range estates {
        if  spec.IsSatisfied(&v) {
            result = append(result, &estates[i])
        }
    }
    return result
}


func main() {
    houses := []Estate{ 
        {"福山一村#1", 3.0, 5500}, 
        {"由由一村#1", 4.0, 4000},
        {"高境一村#1", 7.0, 3500} }
    fmt.Println(houses)
    f := Filter{}
    fmt.Println("---------")
    priceSpec := PriceSpec{2500, 4500}
    for _,v := range f.Filter(houses, priceSpec) {
        fmt.Println(v.name)
    }
    fmt.Println("---------")
    distanceSpec := DistanceSpec{5.0}
    for _,v := range f.Filter(houses, distanceSpec) {
        fmt.Println(v.name)
    }
    fmt.Println("---------")
    andSpec := AndSpec{priceSpec,distanceSpec}
    for _,v := range f.Filter(houses, andSpec) {
        fmt.Println(v.name)
    }
}

Comments