面向对象设计五大基本原则
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之一。 原则的目的: 高内聚,低耦合
| 名称 | 定义 |
|---|---|
| 单一职责原则(Single Responsibility Principle, SRP) | 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。 |
| 开闭原则(Open-Closed Principle, OCP) | 类的改动是通过增加代码进行的,而不是修改源代码。 |
| 里氏代换原则(Liskov Substitution Principle, LSP | 任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能。 |
| 依赖倒转原则(Dependence Inversion Principle, DIP) | 依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程。桥接模式 |
| 接口隔离原则(Interface Segregation Principle, ISP) | 不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去。 |
| 合成复用原则(Composite Reuse Principle, CRP) | 如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。组合模式 |
| 迪米特法则(Law of Demeter, LoD) | 一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理)外观模式 |
1.1 单一职责原则
类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。
1.2 开闭原则
类的改动是通过增加代码进行的,而不是修改源代码。
1.2.1平铺式设计
每增加一个功能就需要在本类中加一个方法,假如一个方法出错,整个类都会出错。如何解决这个问题?
package main
import "fmt"
//我们要写一个类,Banker银行业务员
type Banker struct {
}
//存款业务
func (this *Banker) Save() {
fmt.Println( "进行了 存款业务...")
}
//转账业务
func (this *Banker) Transfer() {
fmt.Println( "进行了 转账业务...")
}
//支付业务
func (this *Banker) Pay() {
fmt.Println( "进行了 支付业务...")
}
func main() {
banker := &Banker{}
banker.Save()
banker.Transfer()
banker.Pay()
}
1.2.2 开闭原则设计
通过interface,那么我们就可以抽象一层出来,制作一个抽象的Banker模块,然后提供一个抽象的方法。 分别根据这个抽象模块,去实现支付Banker(实现支付方法),转账Banker(实现转账方法)
package main
import "fmt"
type Bank interface {
Do()
}
type SaveBanker struct{}
type WithdrawBanker struct{}
type TansformerBanker struct{}
//存款业务
func (this *SaveBanker) Do() {
fmt.Println("进行了 存款业务...")
}
//取钱业务
func (this *WithdrawBanker) Do() {
fmt.Println("进行了 取钱业务...")
}
//转账业务
func (this *TansformerBanker) Do() {
fmt.Println("进行了 转账业务...")
}
// 实现架构层(基于抽象层进行业务封装-针对interface接口进行封装)
func BankBusiness(b Bank) {
//通过接口来向下调用,(多态现象)
b.Do()
}
func main() {
BankBusiness(&SaveBanker{})
BankBusiness(&WithdrawBanker{})
BankBusiness(&TansformerBanker{})
}
实际上接口的最大的意义就是实现多态的思想,就是我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能适应当下所实现的全部模块,也适应未来实现的模块来进行调用。 调用未来可能就是接口的最大意义所在吧,这也是为什么架构师那么值钱,因为良好的架构师是可以针对interface设计一套框架,在未来许多年却依然适用。
1.3 依赖倒转原则
不要依赖具体的实现,面向接口开发
package main
import "fmt"
// === > 奔驰汽车 <===
type Benz struct {
}
func (this *Benz) Run() {
fmt.Println("Benz is running...")
}
// === > 宝马汽车 <===
type BMW struct {
}
func (this *BMW) Run() {
fmt.Println("BMW is running ...")
}
//===> 司机张三 <===
type Zhang3 struct {
//...
}
func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
fmt.Println("zhang3 Drive Benz")
benz.Run()
}
func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
fmt.Println("zhang3 drive BMW")
bmw.Run()
}
//===> 司机李四 <===
type Li4 struct {
//...
}
func (li4 *Li4) DriveBenZ(benz *Benz) {
fmt.Println("li4 Drive Benz")
benz.Run()
}
func (li4 *Li4) DriveBMW(bmw *BMW) {
fmt.Println("li4 drive BMW")
bmw.Run()
}
func main() {
//业务1 张3开奔驰
benz := &Benz{}
zhang3 := &Zhang3{}
zhang3.DriveBenZ(benz)
//业务2 李四开宝马
bmw := &BMW{}
li4 := &Li4{}
li4.DriveBMW(bmw)
}
面向抽象层依赖倒转
package main
import "fmt"
// 抽象层
type Car interface {
Run()
}
type Driver interface {
Drive(car Car)
}
type BenZ struct {
//...
}
// 实现层
func (benz * BenZ) Run() {
fmt.Println("Benz is running...")
}
type Bmw struct {}
func (bmw * Bmw) Run() {
fmt.Println("Bmw is running...")
}
type Zhang_3 struct {}
func (zhang3 *Zhang_3) Drive(car Car) {
fmt.Println("Zhang3 drive car")
car.Run()
}
type Li_4 struct {}
func (li4 *Li_4) Drive(car Car) {
fmt.Println("li4 drive car")
car.Run()
// 业务逻辑层
func main() {
//张3 开 宝马
var zhang3 Driver
zhang3 = &Zhang_3{}
zhang3.Drive(&Bmw{})
//李4 开 奔驰
var li4 Driver
li4 = &Li_4{}
li4.Drive(&BenZ{})
}
1.4 里氏替换原则
子类可以替换父类的方法,逻辑不变。go语言中的接口是很好替换的。
1.5 接口隔离原则
接口和接口之间是相互隔离的,Go是支持的。