Lingyin's Blog

设计模式-指导原则

如果把设计模式原则看作为战略, 那么具体的设计模式(创建型模式、结构型模式、行为型模式)就是具体的战术。

根据不同的设计原则,前人发明出了许许多多不同的设计模式,这些设计模式在实际的项目中不断打磨迭代,最终形成了一种固定的设计套路。在实际的项目开发中熟练运用这些套路, 对我们代码能力的提高是毋庸置疑的。 尤其是一些优秀的开源项目中使用了大量的设计模式, 不懂的话, 连看都看不懂。接下来的篇幅介绍了几种应用广泛的设计原则,必须要重点掌握。

1. 单一职责原则(Single Responsibility Principle, SRP)

单一职责顾名思义就是只负责一件事。往大了说,handler层负责前端的请求处理,biz层负责具体的业务逻辑,dao层负责和数据库的交互,每一层干自己该干的事,这是单一职责。往小了说,在设计一个类的时候,要尽量保证类只会负责单一的工作。下面看一个例子:

package main

import "fmt"

// 定义一个矩形类
type Rect struct {
	StartX int // 起始点x
	StartY int // 起始点y
	EndX   int // 终点X
	EndY   int // 终点Y
}

// Draw方法用于在屏幕上绘制矩形
func (r *Rect) Draw() {
	fmt.Printf("起始点: x=%d y=%d, 终点: x=%d, y=%d, 矩形绘制完成\n", r.StartX, r.StartY, r.EndX, r.EndY)
}

// Area计算矩形的面积
func (r *Rect) Area() {
	fmt.Printf("矩形的面积为: %d\n", (r.EndX-r.StartX)*(r.EndY-r.StartY))
}

func main() {
	rect := &Rect{0, 0, 5, 3}
	rect.Draw()
	rect.Area()
}

上面那段代码的执行逻辑:先实例化了一个矩形对象,然后调用了矩形对象的Draw()和Area()方法,看上去并没什么特别的问题。但是随着业务复杂程度的加大,很可能在Area()方法中增加一些条件判断等业务逻辑并且很可能在无意中更改了矩形类中的属性,从而影响了Draw()方法,导致绘制的不准确(如果缺少文档并且公司的人员流动频繁,则加剧了这种可能性)。

下面根据单一职责原则进行拆分:

package main

import "fmt"

type RectDraw struct {
	StartX int // 起始点x
	StartY int // 起始点y
	EndX   int // 终点X
	EndY   int // 终点Y
}

// Draw方法用于在屏幕上绘制矩形
func (r *RectDraw) Draw() {
	fmt.Printf("起始点: x=%d y=%d, 终点: x=%d, y=%d, 矩形绘制完成\n", r.StartX, r.StartY, r.EndX, r.EndY)
}

type RectCompute struct {
	Length int // 长
	Width  int // 宽
}

// Area计算矩形的面积
func (r *RectCompute) Area() {
	fmt.Printf("矩形的面积为: %d\n", r.Length*r.Width)
}

func main() {
	rd := &RectDraw{0, 0, 5, 3}
	rd.Draw()

	rc := &RectCompute{6, 8}
	rc.Area()
}

上面的代码将原先的类拆分为了两个,一个负责绘制,一个负责计算。如果后续有增加周长方法的需求,只需要将周长方法加到RectCompute类中就好,不会影响到RectDraw类。

总结一下:单一职责原则最大限度了保证了项目的高内聚、低耦合,但是现实中基本不太可能完全按照单一职责原则进行,如果这样的话,那代码就太分散琐碎了。所以单一职责原则只是一个理想情况,要尽量遵循。

2. 开闭原则(The Open/Closed Principle, OCP)

维基百科对开闭原则的定义是:规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。

翻译成人话就是:当我们想要增加一个类的功能时,不要更改类,而要通过扩展类的形式实现功能的添加。为什么呢?假设线上的一个模块已经运行了半年了,基本也没啥bug出现。但是现在我想新添加个功能, 如果不符合开闭原则,那我很可能会更改模块内部的代码,来实现新功能的添加。万一因为这个新添加的功能,导致原有的功能出现bug,那不是完蛋了。

开闭原则的要求就是,原有的模块代码不能动。通过增加新代码的方式,实现功能的添加。这样,哪怕我新功能出现bug了,但并不会影响之前的功能。

看一个实际的例子:

package main

import "fmt"

// 绘制矩形和正方形的类
type Drawer struct {
	x int
	y int
}

