6

一文了解函数设计的最佳实践 - 菜鸟额

 11 months ago
source link: https://www.cnblogs.com/chenjiazhan/p/17498692.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

良好设计的函数具有清晰的职责和逻辑结构,提供准确的命名和适当的参数控制。它们促进代码复用、支持团队协作,降低维护成本,并提供可测试的代码基础。通过遵循最佳实践,我们能够编写出高质量、可读性强的代码,从而提高开发效率和软件质量。下面我们将一一描述函数设计时能够遵循的最佳实践。

2. 遵循单一职责原则

遵循单一职责原则是函数设计的重要原则之一。它要求一个函数只负责完成单一的任务或功能,而不应该承担过多的责任。

通过遵循该原则,我们设计出来的函数将具有以下几个优点:

  1. 代码可读性的提高:函数只关注单一的任务或功能,使其逻辑更加清晰和简洁。这样的函数更易于阅读和理解,能够更快速地理解其作用和目的,提高代码的可读性。
  2. 函数复杂度的降低:单一职责的函数具有较小的代码量和较少的依赖关系。这使得函数的逻辑更加集中和可控,减少了函数的复杂性。在维护和修改代码时,由于函数的功能单一,我们可以更容易地定位和修复问题,降低了维护成本。
  3. 代码可测试性的提高:遵循单一职责原则的函数更容易进行单元测试。因为函数的功能单一,我们可以更精确地定义输入和期望输出,编写针对性的测试用例。这有助于提高代码的可测试性,确保函数的正确性和稳定性。

相对的,如果函数设计时没有遵循单一职责原则,此时将带来函数复杂性的增加,从而导致代码可读性的降低以及代码可测试性的下降。

下面是一个没有遵循单一职责原则的函数与一个遵循该原则的函数的对比。首先是一个未遵循该原则的代码示例:

func processData(data []int) {
    // 1. 验证数据
    
    // 2. 清理数据
    
    // 3. 分析数据
    
    // 4. 保存数据
    
    // 5. 记录日志
}

在上述示例中,processData 函数负责整个数据处理流程,包括验证数据、清理数据、分析数据、保存数据和记录日志。这个函数承担了太多的职责,导致代码逻辑复杂,可读性不高,同时如果某一个节点需要变更,此时需要考虑是否对其他部分是否有影响,代码的可维护性进一步降低。

下面我们将processData函数进行改造,使其遵循单一职责原则,从而凸显出遵循单一职责原则的好处,代码示例如下:

func processData(data []int) {
    // 1. 验证逻辑拆分到calidateData函数中
    validateData(data)
    // 2. 清理数据 拆分到cleanData函数中
    cleanedData := cleanData(data)
    // 3. 分析数据 拆分到 analyzeData 函数中
    result := analyzeData(cleanedData)
    //4. 保存数据 拆分到 saveData 函数中
    saveData(result)
    //5. 记录日志 拆分到 logData 函数中
    logData(result)
}

func validateData(data []int) {
    // 验证数据的逻辑
    // ...
}

func cleanData(data []int) []int {
    // 清理数据的逻辑
    // ...
}

func analyzeData(data []int) string {
    // 分析数据的逻辑
    // ...
}

func saveData(result string) {
    // 保存数据的逻辑
    // ...
}

func logData(result string) {
    // 记录日志的逻辑
    // ...
}

改造后的processData函数中,我们将不同的任务拆分到不同的函数中,每个函数只负责其中一部分功能。由于每个函数只需要专注于其中一项任务,代码的可读性更好,而且每个函数只负责其中一部分功能,故代码的复杂性也明显降低了,而且代码也更容易测试了。

而且由于此时每个函数只负责其中一个任务,如果其存在变更,也不会担心影响到其他部分的内容,代码的可维护性也更高了。

通过对比这两个示例,我们可以很清楚得看到,遵循单一职责函数的函数,其代码可读性更高,复杂度更低,代码可测试性更强,同时也提高了代码的可维护性。

