学习笔记:面向对象设计原则
本文最后更新于241 天前,其中的信息可能已经过时,如有错误请发送邮件到lvlvko233@qq.com

GPT省流

单一职责原则(Single Responsibility Principle)

👉 该原则规定一个类应该只有一个发生变化的原因,也就是说一个类应该只负责一项职责。这样做的目的是高内聚🔑,低耦合⛓️。

💡 当一个类承担太多职责时,其就很容易被修改,从而降低了代码的可维护性和可扩展性。

✅ 使用场景:假设我们有一个处理文件的程序,根据该原则,我们应该将读取文件、解析文件内容以及发送错误报告等功能分离到不同的类中。这样可以提高代码的可维护性和可扩展性。

🔍 总结:单一职责原则强调一个类应该只负责一个职责,这样可以提高内聚性,降低耦合度,从而提高代码的可维护性和可扩展性。

开闭原则(Open-Closed Principle)

👉 该原则规定软件实体应该对扩展开放,对修改关闭。也就是说,当需要扩展系统功能时,应该新增代码,而不是修改原有代码。这样可以提高代码的可维护性和可复用性。

💡 通过这种设计,我们可以在不修改原有代码的情况下,添加新的功能,从而避免了潜在的代码破坏风险。

✅ 使用场景:假设我们有一个计算几何图形面积的程序,根据该原则,我们应该设计一种可扩展的方式,以便在不修改原有代码的情况下,添加新的几何图形类型。

🔍 总结:开闭原则强调软件实体应该对扩展开放,对修改关闭。这样可以提高代码的可维护性和可复用性,避免了潜在的代码破坏风险。

里氏代换原则(Liskov Substitution Principle)

👉 该原则规定,如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,替换o1能使得程序的行为没有变化,那么类型T2就被称为是类型T1的子类型。

💡 这个原则的核心思想是:子类应该可以替换基类的使用,而不会破坏程序的正确性。

✅ 使用场景:假设我们有一个表示几何图形的基类Shape,以及两个子类Rectangle和Square。根据这个原则,我们应该能够使用Square对象替换Rectangle对象,而不会破坏程序的正确性。

🔍 总结:里氏代换原则强调子类应该可以替换基类的使用,而不会破坏程序的正确性。这样可以提高代码的可扩展性和可维护性。

接口隔离原则(Interface Segregation Principle)

👉 该原则规定,客户端不应该被强制依赖它不需要使用的接口。换句话说,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。

💡 这样做可以避免客户端被强制依赖不需要的方法,提高了代码的灵活性和可维护性。

✅ 使用场景:假设我们有一个表示工作人员的接口Employee。不同类型的工作人员可能需要实现不同的方法。根据该原则,我们应该将这些方法划分为多个专门的接口,而不是将它们捆绑在一个大的接口中。

🔍 总结:接口隔离原则规定一个接口应该只定义一组相关的方法,客户端不应该被强制依赖它不需要使用的接口。这样可以提高代码的灵活性和可维护性。

合成复用原则(Composite Reuse Principle)

👉 该原则规定,应该优先使用对象组合而不是类继承来实现代码复用。通过组合可以获得比继承更好的可扩展性。

💡 继承可能会引入一些不需要的方法和属性,增加了代码的复杂性和不必要的耦合。

✅ 使用场景:假设我们正在开发一个汽车模拟系统,我们需要表示不同类型的汽车,每种汽车都有不同的引擎和变速箱。根据该原则,我们应该使用组合而不是继承来实现这个系统。

🔍 总结:合成复用原则强调优先使用对象组合而不是类继承来实现代码复用。这样可以获得更好的可扩展性,避免继承可能引入的不必要的复杂性和耦合。

迪米特法则(Law of Demeter)

👉 迪米特法则,也被称为最少知识原则,规定一个对象应该对其他对象保持最少的了解。换句话说,一个类应该只与它的直接朋友交谈,而不与陌生人交谈。

💡 这样做可以减少类之间的耦合,提高系统的模块化程度和可维护性。

✅ 使用场景:假设我们有一个订单处理系统,包含客户、订单和支付处理器。按照该原则,订单处理应该只与直接相关的对象交互,而不需要知道支付处理的具体细节。

