使用go实现面向对象的SOILD原则

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)

  • 定义:一个类只应该有一个引起变化的原因,即一个类只负责一项职责。

  • 使用场景:

当一个类承担多种职责时,这些职责可能会相互耦合,导致类的修改变得复杂和容易出错。 将不同的职责分离到不同的类中,提高代码的可读性和可维护性。

  • 实例反例:

    go 复制代码
      type User struct {}
    
      func (u *User) saveUserToDatabase() {
              // Save user to database
      }
      func (u *User) generateUserReport() {
              // generate user report
      }   

上面的类User违反了SRP,因为它既负责数据存储又负责报告生成。可以通过拆分成两个类来遵循SRP:

  • SRP正例

    go 复制代码
       type 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)

  • 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

  • 使用场景:

当需要为现有系统添加新功能时,不应通过修改现有代码来实现,而是通过扩展原有代码来实现。

  • 反例实例:

    go 复制代码
       type 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)

  • 定义:子类必须能够替换其基类,并且功能不受影响。

  • 使用场景:

当使用多态时,确保子类对象能够完全替代父类对象。

  • 反例实例:

    go 复制代码
      type 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)

  • 定义:

不应强迫客户端依赖它们不使用的接口,即应为客户端提供小而专用的接口,而不是大而通用的接口。

  • 使用场景:

当一个接口承担了过多职责时,将其拆分为多个更具体的接口。

  • 反例实例:

    scss 复制代码
          type 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)

  • 定义:高层模块不应依赖低层模块,两者都应依赖其抽象;抽象不应依赖细节,细节应依赖抽象。

  • 使用场景:

当高层模块直接依赖于低层模块时,导致耦合度高且难以测试和扩展。

  • 反例实例:

    go 复制代码
      type 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原则可以支持持续改进和快速响应需求变化,促进代码重用和测试,提高系统的灵活性和可扩展性。在不断变化的需求和快速迭代的开发环境中, 通过遵循这些原则,开发团队可以更好地应对变化和交付高质量的软件产品。

相关推荐
m0_6312704027 分钟前
标准C++(二)
开发语言·c++·算法
沫刃起30 分钟前
Codeforces Round 972 (Div. 2) C. Lazy Narek
数据结构·c++·算法
爱coding的橙子35 分钟前
CCF-CSP认证考试准备第十五天 202303-3 LDAP
算法
QXH2000002 小时前
Leetcode—环形链表||
c语言·数据结构·算法·leetcode·链表
小灰灰爱代码3 小时前
C++——判断year是不是闰年。
数据结构·c++·算法
东城绝神3 小时前
《Linux运维总结:基于ARM64+X86_64架构CPU使用docker-compose一键离线部署mongodb 7.0.14容器版副本集群》
linux·运维·mongodb·架构
小灰灰爱代码3 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数数的情况),用函数重载方法。
数据结构·c++·算法
爱coding的橙子4 小时前
CCF-CSP认证考试准备第十七天
数据结构·c++·算法
常某某的好奇心4 小时前
56 - I. 数组中数字出现的次数
算法
hungry12345 小时前
CF EDU 169
算法