在小型的,非正式的项目里,很多的设计工作是程序员坐在键盘前完成的。这里的“设计”可能就是指在编写具体代码前先用伪代码写出一个类的接口,也可能是在编码前先画出几个类之间的关系图,还可能就是询问另一个程序员哪个设计模式更好。无论是何种方式来进行设计,小型项目也能和大型项目一样从精心的设计之中获益,而如果能认识到设计是一项明确的活动,你就更会获益匪浅。 -- 《代码大全》
简述
开闭原则
什么是开闭原则? 为什么要遵守开闭原则?
怎么遵守开闭原则?
什么是开闭原则?
定义
开闭原则(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方法,同时由于原有的
// 模块未经过修改,整体也是易于维护的,这就是开闭原则很好的体现总结
遵守开闭原则的软件可以在需求改变时获得相对的稳定性,而违反这一原则的软件,通常会面临牵一发而动全身的窘境。
怎么遵守开闭原则?
要善于抽象,模块依赖于固定的抽象。
让客户端访问接口,而非结构体,这样可以让客户端在毫不知情的情况下,访问到新的结构体。这里我提供一段代码来说明。
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()
}
单一职责原则
什么是职责?什么是单一职责原则?
为什么要遵守单一职责原则?
怎么遵守单一职责?
什么是职责?什么是单一职责原则?
为什么要遵守单一职责?
用户只想使用这个类里其中一个职责,却不得不把另一个职责也编译到他的软件里。 修改了其中一个职责,另一个职责也要重新编译。
因为职责就是变化的原因,所以引起该类变化的原因就有了多个,该类变得更不稳定。
怎么遵守单一职责?
场景
现在我们要设计一个文件结构体,可以打开文件,写入数据,清空数据,关闭文件。
方案一,不好的实现,所有职责都耦合到了一个接口,一个结构体里
// 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()
}
总结
单一职责原则本质上是我们对模块抽象粒度评判的一个标准,即单一职责的粒度。降低每个模块的复杂度,就会很大程度上提升整体的灵活性,易维护性。
今天就先介绍到这里,关注不迷路,欢迎留言,共同学习,共同探索。大家下篇文章见。