🔍 总结:迪米特法则强调一个对象应该对其他对象保持最少的了解,只与直接相关的对象交互。这样可以减少类之间的耦合,提高系统的模块化程度和可维护性。

总的来说,这些设计原则都是为了提高代码的可维护性、可扩展性和可复用性,从而提高软件系统的整体质量。通过遵循这些原则,我们可以编写出更加健壮、灵活和易于维护的代码。同时,在实际开发中,我们还需要结合具体的场景和需求,权衡利弊,选择最佳的设计方式。


单一职责原则 (Single Responsibility Principle)

单一职责原则规定,一个类应该只有一个发生变化的原因,也就是说一个类应该只负责一项职责。这样做的目的是高内聚,低耦合。当一个类承担太多职责时,其就很容易被修改。

使用场景

假设我们有一个处理文件的程序,它包含了读取文件、解析文件内容以及发送错误报告的功能。根据单一职责原则,我们应该将这些功能分离到不同的类中。

// FileReader 负责读取文件
type FileReader struct {
    // ...
}

func (f *FileReader) Read(path string) ([]byte, error) {
    // 读取文件并返回内容
}

// FileParser 负责解析文件内容
type FileParser struct {
    // ...
}

func (p *FileParser) Parse(data []byte) (result interface{}, err error) {
    // 解析文件内容
}

// ErrorReporter 负责发送错误报告
type ErrorReporter struct {
    // ...
}

func (r *ErrorReporter) Report(err error) {
    // 发送错误报告
}

通过这样的设计,当需要修改某一个职责时,只需要修改相应的类,而不会影响到其他类。这样可以提高代码的可维护性和可扩展性。

开闭原则 (Open-Closed Principle)

开闭原则规定,软件实体应该对扩展开放,对修改关闭。也就是说,当需要扩展系统功能时,应该新增代码,而不是修改原有代码。这样可以提高代码的可维护性和可复用性。

使用场景

假设我们有一个计算几何图形面积的程序,它目前只支持计算矩形和圆形的面积。根据开闭原则,我们应该设计一种可扩展的方式,以便在不修改原有代码的情况下,添加新的几何图形类型。

// Shape 接口定义了计算面积的方法
type Shape interface {
    Area() float64
}

// Rectangle 实现了 Shape 接口
type Rectangle struct {
    width, height float64
}

func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// Circle 实现了 Shape 接口
type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

// AreaCalculator 负责计算不同图形的面积
type AreaCalculator struct {
    shapes []Shape
}

func (c *AreaCalculator) Sum() float64 {
    var sum float64
    for _, s := range c.shapes {
        sum += s.Area()
    }
    return sum
}

在这个设计中,我们定义了一个 Shape 接口,并让矩形和圆形分别实现了这个接口。当需要添加新的几何图形类型时,只需要实现 Shape 接口,而不需要修改原有代码。这样就满足了开闭原则。

里氏代换原则 (Liskov Substitution Principle)

里氏代换原则规定,如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,替换 o1 能使得程序的行为没有变化,那么类型 T2 就被称为是类型 T1 的子类型。这个原则的核心思想是:子类应该可以替换基类的使用,而不会破坏程序的正确性。

使用场景

假设我们有一个表示几何图形的基类 Shape,以及两个子类 RectangleSquare。根据里氏代换原则,我们应该能够使用 Square 对象替换 Rectangle 对象,而不会破坏程序的正确性。

// Shape 定义了几何图形的基本属性和方法
type Shape interface {
    Area() float64
    Resize(ratio float64)
}

// Rectangle 实现了 Shape 接口
type Rectangle struct {
    width, height float64
}

func (r *Rectangle) Area() float64 {
    return r.width * r.height
}

func (r *Rectangle) Resize(ratio float64) {
    r.width *= ratio
    r.height *= ratio
}

// Square 实现了 Shape 接口
type Square struct {
    side float64
}

func (s *Square) Area() float64 {
    return s.side * s.side
}

func (s *Square) Resize(ratio float64) {
    s.side *= ratio
}

