暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于golang阐述面向对象的设计原则

政采云运维团队 2021-09-10
289

在小型的,非正式的项目里,很多的设计工作是程序员坐在键盘前完成的。这里的“设计”可能就是指在编写具体代码前先用伪代码写出一个类的接口,也可能是在编码前先画出几个类之间的关系图,还可能就是询问另一个程序员哪个设计模式更好。无论是何种方式来进行设计,小型项目也能和大型项目一样从精心的设计之中获益,而如果能认识到设计是一项明确的活动,你就更会获益匪浅。  -- 《代码大全》


塔科马海峡吊桥在设计之初,只考虑了它是否足够结实,以应对未来的重量负载。然而1940年的狂风大作,导致桥轰然倒塌,工程师们这才意识到空气动力学的重要性。在软件设计领域,这种问题会被放的更大,尤其是在复杂的系统架构里,上游的瑕疵可能会在下游放大百倍。

简述

一般来讲七大设计原则是指以下七个:开闭原则、里氏替换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则。七个原则又彼此关联,也就是说很多时候会同时满足或者同时违反。其中开闭原则又可以认为是这七大原则中的基石,其他六个原则都满足了开闭原则。

开闭原则

首先我们讲一下开闭原则,包括以下三个问题
    1. 什么是开闭原则?
    2. 为什么要遵守开闭原则?
    1. 怎么遵守开闭原则?

什么是开闭原则?

  • 定义

    • 开闭原则(Open Closed Principle,OCP) 由勃兰特.梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》( Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension, but closed for modification),这就是开闭原则的经典定义。

  • 概念理解

    • 根据勃兰特.梅耶给出的定义,要理解开闭原则就需要我们理解这两点,什么叫做对扩展开放?什么叫做对修改关闭?我的理解是当需求发生变化时,我们应在原有的模块上进行扩展,而不是修改已经存在的模块,以满足新的需求。这种策略可以提高代码的重用率,并且也是更易于维护的。

为什么要遵守开闭原则?

这里我用一个段golang代码去解释,各位看官直接阅读即可。

  • 场景

    • 一位农场主卖猪,每只猪都有种类和单价两个属性,于是我们可以定义这样一个猪的接口,和结构体。

type IPig interface {
Price() float64
Breed() string
}

type Pig struct {
price float64
breed string
}
func (p *Pig)Price() float64 {
return p.price
}
func (p *Pig)Breed() string {
return p.breed
}
    • 这一天,农场主想搞一个优惠大促销,所有猪的价格打八折,这个时候由于Price方法获取的是原价,已经不能满足我们的需求,为了满足需求,现在有三个策略。

    • 直接修改Price方法。

type IPig interface {
Price() float64
Breed() string
}

type Pig struct {
price float64
breed string
}
func (p *Pig)Price() float64 {
return p.price * 0.8
}
func (p *Pig)Breed() string {
return p.breed
}
// 这种方式只需要修改第11行,变为我们现在需要的打八折的情况,很显然这种方式有一个缺陷,
// 就是原有的获取原价的业务逻辑,现在获取到的不是原价了,这是一种拆东墙补西墙的方式。
    • 为IPig接口添加新的方法,用来获取优惠价格。

type IPig interface {
Price() float64
Breed() string
DiscountPrice() float64
}

type Pig struct {
price float64
breed string
}
func (p *Pig)Price() float64 {
return p.price
}
func (p *Pig)Breed() string {
return p.breed
}
func (p *Pig)DiscountPrice() float64 {
return p.price * 0.8
}
// 这种方式既要为接口新增方法也要为结构体新增方法,但是接口是具有契约性质的,实现IPig的类型
// 可能也不止*Pig一种,所以接口定义好了之后,尽量不应在对其进行修改,新增,或者删除。
    • 使用子类扩展来实现。

type IPig interface {
Price() float64
Breed() string
}

type Pig struct {
price float64
breed string
}
func (p *Pig)Price() float64 {
return p.price
}
func (p *Pig)Breed() string {
return p.breed
}

type DiscountPig struct {
Pig
}

