设计原则:KISS、DRY、LOD 原则

267 阅读8分钟

除了了人尽皆知的 SOLID 原则之外,其实还有其他一些有用且很受大家认可的设计原则。本节课就来介绍这些设计原则。主要包括以下 3 种设计原则:

  • KISS 原则;
  • DRY 原则;
  • LOD 原则。

KISS 原则

KISS 原则(Keep It Simple, Stupid)是软件开发中的重要原则,强调在设计和实现软件系统时应该保持简单和直观,避免过度复杂和不必要的设计。

KISS 原则的英文描述有好几个版本,比如下面这几个。

  • Keep It Simple and Stupid;
  • Keep It Short and Simple;
  • Keep It Simple and Straightforward。

不过,仔细看你就会发现,它们要表达的意思其实差不多,翻译成中文就是:尽量保持简单。

KISS 原则是保证代码可读性可维护性的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。

对于如何写出满足 KISS 原则的代码,有下面几条指导原则:

  • 不要使用同事可能不懂的技术来实现代码
  • 不要重复造轮子,要善于使用已经有的工具类库
  • 不要过度优化

下面是一个使用 KISS 原则设计的简单计算器程序的示例:

package main

import "fmt"

// Calculator 定义简单的计算器结构
type Calculator struct{}

// Add 方法用于相加两个数
func (c Calculator) Add(a, b int) int {
    return a + b
}

// Subtract 方法用于相减两个数
func (c Calculator) Subtract(a, b int) int {
    return a - b
}

func main() {
    calculator := Calculator{}

    // 计算 5 + 3
    result1 := calculator.Add(5, 3)
    fmt.Println("5 + 3 =", result1)

    // 计算 8 - 2
    result2 := calculator.Subtract(8, 2)
    fmt.Println("8 - 2 =", result2)
}

在上述示例中,我们定义了一个简单的计算器结构 Calculator,包含 AddSubtract 方法用于实现加法和减法操作。通过简单的设计和实现,这个计算器程序清晰、易懂,符合 KISS 原则的要求。

DRY 原则

DRY 原则,全称为“Don’t Repeat Yourself”,是软件开发中的重要原则之一,强调避免重复代码和功能,尽量减少系统中的冗余。DRY 原则的核心思想是任何信息在系统中应该有且仅有一个明确的表达形式,避免多处重复定义相同的信息或逻辑。

你可能会觉得 DRY 原则非常简单、非常容易应用。只要两段代码长得一样,那就是违反 DRY 原则了。真的是这样吗?答案是否定的。这是很多人对这条原则存在的误解。实际上,重复的代码不一定违反 DRY 原则,而且有些看似不重复的代码也有可能违反 DRY 原则。

通常存在三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。这三种代码重复,有的看似违反 DRY,实际上并不违反;有的看似不违反,实际上却违反了。

实现逻辑重复:

type UserAuthenticator struct{}

func (ua *UserAuthenticator) authenticate(username, password string) {
    if !ua.isValidUsername(username) {
        // ... code block 1
    }

    if !ua.isValidPassword(username) {
        // ... code block 1
    }
    // ...省略其他代码...
}

func (ua *UserAuthenticator) isValidUsername(username string) bool {}

func (ua *UserAuthenticator) isValidPassword(password string) bool {}

假设 isValidUserName() 函数和 isValidPassword() 函数代码重复,看起来明显违反 DRY 原则。为了移除重复的代码,我们对上面的代码做下重构,将 isValidUserName() 函数和 isValidPassword() 函数,合并为一个更通用的函数 isValidUserNameOrPassword()

经过重构之后,代码行数减少了,也没有重复的代码了,是不是更好了呢?答案是否定的。单从名字上看,我们就能发现,合并之后的 isValidUserNameOrPassword() 函数,负责两件事情:验证用户名和验证密码,违反了“单一职责原则”和“接口隔离原则”。

实际上,即便将两个函数合并成 isValidUserNameOrPassword(),代码仍然存在问题。因为 isValidUserName()isValidPassword() 两个函数,虽然从代码实现逻辑上看起来是重复的,但是从语义上并不重复。所谓“语义不重复”指的是:从功能上来看,这两个函数干的是完全不重复的两件事情,一个是校验用户名,另一个是校验密码。尽管在目前的设计中,两个校验逻辑是完全一样的,但如果按照第二种写法,将两个函数的合并,那就会存在潜在的问题。在未来的某一天,如果我们修改了密码的校验逻辑,那这个时候,isValidUserName()isValidPassword() 的实现逻辑就会不相同。我们就要把合并后的函数,重新拆成合并前的那两个函数。