// ResizeShapes 函数接受一个 Shape 切片作为参数,并调整它们的大小
func ResizeShapes(shapes []Shape, ratio float64) {
    for _, s := range shapes {
        s.Resize(ratio)
    }
}

在上面的例子中,我们定义了一个 ResizeShapes 函数,它接受一个 Shape 切片作为参数,并调用每个形状的 Resize 方法。由于 RectangleSquare 都实现了 Shape 接口,我们可以将它们作为参数传递给 ResizeShapes 函数,而不会破坏程序的正确性。这就满足了里氏代换原则。

接口隔离原则 (Interface Segregation Principle)

接口隔离原则规定,客户端不应该被强制依赖它不需要使用的接口。换句话说,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。

使用场景

假设我们有一个表示工作人员的接口 Employee。不同类型的工作人员可能需要实现不同的方法。根据接口隔离原则,我们应该将这些方法划分为多个专门的接口,而不是将它们捆绑在一个大的接口中。

// Employee 接口定义了工作人员的基本方法
type Employee interface {
    GetName() string
    GetAge() int
}

// FullTimeEmployee 接口定义了全职员工的附加方法
type FullTimeEmployee interface {
    Employee
    GetSalary() float64
    GetBenefits() []string
}

// Contractor 接口定义了合同工的附加方法
type Contractor interface {
    Employee
    GetHourlyRate() float64
    GetContractDuration() int
}

// FullTimeWorker 实现了 FullTimeEmployee 接口
type FullTimeWorker struct {
    name     string
    age      int
    salary   float64
    benefits []string
}

func (f *FullTimeWorker) GetName() string {
    return f.name
}

func (f *FullTimeWorker) GetAge() int {
    return f.age
}

func (f *FullTimeWorker) GetSalary() float64 {
    return f.salary
}

func (f *FullTimeWorker) GetBenefits() []string {
    return f.benefits
}

// Freelancer 实现了 Contractor 接口
type Freelancer struct {
    name           string
    age            int
    hourlyRate     float64
    contractDuration int
}

// 实现 Contractor 接口的其他方法...

在这个例子中,我们定义了一个基本的 Employee 接口,以及两个特定的接口 FullTimeEmployeeContractorFullTimeEmployee 接口继承了 Employee 接口,并添加了一些全职员工特有的方法,而 Contractor 接口则添加了一些合同工特有的方法。通过这种设计,不同类型的工作人员只需要实现与自己相关的接口,而不会被强制实现不需要的方法,从而满足了接口隔离原则。

合成复用原则 (Composite Reuse Principle)

合成复用原则规定,应该优先使用对象组合而不是类继承来实现代码复用。通过组合可以获得比继承更好的可扩展性。

使用场景

假设我们正在开发一个汽车模拟系统,我们需要表示不同类型的汽车,每种汽车都有不同的引擎和变速箱。根据合成复用原则,我们应该使用组合而不是继承来实现这个系统。

// Engine 接口定义了引擎的基本方法
type Engine interface {
    Start()
    Stop()
}

// Transmission 接口定义了变速箱的基本方法
type Transmission interface {
    ChangeGear(gear int)
}

// Car 结构体使用组合来包含引擎和变速箱
type Car struct {
    engine       Engine
    transmission Transmission
}

func (c *Car) Start() {
    c.engine.Start()
}

func (c *Car) Stop() {
    c.engine.Stop()
}

func (c *Car) ChangeGear(gear int) {
    c.transmission.ChangeGear(gear)
}

// GasolineEngine 实现了 Engine 接口
type GasolineEngine struct{}

func (e *GasolineEngine) Start() {
    fmt.Println("Gasoline engine started")
}

func (e *GasolineEngine) Stop() {
    fmt.Println("Gasoline engine stopped")
}

// ElectricEngine 实现了 Engine 接口
type ElectricEngine struct{}

func (e *ElectricEngine) Start() {
    fmt.Println("Electric engine started")
}

func (e *ElectricEngine) Stop() {
    fmt.Println("Electric engine stopped")
}

// AutomaticTransmission 实现了 Transmission 接口
type AutomaticTransmission struct{}