func (d *DiscountPig) Price() float64 {
return d.price * 0.8
}
// 这种方式只增加类DiscountPig并嵌入Pig类,对原有的模块无任何修改,需要获取优惠价格时就使用DiscountPig,
// 需要获取原价时就使用Pig,通过这种扩展而不修改的方式,复用了Pig的属性和Breed方法,同时由于原有的
// 模块未经过修改,整体也是易于维护的,这就是开闭原则很好的体现
  • 总结

    • 遵守开闭原则的软件可以在需求改变时获得相对的稳定性,而违反这一原则的软件,通常会面临牵一发而动全身的窘境。

怎么遵守开闭原则?

  1. 要善于抽象,模块依赖于固定的抽象。

  2. 让客户端访问接口,而非结构体,这样可以让客户端在毫不知情的情况下,访问到新的结构体。这里我提供一段代码来说明。

package main

type Reader interface {
Read()
}

type Monkey struct {

}

func (m *Monkey)Read() {

}

type Pig struct {

}

func (m *Pig)Read() {

}

func main() {
// 客户端使用Read功能时,永远都是reader.Read(),具体是猴子阅读还是猪阅读,我们可以很方便的切换
var reader Reader
reader = &Pig{}
reader.Read()
reader = &Monkey{}
reader.Read()
}

单一职责原则

我个人认为单一职责原则是设计原则里最容易理解的,同时也是最难做好的,因为场景的不同,职责的划分粒度就会不同,是没有公式可套用的。接下来从以下几个方面来介绍单一职责原则。
    1. 什么是职责?什么是单一职责原则?

    2. 为什么要遵守单一职责原则?

    1. 怎么遵守单一职责?

什么是职责?什么是单一职责原则?

Robert.C Martin对职责给出的定义是这样的:变化的原因(a reason for change)。
因此单一职责的定义也就很明朗了:即只能有一个变化的原因。在软件开发的世界里,就是模块,类,方法,函数只能有一个变化的原因。

为什么要遵守单一职责?

因为软件的首要技术使命就是管理复杂度
其次,要解释为什么要遵守单一职责,本质上也是解释如果不遵守会怎么样?
    1. 用户只想使用这个类里其中一个职责,却不得不把另一个职责也编译到他的软件里。
    2. 修改了其中一个职责,另一个职责也要重新编译。
    1. 因为职责就是变化的原因,所以引起该类变化的原因就有了多个,该类变得更不稳定。

怎么遵守单一职责?

怎么做好单一职责,最核心的问题是职责划分的粒度。其实我认为这是一个没有明显规律可寻的问题,很多时候我们不得不在不停的试错中重构,但是,犯错误也许就是设计的关键所在,毕竟在设计阶段就发现错误,并改正,其代价要远低于编码时期。下面还是通过一段代码加以解释。
  • 场景

    • 现在我们要设计一个文件结构体,可以打开文件,写入数据,清空数据,关闭文件。

    • 方案一,不好的实现,所有职责都耦合到了一个接口,一个结构体里

// bad
type IFile interface {
Open()
Close()
Write([]byte)
Clear()
}

type File struct {
name string
}

func (f *File)Open() {

}
func (f *File)Close() {

}
func (f *File)Write([]byte) {

}
func (f *File)Clear() {

}

func main() {
f := &File{}
f.Open()
defer f.Close()
f.Write([]byte("hello"))
f.Clear()
}
    • 方案二,好的实现,将文件的四个功能分类成了数据控制和开关控制两种接口,虽然所有功能还是全部耦合到了File结构体里,但是这是符合当前语义的。

type IFileData interface {
Write([]byte)
Clear()
}
type IFileControl interface {
Open()
Close()
}

type File struct {
name string
}

func (f *File)Open() {

}
func (f *File)Close() {

}
func (f *File)Write([]byte) {

}
func (f *File)Clear() {

}

func main() {
f := &File{
name: "demo",
}
f.Open()
defer f.Close()
f.Write([]byte("hello"))
f.Clear()
}
方案三,如果场景允许,您应将尽可能的把结构体也拆解成两个,分别实现数据控制接口和开关接口。
  • 总结

    • 单一职责原则本质上是我们对模块抽象粒度评判的一个标准,即单一职责的粒度。降低每个模块的复杂度,就会很大程度上提升整体的灵活性,易维护性。


今天就先介绍到这里,关注不迷路,欢迎留言,共同学习,共同探索。大家下篇文章见。




文章转载自政采云运维团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论