3. 控制函数参数数量

函数在不断进行迭代过程中,函数参数往往会不断增多,此时我们在每次迭代过程中,都需要思考函数参数是否过多。通过避免函数参数过多,这能够给我们一些好处:

  1. 首先是函数更加容易使用,过多的参数会增加函数的复杂性,使函数调用时的意图不够清晰。通过控制参数数量,可以使函数的调用更加简洁和方便。
  2. 其次是函数的耦合度的降低: 过多的参数会增加函数与调用者之间的耦合度,使函数的可复用性和灵活性降低。通过封装相关参数为对象或结构体,可以减少参数的数量,从而降低函数之间的依赖关系,提高代码的灵活性和可维护性。
  3. 同时也提高了函数的扩展性,当需要对函数进行功能扩展时,过多的参数会使函数的修改变得复杂,可能需要修改大量的调用代码。而通过封装相关参数,只需修改封装对象或结构体的定义,可以更方便地扩展函数的功能,同时对现有的调用代码影响较小。
  4. 能够及时识别函数是否符合单一职责原则,当函数参数过多时,同时我们又无法将其抽取为一个结构体参数,这往往意味着函数的职责不单一。从另外一个方面,迫使我们在函数还没有堆积更多功能前,及时将其拆分为多个函数,提高代码的可维护性。

下面,我们通过一个代码示例,展示一个函数参数数量过多的例子和优化后的示例,首先是优化前的函数代码示例:

func processOrder(orderID string, customerName string, customerEmail string, shippingAddress string, billingAddress string, paymentMethod string, items []string) {
    // 处理订单的逻辑
    // ...
}

在这个示例中,函数 processOrder 的参数数量较多,包括订单ID、顾客姓名、顾客邮箱、收货地址、账单地址、支付方式和商品列表等。调用该函数时,需要传递大量的参数,使函数调用变得冗长且难以阅读。

下面,我们将processOrder的参数抽取成一个结构体,控制函数参数的数量,代码示例如下:

type Order struct {
    ID               string
    CustomerName     string
    CustomerEmail    string
    ShippingAddress  string
    BillingAddress   string
    PaymentMethod    string
    Items            []string
}

func processOrder(order Order) {
    // 处理订单的逻辑
    // ...
}

在优化后的示例中,我们将相关的订单信息封装为一个 Order 结构体。通过将参数封装为结构体,函数的参数数量大大减少,只需传递一个结构体对象即可。

这样的设计使函数调用更加简洁和易于理解,同时也提高了代码的可读性和可维护性。如果需要添加或修改订单信息的字段,只需修改结构体定义,而不需要修改调用该函数的代码,提高了代码的扩展性和灵活性。

其次,在processOrder函数参数抽取的过程中,如果发现无法将函数参数抽取为结构体的话,也能帮助我们及时识别到函数职责不单一的问题,从而能够及时将函数进行拆分,提高代码的可维护性。

因此,在函数设计迭代过程中,控制函数参数过多是非常有必要的,能够提高函数的可用性和扩展性,其次也能够帮助我们识别函数是否满足符合单一职责原则,也间接提高了代码的可维护性。

4. 函数命名要准确

函数设计时,适当的函数命名是至关重要的,它能够准确、清晰地描述函数的功能和作用。一个好的函数名能够使代码易于理解和使用,提高代码的可读性和可维护性。

相对准确的函数命名,能够明确传达函数的用途和功能,避免其他人对函数的误用。同时,也提高了代码的可读性,其他人阅读代码时,能够更加轻松得理解函数的含义和逻辑。因此,设计函数时,一个清晰准确的函数名也是至关重要的。

下面再通过一个代码的例子,展示准确清晰的函数命名,和一个含糊不清的函数命名之间的区别:

