SOLID原则介绍

SOLID原则介绍

SOLID原则是由罗伯特·C·马丁在21世纪早期引入,指代了面向对象编程和面向对象设计的五个基本原则。遵循SOLID原则可以确保我们设计的代码是易维护、易扩展、易阅读的。SOLID原则同样也适用于Go程序设计。具体SOLID编码原则见下表:

简写全称中文描述
SRPThe Single Responsibility Principle单一功能原则
OCPThe Open Closed Principle开闭原则
LSPThe Liskov Substitution Principle里氏替换原则
DIPThe Dependency Inversion Principle依赖倒置原则
ISPThe Interface Segregation Principle接口分离原则

Single Responsibility Principle - 单一功能原则

单一功能原则:一个类或者模块只负责完成一个职责(或者功能)。

简单来说就是保证我们在设计函数、方法时做到功能单一,权责明确,当发生改变时,只有一个改变它的原因。如果函数/方法承担的功能过多,就意味着很多功能会相互耦合,这样当其中一个功能发生改变时,可能会影响其它功能。单一功能原则,可以使代码后期的维护成本更低、改动风险更低。

例如,有以下代码,用来创建一个班级,班级包含老师和学生,代码如下:

package srp

type Class struct {
	Teacher *Teacher
	Student *Student
}

type Teacher struct {
	Name  string
	Class int
}

type Student struct {
	Name  string
	Class int
}

func createClass(teacherName, studentName string, class int) (*Teacher, *Student) {
	teacher := &Teacher{
		Name:  teacherName,
		Class: class,
	}
	student := &Student{
		Name:  studentName,
		Class: class,
	}

	return teacher, student
}

func CreateClass() *Class {
	teacher, student := createClass("colin", "lily", 1)
	return &Class{
		Teacher: teacher,
		Student: student,
	}
}

上面的代码段通过createClass函数创建了一个老师和学生,老师和学生属于同一个班级。但是现在因为老师资源不够,要求一个老师管理多个班级。这时候,需要修改createClass函数的class参数,因为创建学生和老师是通过createClass函数的class参数偶合在一起,所以修改创建老师的代码,势必会影响创建学生的代码,其实,创建学生的代码我们是压根不想改动的。这时候createClass函数就不满足单一功能原则。需要修改为满足单一功能原则的代码,修改后代码段如下:

package srp

type Class struct {
	Teacher *Teacher
	Student *Student
}

type Teacher struct {
	Name  string
	Class int
}

type Student struct {
	Name  string
	Class int
}

func CreateStudent(name string, class int) *Student {
	return &Student{
		Name:  name,
		Class: class,
	}
}

func CreateTeacher(name string, classes []int) *Teacher {
	return &Teacher{
		Name:  name,
		Class: classes,
	}
}

func CreateClass() *Class {
	teacher := CreateTeacher("colin", []int{1, 2})
	student := CreateStudent("lily", 1)
	return &Class{
		Teacher: teacher,
		Student: student,
	}
}

上述代码,我们将createClass函数拆分成2个函数CreateStudentCreateTeacher,分别用来创建学生和老师,各司其职,代码互不影响。

Open / Closed Principle - 开闭原则

开闭原则:软件实体应该对扩展开放、对修改关闭。

简单来说就是通过在已有代码基础上扩展代码,而非修改代码的方式来完成新功能的添加。开闭原则,并不是说完全杜绝修改,而是尽可能不修改或者以最小的代码修改代价来完成新功能的添加。

以下是一个满足开闭原则的代码段:

type IBook interface {
	GetName() string
	GetPrice() int
}

// NovelBook 小说
type NovelBook struct {
	Name   string
	Price  int
}

func (n *NovelBook) GetName() string {
	return n.Name
}

func (n *NovelBook) GetPrice() int {
	return n.Price
}

上述代码段,定义了一个Book接口和Book接口的一个实现:NovelBook(小说)。现在有新的需求,对所有小说打折统一打5折,根据开闭原则,打折相关的功能应该利用扩展实现,而不是在原有代码上修改,所以,新增一个OffNovelBook接口,继承NovelBook,并重写GetPrice方法。

type OffNovelBook struct {
	NovelBook
}

// 重写GetPrice方法
func (n *OffNovelBook) GetPrice() int {
	return n.NovelBook.GetPrice() / 5
}

Liskov Substitution Principle - 里氏替换原则

里氏替换原则:如果S是T的子类型,则类型T的对象可以替换为类型S的对象,而不会破坏程序。在Go开发中,里氏替换原则可以通过接口来实现。

例如,以下是一个符合里氏替换原则的代码段:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type ReadWriter interface {
	Reader
	Writer
}

func Write(w Writer, p []byte) (int, error) {
	return w.Write(p)
}

我们可以将Write函数中的Writer参数替换为其子类型ReadWriter,而不影响已有程序:

func Write(rw ReadWriter, p []byte) (int, error) {
	return rw.Write(p)
}

Dependency Inversion Principle - 依赖倒置原则

依赖倒置原则:依赖于抽象而不是一个实例,其本质是要面向接口编程,不要面向实现编程。

以下是一个符合依赖倒置原则的代码段:

package solid

import "fmt"

 // 司机抽象接口
IDriver interface {
	Drive(car ICar) // 开车
}

// Driver 具体的司机
type Driver struct{}

// Drive 实现IDriver接口
func (Driver) Drive(car ICar) {
	car.Run()
}

// ICar 汽车抽象接口
type ICar interface {
	Run()
}

// 不同汽车的实现
// Benz 奔驰汽车
type Benz struct{}

func (Benz) Run() {
	fmt.Println("奔驰汽车开始运行...")
}

// BMW 宝马汽车
type BMW struct{}

func (BMW) Run() {
	fmt.Println("宝马汽车开始运行...")
}

上述代码中,IDriver接口的Drive方法,依赖于ICar接口,而不是某一具体的汽车类型。这样司机就可以开不同的汽车。

Interface Segregation Principle - 接口隔离原则

接口隔离原则:客户端程序不应该依赖它不需要的方法。

例如,我们通过Save函数将文档保存到磁盘:

// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error

上面的Save函数虽然能够完成任务,但有下面的问题:

  1. Save函数使用*os.File做为保存Document的文件,如果后面需要将Document保存到网络存储,Save函数就无法满足。
  2. 因为Save函数直接保存文档到磁盘,不方便后续的测试。
  3. *os.File包含了许多跟Save函数的方法,例如:读取路径以及检查路径是否是软连接等。

可以说上述的Save方法不满足接口隔离原则。可以优化为:

// Save writes the contents of doc to the supplied ReadWriter.
func Save(rw io.ReadWriter, doc *Document) error

但是上述的Save函数仍然有问题,我们其实只需要写操作,不需要读操作,因此,Save函数可以进一步优化为:

// Save writes the contents of doc to the supplied Writer.
func Save(w io.Writer, doc *Document) error

func Save(w io.Writer, doc *Document) error就是最终的符合接口隔离原则的函数。