func (d *Drawer) Rect(w, h int) {
	fmt.Printf("起点x: %d, 起点y:%d, 长: %d, 宽: %d\n", d.x, d.y, w, h)
}

func (d *Drawer) Square(length int) {
	fmt.Printf("起点x: %d, 起点y:%d, 边长: %d\n", d.x, d.y, length)
}

func main() {
	d := &Drawer{0, 0}
	d.Rect(8, 6)
	d.Square(6)
}

上面的代码定义了一个Drawer类,该类有两个属性: x和y,代表绘制的起始点;两个方法: Rect和Square,用来绘制矩形和正方形。现在我需要新增加一个方法,用来绘制圆形,新添加后的代码如下:

package main

import "fmt"

// 绘制矩形和正方形的类
type Drawer struct {
	x int
	y int
}

func (d *Drawer) Rect(w, h int) {
	fmt.Printf("起点x: %d, 起点y:%d, 长: %d, 宽: %d\n", d.x, d.y, w, h)
}

func (d *Drawer) Square(length int) {
	fmt.Printf("起点x: %d, 起点y:%d, 边长: %d\n", d.x, d.y, length)
}

func (d *Drawer) Circle(radius int) {
	fmt.Printf("起点x: %d, 起点y:%d, 半径: %d\n", d.x, d.y, radius)
}

func main() {
	d := &Drawer{0, 0}
	d.Rect(8, 6)
	d.Square(6)
	d.Circle(3)
}

给Drawer类新添加了Circle方法,其实就已经违反了开闭原则,为什么呢?因为Circle方法改动了Drawer类代码。当然由于Golang特殊的语法特性,不是很容易看出来,如果换做是Python等语言,那就容易看出来了。还是同样的情况,由于代码简单可能看不出有什么问题。但是随着代码量的增加,再添加新类的时候很有可能改变了原有类的属性,导致所有依赖该属性的方法出现不可预估的bug。为此,我们引入开闭原则,将代码进行改造:

package main

import "fmt"

type Drawer interface {
	Draw()
}

type RectDrawer struct {
	x int
	y int
	w int
	h int
}

func (rd *RectDrawer) Draw() {
	fmt.Printf("起点x: %d, 起点y:%d, 长: %d, 宽: %d\n", rd.x, rd.y, rd.w, rd.h)
}

type SquareDrawer struct {
	x      int
	y      int
	length int
}

func (sd *SquareDrawer) Draw() {
	fmt.Printf("起点x: %d, 起点y:%d, 边长: %d\n", sd.x, sd.y, sd.length)
}

func DrawShape(d Drawer) {
	d.Draw()
}

func main() {
	rd := &RectDrawer{0, 0, 8, 6}
	DrawShape(rd)

	sd := &SquareDrawer{0, 0, 6}
	DrawShape(sd)
}

上边的代码把Drawer抽象为一个接口,这样就提供了可扩展的能力。新添加的功能只要实现该接口,就可以被DrawShape调用,新添加的功能具体是怎么实现的并没有人关心。而且新添加的功能并没有影响到之前Rect和Square的代码,做到了高内聚低耦合。 添加Cicle类,并被调用的代码如下:

package main

import "fmt"

type Drawer interface {
	Draw()
}

type RectDrawer struct {
	x int
	y int
	w int
	h int
}

func (rd *RectDrawer) Draw() {
	fmt.Printf("起点x: %d, 起点y:%d, 长: %d, 宽: %d\n", rd.x, rd.y, rd.w, rd.h)
}

type SquareDrawer struct {
	x      int
	y      int
	length int
}

func (sd *SquareDrawer) Draw() {
	fmt.Printf("起点x: %d, 起点y:%d, 边长: %d\n", sd.x, sd.y, sd.length)
}

type CircleDrawer struct {
	x      int
	y      int
	radius int
}

func (cd *CircleDrawer) Draw() {
	fmt.Printf("起点x: %d, 起点y:%d, 半径: %d\n", cd.x, cd.y, cd.radius)
}

func DrawShape(d Drawer) {
	d.Draw()
}

func main() {
	rd := &RectDrawer{0, 0, 8, 6}
	DrawShape(rd)

	sd := &SquareDrawer{0, 0, 6}
	DrawShape(sd)

	cd := &CircleDrawer{0, 0, 3}
	DrawShape(cd)
}

可以看到,只要新定义Circle类并实现Draw方法,就可以将绘制圆形的功能接入系统,不会影响到原有的系统功能。