0 简介:SOILD 面向对象设计的原则
SOLID原则是面向对象设计(OOD)中的五个基本原则,它们有助于创建更健壮、灵活和可维护的软件系统。SOLID是五个原则的首字母缩写。
SOLID原则促进代码清晰、易测试及高效开发,尤其在敏捷环境下,支持持续改进和快速响应需求变化
markdown
单一职责原则(Single Responsibility Principle, SRP)
- **SRP**: 单一职责原则确保类只负责单一功能,提高可维护性。
开放封闭原则(Open/Closed Principle, OCP)
- **OCP**: 开放封闭原则提倡对扩展开放,对修改封闭,增强灵活性。
里氏替换原则(Liskov Substitution Principle, LSP)
- **LSP**: 里氏替换原则保证子类可透明替换基类,维护代码一致性。
接口隔离原则(Interface Segregation Principle, ISP)
- **ISP**: 接口隔离原则主张接口应具体化,避免不必要依赖。
依赖倒置原则(Dependency Inversion Principle, DIP)
- **DIP**: 依赖倒置原则要求依赖于抽象而非具体实现,减少耦合。
1. 单一职责原则(Single Responsibility Principle, SRP)
-
定义:一个类只应该有一个引起变化的原因,即一个类只负责一项职责。
-
使用场景:
当一个类承担多种职责时,这些职责可能会相互耦合,导致类的修改变得复杂和容易出错。 将不同的职责分离到不同的类中,提高代码的可读性和可维护性。
-
实例反例:
gotype User struct {} func (u *User) saveUserToDatabase() { // Save user to database } func (u *User) generateUserReport() { // generate user report }
上面的类User违反了SRP,因为它既负责数据存储又负责报告生成。可以通过拆分成两个类来遵循SRP:
-
SRP正例
gotype UserRepository struct {} func (ur *UserRepository) saveUserToDatabase(User user) { // Save user to database } type UserReportGenerator struct {} func (up *UserRepository) generateUserReport(User user) { // generate user report }
-
单一职责的优势:
提高代码的可读性和可维护性。
减少类的复杂度,使代码更易于理解和修改。
在敏捷的迭代开发中,更容易对单一职责的类进行重构和扩展。 促进更频繁的代码重用和测试。
2. 开放封闭原则(Open/Closed Principle, OCP)
-
定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。
-
使用场景:
当需要为现有系统添加新功能时,不应通过修改现有代码来实现,而是通过扩展原有代码来实现。
-
反例实例:
gotype Shape interface{ draw(); } type Circle struct {} func (c *Circle) draw() { // Draw circle } type Rectangle struct{} func (r *Rectangle)draw() { // Draw rectangle } type GraphicEditor struct {} func (ge *GraphicEditor) drawShape(s Shape) { var i interface{} = s if b, ok := i.(Circle); ok { fmt.Println("i is of type Circle",b) b.draw() } if c, ok := i.(Rectangle); ok { fmt.Println("i is of type Rectangle",c) c.draw() } }
上面的代码违反了OCP,因为每次添加新形状都需要修改GraphicEditor类。可以通过继承和多态来遵循OCP:
go
// Shape 接口定义了所有形状必须实现的 draw 方法
type Shape interface {
draw()
}
// Circle 结构体表示圆形,并实现了 Shape 接口
type Circle struct{}
func (c *Circle) draw() {
fmt.Println("Drawing a circle")
}
// Rectangle 结构体表示矩形,并实现了 Shape 接口
type Rectangle struct{}
func (r *Rectangle) draw() {
fmt.Println("Drawing a rectangle")
}
// GraphicEditor 结构体表示图形编辑器
type GraphicEditor struct{}
// drawShape 方法接受一个实现了 Shape 接口的参数,并调用其 draw 方法
func (ge *GraphicEditor) drawShape(s Shape) {
s.draw()
}
func main() {
// 创建图形编辑器实例
editor := &GraphicEditor{}
// 创建一些形状
circle := &Circle{}
rectangle := &Rectangle{}
// 使用图形编辑器绘制形状
editor.drawShape(circle)
editor.drawShape(rectangle)
}
- 开闭原则的优势:
提高系统的灵活性和可扩展性。 降低修改现有代码引入bug的风险。 在敏捷开发中的作用:
支持增量开发和频繁交付新功能。 有助于实现更快速和低风险的变更。
3. 里氏替换原则(Liskov Substitution Principle, LSP)
-
定义:子类必须能够替换其基类,并且功能不受影响。
-
使用场景:
当使用多态时,确保子类对象能够完全替代父类对象。
-
反例实例:
gotype Bird struct {} func (b *Bird) fly() {//fly} type Ostrich struct { Brid } func (ot *Ostrich) fly() { throw new UnsupportedOperationException("Ostriches can't fly"); }
上面的代码违反了LSP,因为Ostrich不能替代Bird。可以通过引入接口来解决:
go
type FlayAble interface{
fly()
}
type Bird struct {}
func (b *Bird) fly() {//fly}
type Ostrich struct {
Brid
}
func (ot *Ostrich) fly() {
//使用panic来模拟异常,类似Java中的UnsupportedOperationException。
panic("Ostriches can't fly")
}
- LSP优势:
保证代码的正确性和一致性。
通过确保子类的行为与基类一致,提高代码的可替换性。
在敏捷开发中提高代码的可重用性和可测试性。 有助于在迭代开发中确保新功能与现有功能的兼容性。
4. 接口隔离原则(Interface Segregation Principle, ISP)
- 定义:
不应强迫客户端依赖它们不使用的接口,即应为客户端提供小而专用的接口,而不是大而通用的接口。
- 使用场景:
当一个接口承担了过多职责时,将其拆分为多个更具体的接口。
-
反例实例:
scsstype Worker interface { work() eat() } type WorkerImpl struct { } func (wi *WorkerImpl) work() {//working} func (wi *WorkerImpl) eat() {//eating}
Worker接口定义了两个方法:work和eat。WorkerImpl结构体实现了Worker接口, 通过定义接收者为* WorkerImpl的方法来实现work和eat方法。
在main函数中,可以建了一个WorkerImpl实例,并调用了它的work和eat方法。
由于Go的接口是隐式的,WorkerImpl不需要显式声明它实现了Worker接口,只要它实现了接口中定义的所有方法即可。
但是上面的接口Worker不满足ISP原则,因为work和eat是不同的职责。可以通过拆分接口来遵循ISP:
go
type WorkAble interface {
work()
}
type EatAble interface {
eat()
}
type WorkerImpl struct {}
func (wi *WorkerImpl) work() {//working}
func (wi *WorkerImpl) eat() {//eating}
在这个Go代码示例中:
Workable 接口定义了一个 work 方法。 Eatable 接口定义了一个 eat 方法。
WorkerImpl 结构体实现了 Workable 和 Eatable 接口,通过定义接收者为 *WorkerImpl 的方法来实现 work 和 eat 方法。
在 main 函数中,可以创建了一个 WorkerImpl 实例,并将其分别作为 Workable 和 Eatable 接口的实例来调用 work 和 eat 方法。这展示了Go语言中多接口实现的能力。
- 接口隔离原则的优势:
提高代码的灵活性和可维护性。
使接口更加清晰和易于理解。
在敏捷开发中允许团队在不同模块间并行开发。提高代码重用性和测试效率。
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
-
定义:高层模块不应依赖低层模块,两者都应依赖其抽象;抽象不应依赖细节,细节应依赖抽象。
-
使用场景:
当高层模块直接依赖于低层模块时,导致耦合度高且难以测试和扩展。
-
反例实例:
gotype LightBulb struct {} func (lb *LightBulb) turnOn() {//turn on light} func (lb *LightBulb) turnOff() {//turn off light} type Switch struct { LightBulb } func (s *Switch) Switch(lb LightBulb) {s.LightBulb = lb} func (s *Switch) operate {// Operate light bulb}
上面的代码违反了DIP,因为Switch依赖于具体实现LightBulb。可以通过依赖注入来遵循DIP:
go
// LightBulb 接口定义了灯泡的操作
type LightBulb interface {
turnOn()
turnOff()
}
// LightBulbImpl 是一个实现了 LightBulb 接口的结构体
type LightBulbImpl struct{}
func (l *LightBulbImpl) turnOn() {
fmt.Println("Light bulb turned on")
}
func (l *LightBulbImpl) turnOff() {
fmt.Println("Light bulb turned off")
}
// Switch 结构体包含一个 LightBulb 接口类型的字段
type Switch struct {
lightBulb LightBulb
}
// NewSwitch 函数创建并返回一个 *Switch 实例
func NewSwitch(lightBulb LightBulb) *Switch {
return &Switch{lightBulb: lightBulb}
}
// Operate 方法操作灯泡
func (s *Switch) Operate() {
fmt.Println("Operating light bulb")
s.lightBulb.turnOn() // 例如,这里我们让灯泡亮起来
}
func main() {
// 创建一个 LightBulbImpl 实例
lightBulb := &LightBulbImpl{}
// 创建一个 Switch 实例,传入 lightBulb
switcher := NewSwitch(lightBulb)
// 操作开关
switcher.Operate()
}
- 依赖倒置的优势:
提高系统的灵活性和可扩展性。
使高层模块不依赖于具体实现,更易于修改和测试。
在敏捷开发中支持持续集成和持续交付。促进模块的独立开发和测试,提高开发效率。
6 总结
SOLID原则是面向对象设计中的五个基本原则, 这些原则有助于创建更健壮、灵活和可维护的软件系统,提高代码的清晰度、可测试性和开发效率。
特别在敏捷开发环境下,遵循SOLID原则可以支持持续改进和快速响应需求变化,促进代码重用和测试,提高系统的灵活性和可扩展性。在不断变化的需求和快速迭代的开发环境中, 通过遵循这些原则,开发团队可以更好地应对变化和交付高质量的软件产品。