第 41 章 - Go语言 软件工程原则

在软件工程中,有一些广泛接受的原则和最佳实践,它们帮助开发者构建更易于维护、扩展和理解的代码。本章将介绍几个重要的原则:SOLID、DRY(Don't Repeat Yourself)、KISS(Keep It Simple, Stupid)等,并通过Go语言的例子来展示如何应用这些原则。

SOLID 原则

SOLID 是五个面向对象设计原则的首字母缩写,它包括:

  • Single Responsibility Principle (单一职责原则)
  • Open/Closed Principle (开闭原则)
  • Liskov Substitution Principle (里氏替换原则)
  • Interface Segregation Principle (接口隔离原则)
  • Dependency Inversion Principle (依赖倒置原则)
单一职责原则

一个类应该只有一个引起它变化的原因。也就是说,一个类或模块应该负责一项功能,而不是多项功能。

示例代码

go 复制代码
// 不好的例子
type User struct {
    ID   int
    Name string
}

func (u *User) Save() error { /* ... */ }
func (u *User) Validate() bool { /* ... */ }

// 更好的例子
type UserRepository interface {
    Save(user *User) error
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) Validate(user *User) bool { /* ... */ }
开闭原则

软件实体(类、模块、函数等)应该是开放扩展的,但对修改是封闭的。这意味着可以通过添加新代码来扩展行为,而不需要修改现有的代码。

示例代码

go 复制代码
type DiscountCalculator interface {
    Calculate(price float64) float64
}

type BasicDiscount struct{}

func (b *BasicDiscount) Calculate(price float64) float64 {
    return price * 0.9 // 10% discount
}

// 扩展新的折扣类型
type SpecialDiscount struct{}

func (s *SpecialDiscount) Calculate(price float64) float64 {
    return price * 0.85 // 15% discount
}
里氏替换原则

子类型必须能够替代其基类型。即任何基类可以出现的地方,子类一定可以出现。

示例代码

go 复制代码
type Bird interface {
    Fly()
}

type Duck struct{}

func (d *Duck) Fly() {
    fmt.Println("Duck is flying")
}

// 遵循里氏替换原则
type FlyingBird struct {
    Bird
}

func (f *FlyingBird) Fly() {
    f.Bird.Fly()
}
接口隔离原则

不应该强迫客户端依赖于它们不使用的方法。应当创建小的、具体的接口,而不是大的、通用的接口。

示例代码

go 复制代码
// 不好的例子
type Printer interface {
    Print()
    Scan()
}

// 更好的例子
type PrinterOnly interface {
    Print()
}

type ScannerOnly interface {
    Scan()
}
依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

示例代码

go 复制代码
type Reader interface {
    Read(p []byte) (n int, err error)
}

type FileReader struct{}

func (r *FileReader) Read(p []byte) (int, error) {
    // 实现读取文件逻辑
    return 0, nil
}

type Service struct {
    reader Reader
}

func (s *Service) ProcessData() {
    data := make([]byte, 1024)
    s.reader.Read(data)
    // 处理数据
}

DRY 原则

不要重复自己。尽量减少代码中的重复,通过抽象公共部分来提高代码的可重用性。

示例代码

go 复制代码
// 不好的例子
func calculateTotal(items []Item) float64 {
    total := 0.0
    for _, item := range items {
        if item.Type == "book" {
            total += item.Price * 0.9 // 书籍打9折
        } else {
            total += item.Price
        }
    }
    return total
}

// 更好的例子
func applyDiscount(price float64, itemType string) float64 {
    if itemType == "book" {
        return price * 0.9
    }
    return price
}

func calculateTotal(items []Item) float64 {
    total := 0.0
    for _, item := range items {
        total += applyDiscount(item.Price, item.Type)
    }
    return total
}

KISS 原则

保持简单直接。避免不必要的复杂性,以最简单的方式实现功能。

示例代码

go 复制代码
// 不好的例子
func complexCalculation(a, b, c, d, e, f int) int {
    result := a + b
    if c > 0 {
        result *= c
    }
    if d > 0 && e > 0 {
        result -= (d + e)
    }
    if f > 0 {
        result /= f
    }
    return result
}

// 更简单的例子
func simpleCalculation(a, b, c, d, e, f int) int {
    sum := a + b
    product := sum * max(c, 1) // 避免除以0
    subtrahend := (d + e) * boolToInt(d>0 && e>0)
    quotient := product - subtrahend
    return quotient / max(f, 1)
}

func max(x, y int) int {
    if x > y {
        return x
    }
    return y
}

func boolToInt(b bool) int {
    if b {
        return 1
    }
    return 0
}

以上就是关于SOLID原则、DRY原则以及KISS原则的一些基本概念及其在Go语言中的应用案例。遵循这些原则可以帮助我们编写出更加健壮、灵活和易于维护的代码。

更多 SOLID 原则的应用

单一职责原则(SRP) - 进一步的例子

考虑一个更复杂的场景,比如一个订单处理系统。我们希望保持每个类或结构体只负责一项功能。

不好的例子

go 复制代码
type Order struct {
    ID     int
    Items  []Item
    Status string
}

func (o *Order) AddItem(item Item) {
    o.Items = append(o.Items, item)
}

func (o *Order) CalculateTotal() float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.Price
    }
    return total
}