对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。

功能语义重复:

在同一个项目代码中有下面两个函数:isValidIp() checkIfIpValid()。尽管两个函数的命名不同,实现逻辑不同,但功能是相同的,都是用来判定 IP 地址是否合法的。

func isValidIp(ipAddress string) bool {
    // ... 正则表达式判断
}

func checkIfIpValid(ipAddress string) bool {
    // ... 字符串方式判断
}

在这个例子中,尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。

代码执行重复:

type UserService struct {
    userRepo UserRepo
}

func (us *UserService) login(email, password string) {
    existed := us.userRepo.checkIfUserExisted(email, password)
    if !existed {
        // ...
    }
    user := us.userRepo.getUserByEmail(email)
}

type UserRepo struct{}

func (ur *UserRepo) checkIfUserExisted(email, password string) bool {
    if !ur.isValidEmail(email) {
        // ...
    }
}

func (ur *UserRepo) getUserByEmail(email string) User {
    if !ur.isValidEmail(email) {
        // ...
    }
}

上面这段代码,既没有逻辑重复,也没有语义重复,但仍然违反了 DRY 原则。这是因为代码中存在“执行重复”。这个问题解决起来比较简单,我们只需要将校验逻辑从 UserRepo 中移除,统一放到 UserService 中就可以了。

如何提高代码复用性?

  • 减少代码耦合;
  • 满足单一职责原则;
  • 模块化业务与非业务逻辑分离;
  • 通用代码下沉;
  • 继承、多态、抽象、封装;
  • 应用模板等设计模式。

下面是一个简单的人员管理系统示例,使用 DRY 原则来确保代码的清晰和重用性:

package main

import "fmt"

// Person 结构体表示人员信息
type Person struct {
    Name string
    Age  int
}

// PrintPersonInfo 打印人员信息
func PrintPersonInfo(p Person) {
    fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}

func main() {
    // 创建两个人员信息
    person1 := Person{Name: "Alice", Age: 30}
    person2 := Person{Name: "Bob", Age: 25}

    // 打印人员信息
    PrintPersonInfo(person1)
    PrintPersonInfo(person2)
}

在上述示例中,我们定义了一个 Person 结构体表示人员信息,以及一个 PrintPersonInfo 函数用于打印人员信息。通过将打印人员信息的逻辑封装在 PrintPersonInfo 函数中,遵循DRY原则,避免重复编写打印逻辑,提高了代码的复用性和可维护性。

LOD 原则

LOD原则(Law of Demeter),又称为最少知识原则,旨在降低对象之间的耦合度,减少系统中各部分之间的依赖关系。LOD原则强调一个对象应该对其他对象了解得越少越好,不应直接与陌生对象通信,而通过自己的成员进行操作。

迪米特法则法则强调不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。

下面是一个使用LOD原则设计的简单用户管理系统示例:

package main

import "fmt"

// UserService 用户服务,负责用户管理
type UserService struct{}

// GetUserByID 根据用户ID获取用户信息
func (us UserService) GetUserByID(id int) User {
    userRepo := UserRepository{}
    return userRepo.FindByID(id)
}

// UserRepository 用户仓库,负责用户数据维护
type UserRepository struct{}

// FindByID 根据用户ID查询用户信息
func (ur UserRepository) FindByID(id int) User {
    // 模拟从数据库中查询用户信息
    return User{id, "Alice"}
}

// User 用户结构
type User struct {
    ID   int
    Name string
}

func main() {
    userService := UserService{}

    user := userService.GetUserByID(1)
    fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}

在上述示例中,我们设计了一个简单的用户管理系统,包括 UserService 用户服务和 UserRepository 用户仓库两个部分。UserService 通过调用 UserRepository 来查询用户信息,遵循了LOD原则中只与直接的朋友通信的要求。

往期文章回顾

  • 知识星球:云原生AI实战营。10+ 高质量体系课( Go、云原生、AI Infra)、15+ 实战项目,P8 技术专家助你提高技术天花板,入大厂拿高薪;
  • 公众号:令飞编程,分享 Go、云原生、AI Infra 相关技术。回复「资料」免费下载 Go、云原生、AI 等学习资料;
  • 哔哩哔哩:令飞编程 ,分享技术、职场、面经等,并有免费直播课「云原生AI高新就业课」,大厂级项目实战到大厂面试通关;