面向对象设计中的单一原则

463 阅读5分钟

单一职责原则的定义

单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中最基本、最重要的原则之一。它的定义如下:

“一个类应该只有一个引起它变化的原因” 或者 “一个类或者模块只负责完成一个职责”。

简单来说,SRP 原则就是要求一个类或模块只负责一项职责,不要存在多个引起类变化的原因。也就是说,一个类或模块应该只有一个单一的功能或职责。如果一个类或模块存在多种职责,那么它的设计就是存在缺陷的。

为什么要遵循 SRP 原则?

遵循 SRP 原则有以下几个好处:

  1. 更容易维护和管理。当一个类或模块只有一个职责时,它的代码更加清晰明了,更容易阅读、修改和测试,降低了代码维护成本。
  2. 更好的重用性。单一职责的类或模块更容易被其他代码重用,因为它的功能更为清晰,更加独立。
  3. 更好的扩展性。单一职责的类或模块更容易扩展,增加新的功能时不会对原有功能造成影响。

下面通过一个简单的 Golang 实战案例来说明 SRP 原则的应用。

假设我们正在为一个电商网站编写购物车的代码,购物车可以添加、修改、删除商品,我们先来看一下不遵守 SRP 原则的代码:

type Cart struct {
    items []*Item
}

func (c *Cart) AddItem(i *Item) {
    c.items = append(c.items, i)
}

func (c *Cart) RemoveItem(i *Item) {
    for j, item := range c.items {
        if item.ID == i.ID {
            c.items = append(c.items[:j], c.items[j+1:]...)
            break
        }
    }
}

func (c *Cart) UpdateItem(i *Item) {
    for _, item := range c.items {
        if item.ID == i.ID {
            item.Quantity = i.Quantity
            break
        }
    }
}

func (c *Cart) Checkout() float64 {
    var total float64
    for _, item := range c.items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}

上面的代码实现了购物车的添加、修改、删除商品以及结算的操作。但是这个 Cart 结构体并不只有一个职责,它是既负责购物车的信息管理,又负责结算函数的计算。

这样一来,如果计算函数的算法改变了,那么 Cart 结构体也需要跟着变化。如果购物车的信息管理和结算函数的职责分离,这种问题就会得到解决。

再来看一下使用了“单一原则”设计后的代码:

type Cart struct {
    items []*Item
}

func (c *Cart) AddItem(i *Item) {
    c.items = append(c.items, i)
}

func (c *Cart) RemoveItem(i *Item) {
    for j, item := range c.items {
        if item.ID == i.ID {
            c.items = append(c.items[:j], c.items[j+1:]...)
            break
        }
    }
}

func (c *Cart) UpdateItem(i *Item) {
    for _, item := range c.items {
        if item.ID == i.ID {
            item.Quantity = i.Quantity
            break
        }
    }
}

type Checkout struct {
    cart *Cart
}

func NewCheckout(c *Cart) *Checkout {
    return &Checkout{c}
}

func (co *Checkout) Calculate() float64 {
    var total float64
    for _, item := range co.cart.items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}

上面的代码将计算函数的职责分离出去,放到结算类 Checkout 中。这样做的好处是:

  1. Cart 结构体只负责购物车的信息管理,职责划分更为清晰;
  2. Checkout 结构体只负责计算结账金额,职责也更为清晰;
  3. 两个结构体的代码都更容易维护和重用。

怎样判断类的职责是否足够单一?

说到判断一个类的职责是否足够单一,其实并没有一个非常明确的、可以量化的标准答案。但是设计原则也不能含糊不清、模棱两可。

在面向对象编程中,一个类应该专注于实现一个明确的职责,并且尽可能接近于只专注于执行这个职责。这样做有助于使代码更加清晰、易于维护和扩展。

下面是一些可以帮助你判断一个类的职责是否单一的方法:

  1. 职责影响范围

如果一个类有多个职责,那么当一个职责发生变化时,可能会影响其他职责的实现。这样做会导致代码不稳定,难以维护和扩展。因此,通过单一职责原则,我们可以确保每个类只专注于一件事情。

  1. 类是否具有高内聚性

如果一个类具有高内聚性,说明该类的所有成员都是为了实现同一个目标而存在的。如果一个类具有低内聚性,说明该类的成员可能难以组织在一起,或者存在很多不相关的操作。因此,通过问自己这个类是否具有高内聚性,我们可以判断这个类的职责是否单一。

  1. 类是否具有低耦合性

一个类应该尽可能少地依赖其他类或模块。如果一个类具有高耦合性,那么当其他类或模块发生变化时,可能会影响到该类的实现。因此,通过问自己这个类是否具有低耦合性,我们可以判断这个类的职责是否单一。

  1. 接口是否清晰

一个类的接口应该是清晰、简单明了的。如果一个类具有多个职责,那么它的接口可能会变得复杂难懂,难以维护和使用。因此,通过观察这个类的接口是否清晰,我们可以判断这个类的职责是否单一。

  1. 类中的代码行数

如果一个类中代码、函数或属性过多,会影响代码的可读性和可维护性,就需要考虑对类进行拆分;

总之,一个类的职责是否单一是很重要的,在设计和实现过程中需要认真思考和把握。通过上述的方法,可以帮助开发者更好地判断一个类的职责是否单一,并提高代码的质量和维护性。

总结

总之,SRP 是软件开发中非常重要的一个原则,遵循 SRP 可以让代码更加容易维护、管理、复用和扩展。

在实际开发中,我们要时刻关注每个类或模块的职责是否单一,遵循 SRP 原则是提高代码质量的非常有效的方式。