func (t *AutomaticTransmission) ChangeGear(gear int) {
    fmt.Printf("Automatic transmission changed to gear %d\n", gear)
}

// 使用示例
func main() {
    gasolineCar := &Car{
        engine:       &GasolineEngine{},
        transmission: &AutomaticTransmission{},
    }

    electricCar := &Car{
        engine:       &ElectricEngine{},
        transmission: &AutomaticTransmission{},
    }

    gasolineCar.Start()
    gasolineCar.ChangeGear(3)
    gasolineCar.Stop()

    electricCar.Start()
    electricCar.ChangeGear(2)
    electricCar.Stop()
}

在这个例子中,我们使用组合而不是继承来构建汽车系统。Car 结构体包含了 EngineTransmission 接口,而不是继承它们。这样,我们可以轻松地创建不同类型的汽车,只需要组合不同的引擎和变速箱实现即可。这种设计更加灵活,易于扩展,符合合成复用原则。

迪米特法则 (Law of Demeter)

迪米特法则,也被称为最少知识原则,规定一个对象应该对其他对象保持最少的了解。换句话说,一个类应该只与它的直接朋友交谈,而不与陌生人交谈。

使用场景

假设我们有一个订单处理系统,包含客户、订单和支付处理器。按照迪米特法则,订单处理应该只与直接相关的对象交互。

// Customer 表示客户
type Customer struct {
    Name string
}

// Order 表示订单
type Order struct {
    Items     []string
    TotalCost float64
}

// PaymentProcessor 处理支付
type PaymentProcessor struct{}

func (pp *PaymentProcessor) ProcessPayment(amount float64) error {
    // 处理支付逻辑
    fmt.Printf("Processing payment of $%.2f\n", amount)
    return nil
}

// OrderProcessor 处理订单
type OrderProcessor struct {
    paymentProcessor *PaymentProcessor
}

func (op *OrderProcessor) ProcessOrder(customer *Customer, order *Order) error {
    fmt.Printf("Processing order for %s\n", customer.Name)
    
    // OrderProcessor 只与 PaymentProcessor 直接交互
    return op.paymentProcessor.ProcessPayment(order.TotalCost)
}

// 使用示例
func main() {
    customer := &Customer{Name: "John Doe"}
    order := &Order{
        Items:     []string{"Book", "Pen"},
        TotalCost: 25.99,
    }

    paymentProcessor := &PaymentProcessor{}
    orderProcessor := &OrderProcessor{paymentProcessor: paymentProcessor}

    err := orderProcessor.ProcessOrder(customer, order)
    if err != nil {
        fmt.Println("Error processing order:", err)
    } else {
        fmt.Println("Order processed successfully")
    }
}

在这个例子中,OrderProcessor 只与 CustomerOrderPaymentProcessor 直接交互。它不需要知道支付处理的具体细节,而是将这个责任委托给了 PaymentProcessor。这种设计减少了类之间的耦合,提高了系统的模块化程度,符合迪米特法则的要求。

总结

本篇文章主要介绍了几种面向对象设计的核心思想。再次回顾一下上面提到的设计模式:

  1. 🔍 单一职责原则指出,一个类应该只有一个发生变化的原因,即只负责一个职责,这有助于提高代码的可维护性和可扩展性。

  2. 🔓 开闭原则要求软件实体对扩展开放,对修改关闭,新增功能时应该添加新代码而不是修改原有代码。这可以提高代码的复用性。

  3. 👩‍🎓 里氏代换原则要求子类可以替换基类,不会破坏程序的正确性。这有助于建立健壮的继承体系。

  4. 🧱 接口隔离原则指出,一个接口应该只定义一组相关的方法,而不应该将不相关的方法捆绑在一起。这有助于降低类之间的耦合度。

  5. 🔨 合成复用原则优先使用对象组合而不是类继承来实现代码复用,这可以获得更好的可扩展性。

  6. 🤝 迪米特法则要求一个对象应该对其他对象保持最少的了解,只与直接相关的对象交互,这有助于提高模块化程度。

后面的设计模式主要都是这几种设计原则的体现,但是,不存在满足所有原则的设计模式,更多是多种原则的组合与取舍。

感谢阅读~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