func (o *Order) ProcessPayment(payment Payment) bool {
    // 处理支付逻辑
    return true
}

在这个例子中,Order 结构体不仅管理订单项,还计算总价和处理支付。这违反了单一职责原则。

更好的例子

go 复制代码
type Order struct {
    ID     int
    Items  []Item
    Status string
}

type OrderService struct {
    repo OrderRepository
}

func (s *OrderService) AddItem(order *Order, item Item) {
    order.Items = append(order.Items, item)
    s.repo.Save(order)
}

func (s *OrderService) CalculateTotal(order *Order) float64 {
    var total float64
    for _, item := range order.Items {
        total += item.Price
    }
    return total
}

// 假设有一个支付服务
type PaymentService struct{}

func (p *PaymentService) ProcessPayment(payment Payment) bool {
    // 处理支付逻辑
    return true
}

这里,Order 只是一个数据容器,而 OrderServicePaymentService 分别处理业务逻辑和支付逻辑。

开闭原则(OCP)- 进一步的例子

假设我们需要为不同的客户类型提供不同的折扣策略。

不好的例子

go 复制代码
type Customer struct {
    Type string
}

func CalculateDiscount(customer *Customer, price float64) float64 {
    if customer.Type == "Regular" {
        return price * 0.95
    } else if customer.Type == "VIP" {
        return price * 0.9
    }
    return price
}

每当我们添加新的客户类型时,都需要修改这个函数,这违反了开闭原则。

更好的例子

go 复制代码
type DiscountStrategy interface {
    Calculate(price float64) float64
}

type RegularDiscount struct{}

func (r *RegularDiscount) Calculate(price float64) float64 {
    return price * 0.95
}

type VIPDiscount struct{}

func (v *VIPDiscount) Calculate(price float64) float64 {
    return price * 0.9
}

type Customer struct {
    Type             string
    DiscountStrategy DiscountStrategy
}

func NewCustomer(typ string) *Customer {
    switch typ {
    case "Regular":
        return &Customer{Type: typ, DiscountStrategy: &RegularDiscount{}}
    case "VIP":
        return &Customer{Type: typ, DiscountStrategy: &VIPDiscount{}}
    default:
        return &Customer{Type: typ, DiscountStrategy: nil}
    }
}

func (c *Customer) GetDiscountedPrice(price float64) float64 {
    if c.DiscountStrategy != nil {
        return c.DiscountStrategy.Calculate(price)
    }
    return price
}

这种方式允许我们轻松地添加新的折扣策略,而无需修改现有的代码。

DRY 原则 - 更多实践

DRY原则强调减少重复代码。例如,在多个地方使用相同的配置加载逻辑。

不好的例子

go 复制代码
func loadConfig1() Config {
    // 配置加载逻辑
}

func loadConfig2() Config {
    // 相同的配置加载逻辑
}

更好的例子

go 复制代码
func loadConfig() Config {
    // 统一的配置加载逻辑
}

func loadConfig1() Config {
    return loadConfig()
}

func loadConfig2() Config {
    return loadConfig()
}

KISS 原则 - 更多实践

KISS原则鼓励保持简单。例如,避免不必要的复杂性。

不好的例子

go 复制代码
func complexLogic(a, b, c, d, e, f, g, h int) int {
    // 复杂的条件判断和计算
    // ...
}

func simpleLogic(a, b int) int {
    return a + b
}

更好的例子

go 复制代码
func simpleLogic(a, b int) int {
    return a + b
}

通过简化逻辑,代码变得更容易理解和维护。

好的,我们可以继续探讨更多关于软件工程原则的应用,并提供进一步的Go语言示例。接下来,我们将更深入地讨论一些额外的原则和最佳实践,包括:

  • YAGNI(You Aren't Gonna Need It)原则:不要添加你认为将来可能会需要的功能。
  • Law of Demeter(迪米特法则/最少知识原则):一个对象应当对其他对象有尽可能少的了解。
  • 代码重构:改善现有代码的设计而不改变其行为。

YAGNI 原则

YAGNI原则强调只实现当前确实需要的功能,避免过早优化或添加不必要的功能,因为这些功能可能永远不会被使用。

不好的例子

go 复制代码
type User struct {
    ID   int
    Name string
    // 添加了未来可能用到但目前不需要的字段
    Email        string
    PhoneNumber  string
    Registration Date
}

func (u *User) Save() error {
    // 保存用户信息,包括未来可能用到的字段
}

更好的例子

go 复制代码
type User struct {
    ID   int
    Name string
}

func (u *User) Save() error {
    // 只保存必需的信息
}

Law of Demeter(迪米特法则)

该原则建议一个对象不应该直接访问另一个对象的属性或方法,而是应该通过自己的属性来间接访问,以减少对象之间的耦合度。

不好的例子

go 复制代码
type Order struct {
    Customer *Customer
}

type Customer struct {
    Address *Address
}

type Address struct {
    City string
}

func printCity(order *Order) {
    fmt.Println(order.Customer.Address.City)
}

更好的例子

go 复制代码
type Order struct {
    Customer *Customer
}

type Customer struct {
    Address *Address
}

type Address struct {
    City string
}

func (c *Customer) GetCity() string {
    return c.Address.City
}

func printCity(order *Order) {
    fmt.Println(order.Customer.GetCity())
}

在这个例子中,printCity 函数不再直接访问 order.Customer.Address.City,而是调用 CustomerGetCity 方法,这样就减少了 OrderAddress 的依赖。

代码重构

代码重构是一种在不改变外部行为的情况下改进代码结构的过程。它可以帮助提高代码质量、可读性和可维护性。

原始代码

go 复制代码
func processItems(items []Item) float64 {
    total := 0.0
    for _, item := range items {
        if item.Type == "book" {
            total += item.Price * 0.9
        } else if item.Type == "food" {
            total += item.Price * 0.85
        } else {
            total += item.Price
        }
    }
    return total
}

重构后的代码

go 复制代码
// 定义折扣策略接口
type DiscountStrategy interface {
    Apply(price float64) float64
}

// 实现具体的折扣策略
type BookDiscount struct{}

func (b *BookDiscount) Apply(price float64) float64 {
    return price * 0.9
}

type FoodDiscount struct{}

func (f *FoodDiscount) Apply(price float64) float64 {
    return price * 0.85
}

type NoDiscount struct{}

func (n *NoDiscount) Apply(price float64) float64 {
    return price
}

// 工厂函数根据类型返回相应的折扣策略
func getDiscountStrategy(itemType string) DiscountStrategy {
    switch itemType {
    case "book":
        return &BookDiscount{}
    case "food":
        return &FoodDiscount{}
    default:
        return &NoDiscount{}
    }
}

// 使用折扣策略计算总价
func processItems(items []Item) float64 {
    total := 0.0
    for _, item := range items {
        discount := getDiscountStrategy(item.Type)
        total += discount.Apply(item.Price)
    }
    return total
}

通过引入折扣策略模式,我们使 processItems 函数更加清晰和易于扩展。如果需要添加新的折扣类型,只需定义一个新的折扣策略并更新工厂函数即可。

以上是关于YAGNI原则、迪米特法则以及代码重构的一些基本概念及其在Go语言中的应用案例。遵循这些原则和最佳实践可以帮助我们编写出更加健壮、灵活和易于维护的代码。希望这些信息对您有所帮助!

相关推荐
梦想CAD控件1 分钟前
在线CAD开发包结构与功能说明
前端·javascript·vue.js
张拭心6 分钟前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
时光不负努力6 分钟前
typescript常用的dom 元素类型
前端·typescript
小怪点点12 分钟前
大文件切片上传
前端
子玖13 分钟前
go实现通过ip解析城市
后端·go
时光不负努力13 分钟前
TS 常用工具类型
前端·javascript·typescript
SuperEugene14 分钟前
Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比
前端·vue.js·面试
张拭心16 分钟前
Android 17 来了!新特性介绍与适配建议
android·前端
Java不加班20 分钟前
Java 后端定时任务实现方案与工程化指南
后端
徐小夕21 分钟前
pxcharts-vue:一款专为 Vue3 打造的开源多维表格解决方案
前端·vue.js·github