Bootstrap

第 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 (依赖倒置原则)
单一职责原则

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

示例代码

// 不好的例子
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 { /* ... */ }
开闭原则

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

示例代码

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
}
里氏替换原则

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

示例代码

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()
}
接口隔离原则

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

示例代码

// 不好的例子
type Printer interface {
    Print()
    Scan()
}

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

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

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

示例代码

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 原则

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

示例代码

// 不好的例子
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 原则

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

示例代码

// 不好的例子
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) - 进一步的例子

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

不好的例子

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 结构体不仅管理订单项,还计算总价和处理支付。这违反了单一职责原则。

更好的例子

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)- 进一步的例子

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

不好的例子

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
}

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

更好的例子

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原则强调减少重复代码。例如,在多个地方使用相同的配置加载逻辑。

不好的例子

func loadConfig1() Config {
    // 配置加载逻辑
}

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

更好的例子

func loadConfig() Config {
    // 统一的配置加载逻辑
}

func loadConfig1() Config {
    return loadConfig()
}

func loadConfig2() Config {
    return loadConfig()
}

KISS 原则 - 更多实践

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

不好的例子

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

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

更好的例子

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

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

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

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

YAGNI 原则

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

不好的例子

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

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

更好的例子

type User struct {
    ID   int
    Name string
}

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

Law of Demeter(迪米特法则)

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

不好的例子

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)
}

更好的例子

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 的依赖。

代码重构

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

原始代码

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
}

重构后的代码

// 定义折扣策略接口
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语言中的应用案例。遵循这些原则和最佳实践可以帮助我们编写出更加健壮、灵活和易于维护的代码。希望这些信息对您有所帮助!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;