// 不合适的函数命名示例
func F(a, b int) int {
    // 函数体的逻辑
    // ...
}

// 适当的函数命名示例
func Add(a, b int) int {
    // 将两个数相加
    return a + b
}

在上述示例中,第一个函数命名为 F,没有提供足够的信息来描述函数的功能和用途。这样的函数命名使其他人难以理解函数的目的和作用。

而在第二个函数中,我们将函数命名为 Add,清晰地描述了函数的功能,即将两个数相加。这样的命名使得代码更易于理解和使用。

因此,在函数设计中,我们需要定义一个清晰和准确的函数命名,这样能够提高代码的可读性,让其他人更容易理解我们的意图。

5. 控制函数长度

在函数编写和迭代过程中,一个超过1000行的函数,一般不是一开始实现便是如此,而是在不断迭代过程中,不断往其中迭代功能,才最终出现了这个大函数。由此造成的后果,各种业务逻辑在该函数中错综复杂,接手的同事往往难以快速理解其功能和行为。而且,在功能迭代过程中,由于各种逻辑穿插其中,此时函数将变得难以修改和维护,代码基本不具有可读性和可维护性。

因此,在代码迭代过程中,时时考虑函数的长度是至关重要的。当在迭代过程中,发现函数已经过长了,此时应该尽快通过一些手段重构该函数,避免函数最终无法维护,下面是一些可能的手段:

  1. 确保函数只负责完成单一的任务或功能,避免函数承担过多的责任。
  2. 当函数过长时,将其拆分为多个较小的函数,每个函数负责特定的功能或操作。
  3. 将长函数中的某些逻辑提取出来,形成独立的辅助函数,以减少函数的长度和复杂度。

在需求迭代过程中,我们时时关注函数的长度,当长度过长时,便适当进行重构,保持代码的可读性和可维护性。

6. 进行防御式编程

在函数编写过程中,尽量考虑各种可能的错误和异常情况,以及相应的处理策略。这能够带来一些好处:

  1. 增强程序的健壮性: 防御式编程通过对可能的错误和异常情况进行处理,它可以帮助程序更好地处理无效的输入、边界条件和异常情况,从而提高程序的健壮性和可靠性。
  2. 减少程序的崩溃和故障: 通过合理的错误处理和异常处理机制,防御式编程可以防止程序在出现错误时崩溃或产生不可预测的行为。它可以使程序在遇到问题时能够适当地处理和恢复,从而减少系统的故障和崩溃。

下面是一个对比的示例代码,展示一个进行防御式编程的代码和一个未进行防御式编程的代码示例:

// 没有防御编程的函数示例
func Divide(a, b int) int {
    return a / b
}

// 有防御编程的函数示例
func SafeDivide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

在上述示例中,第一个函数 Divide 没有进行错误处理,如果除数 b 为零,会导致运行时发生除以零的错误,可能导致程序异常终止。而第二个函数 SafeDivide 在执行除法之前,先进行了错误检查,如果除数 b 为零,则返回一个自定义的错误,避免了程序崩溃。

因此,我们在函数编写过程中,尽量考虑各种可能的错误和异常情况,对其进行处理,保证函数的健壮性。

在这篇文章中,我们总结了几个函数设计的最佳实践,如遵循单一职责原则,控制函数参数数量,函数命名要清晰准确等,通过遵循这些原则,能够让我们设计出来高质量、可读性强的代码,同时也具有更强的可维护性。

但是也需要注意的是,函数一开始设计时总是相对比较完美的,只是在不断迭代中,不断堆积代码,最终代码冗长,复杂,各种逻辑穿插其中,使得维护起来越发困难。因此,我们更多的应该是在迭代过程中,多考虑函数设计是否违反了我们这里提出的原则,能在一开始就识别到代码的坏味道,从而避免最终演变成难以维护和迭代的函数。

基于此,我们完成了对函数设计最佳实践的介绍,希望对你有所帮助。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK