当前位置: 首页 > news >正文

开闭原则实战:C语言中如何通过抽象接口实现可扩展的校验器设计

1. 开闭原则:软件设计的“长寿”密码

在软件开发的江湖里,我们总在追求一种境界:写出的代码既能应对未来的变化,又不会因为一点小改动就牵一发而动全身,导致整个系统“伤筋动骨”。这听起来有点像武侠小说里的“以不变应万变”,而在面向对象设计原则中,这个境界有一个响亮的名字——开闭原则。我第一次深入理解这个概念,不是在教科书里,而是在一个维护了五年的老项目里。当时,为了给一个核心模块增加一个看似简单的数据校验规则,我们不得不修改了十几个文件,引入了无数个隐藏的Bug,那感觉就像是在一个精密的钟表里硬塞进一个齿轮,结果整个表都停了。从那时起,我就明白,遵循开闭原则不是一种教条,而是让代码“活”得更久、更健康的生存法则。

开闭原则,英文叫Open-Closed Principle,简称OCP。它的定义非常精炼:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。简单来说,就是当需求变化时,我们应该通过添加新的代码来扩展系统的行为,而不是去修改那些已经工作正常、经过测试的旧代码。这个原则是敏捷软件开发乃至整个软件工程领域的基石之一,它直接关系到代码的可维护性、可复用性和稳定性。无论你是刚入行的新手,还是在架构设计上摸爬滚打多年的老手,吃透并实践OCP,都能让你的代码从“一次性用品”升级为“可长期服役的资产”。

2. 核心思想:从“USB接口”到抽象隔离

2.1 生活化类比:无处不在的“开闭”智慧

要理解开闭原则,最好的方式就是从我们身边的事物找例子。最经典的类比就是计算机的USB接口。你的笔记本电脑上有一个USB Type-A接口,这个接口的设计是“关闭”的——它的物理尺寸、电气规范、通信协议在出厂时就已经固定,你无法也不应该去修改它。但是,这个接口又是“开放”的——你可以随时插入一个新的U盘、移动硬盘、键盘或者手机数据线,来扩展电脑的功能。电脑本身不需要为了识别这个新U盘而重写主板驱动或修改操作系统内核,它只需要遵循USB接口定义好的标准协议即可工作。这个“接口稳定,实现可变”的模式,正是开闭原则的精髓。

另一个例子是家里的电源插座。插座的规格(如220V电压、两孔或三孔)是固定的、关闭修改的。但你可以随时插上新的电器,比如今天插台灯,明天插充电器,后天插电风扇。电网和插座本身不需要因为你换了电器而进行改造。这里的“插座规范”就是那个需要保持稳定的抽象层。

注意:理解“关闭修改”的对象至关重要。OCP强调的是对高层策略、核心抽象、关键接口的修改关闭,而不是禁止修改所有代码。底层具体实现、新增的业务模块,自然是需要不断编写和修改的。

2.2 抽象是应对变化的唯一武器

为什么修改旧代码是危险的?因为任何修改都可能引入新的错误,破坏原有的功能,并且需要重新进行测试,成本高昂。开闭原则通过抽象来构建一个灵活的体系,将变化隔离在具体的、可替换的实现中,而让系统的核心架构依赖于稳定的抽象。

在原文栈校验器的例子中,最初的需求是校验数字范围(0-9)。最直接(也是最脆弱)的做法,就是把minmax的判断逻辑直接写死在push函数里。但需求很快变成了“还要校验奇偶性”。如果按照老思路,我们就得去修改push函数的内部逻辑,增加一个if-else分支。如果未来要增加“质数校验”、“大于某值的校验”等等,这个函数会迅速膨胀成一个难以维护的“巨无霸”,每次修改都心惊胆战。

开闭原则指导我们换一种思路:将“校验”这个行为抽象出来。我们定义一个抽象的“校验器”(Validator),它只有一个职责:接收一个值,返回它是否合法。至于它是如何校验的(是校验范围,还是校验奇偶),那是具体校验器(如RangeValidator,OddEvenValidator)的实现细节。栈的push函数不再关心具体的校验逻辑,它只依赖于“校验器”这个抽象接口。当需要新增一种校验规则时,我们只需要创建一个新的、实现了校验器接口的类,然后将其传递给栈使用即可。栈的核心代码一行都不用改。

这里的核心转变是:从“修改代码逻辑以适应新需求”,转变为“创建新的代码模块并通过抽象接口接入系统”。

3. 实战演进:从硬编码到抽象接口的代码重构

让我们沿着原文的例子,一步步拆解如何将一段违反OCP的代码,重构为符合OCP的优雅设计。这个过程就像给代码做一次“外科手术”,目标是让它的扩展性变得更强。

3.1 初始状态:硬编码的校验逻辑

假设我们有一个简单的栈(Stack)数据结构,初始需求是只能压入0到9的数字。

// stack.h typedef struct { int data[100]; int top; } Stack; void push(Stack *s, int value);

最初的、最直接的实现可能会把校验写在push内部:

// stack.c (违反OCP的实现) void push(Stack *s, int value) { // 硬编码的校验逻辑 if (value < 0 || value > 9) { printf("Value out of range!\\n"); return; } if (s->top < 100) { s->data[s->top++] = value; } }

问题分析:校验逻辑(0-9)与栈的核心功能(压入数据)紧密耦合。任何校验规则的改变(如范围变成1-100,或增加奇偶性校验)都必须直接修改push函数。这违反了“对修改关闭”的原则。

3.2 第一步重构:参数化校验逻辑

为了将校验逻辑抽离,我们创建一个专门的校验函数,并将可变的部分(最小值、最大值、待校验值)作为参数。

// validator_util.c bool pushWithRangeCheck(Stack *s, int value, int min, int max) { if (value < min || value > max) { return false; // 校验失败 } // 栈满检查是栈自身的职责,保留 if (s->top >= 100) { return false; } push(s, value); // 调用原始的、无校验的push return true; }

进步与局限:校验逻辑从push中移出来了,这是一个好的开始。调用方现在可以这样用:pushWithRangeCheck(&myStack, 5, 0, 9)。但是,如果我们需要同时进行范围校验和奇偶校验呢?函数签名可能会变成pushWithRangeAndOddEvenCheck(..., min, max, shouldBeEven),参数列表会越来越长,组合爆炸。而且,每增加一种校验规则或组合,我们就要写一个新的函数,这本质上是另一种形式的“修改”(新增函数也是修改代码库)。

3.3 第二步重构:抽象出校验接口

这是迈向OCP的关键一步。我们意识到,无论是范围校验还是奇偶校验,它们都有一个共同的行为模式:输入一个值,输出一个布尔值(是否合法)。我们可以将这个行为抽象成一个接口。

首先,定义抽象的校验器接口。在C语言中,我们通常用函数指针和结构体来模拟接口。

// validator.h #ifndef VALIDATOR_H #define VALIDATOR_H #include <stdbool.h> // 定义校验函数指针类型 // pData: 指向具体校验器上下文数据的指针(如Range结构体) // value: 待校验的值 typedef bool (*ValidateFunc)(void *pData, int value); // 抽象的校验器结构体 typedef struct { void *pData; // 指向具体校验器数据的指针 ValidateFunc validate; // 校验函数指针 } Validator; // 一个工具函数,用于统一执行校验 bool validate(Validator *pValidator, int value); #endif
// validator.c #include "validator.h" bool validate(Validator *pValidator, int value) { if (pValidator == NULL || pValidator->validate == NULL) { // 没有校验器,默认通过 return true; } return pValidator->validate(pValidator->pData, value); }

3.4 第三步重构:实现具体校验器

现在,我们可以创建独立的具体校验器,它们互不干扰。

1. 范围校验器:

// range_validator.h #ifndef RANGE_VALIDATOR_H #define RANGE_VALIDATOR_H #include "validator.h" typedef struct { Validator base; // 基类,必须放在第一个成员位置(模拟继承) int min; int max; } RangeValidator; // 构造函数:初始化一个范围校验器 RangeValidator rangeValidatorNew(int min, int max); #endif
// range_validator.c #include "range_validator.h" // 具体的范围校验函数 static bool rangeValidate(void *pData, int value) { RangeValidator *pThis = (RangeValidator *)pData; return (value >= pThis->min) && (value <= pThis->max); } // 实现构造函数 RangeValidator rangeValidatorNew(int min, int max) { RangeValidator validator = { .base = { .pData = NULL, .validate = rangeValidate }, .min = min, .max = max }; // 关键一步:让基类的pData指向自己,这样在validate时才能拿到min/max validator.base.pData = &validator; return validator; }

2. 奇偶校验器:

// odd_even_validator.h #ifndef ODD_EVEN_VALIDATOR_H #define ODD_EVEN_VALIDATOR_H #include "validator.h" typedef struct { Validator base; bool isEven; // true表示校验偶数,false表示校验奇数 } OddEvenValidator; OddEvenValidator oddEvenValidatorNew(bool isEven); #endif
// odd_even_validator.c #include "odd_even_validator.h" static bool oddEvenValidate(void *pData, int value) { OddEvenValidator *pThis = (OddEvenValidator *)pData; if (pThis->isEven) { return (value % 2) == 0; } else { return (value % 2) != 0; } } OddEvenValidator oddEvenValidatorNew(bool isEven) { OddEvenValidator validator = { .base = { .pData = NULL, .validate = oddEvenValidate }, .isEven = isEven }; validator.base.pData = &validator; return validator; }

3.5 第四步重构:栈使用抽象校验器

最后,我们改造栈的push函数,让它接受一个抽象的Validator指针。

// stack.h (新版本) #include "validator.h" typedef struct { int data[100]; int top; } Stack; // 新的push函数,接收一个可选的校验器 bool stackPush(Stack *s, int value, Validator *pValidator);
// stack.c (符合OCP的实现) #include "stack.h" bool stackPush(Stack *s, int value, Validator *pValidator) { // 1. 先进行校验(如果提供了校验器) if (!validate(pValidator, value)) { return false; // 校验失败 } // 2. 栈满检查 if (s->top >= 100) { return false; } // 3. 执行压栈 s->data[s->top++] = value; return true; }

3.6 最终效果:符合OCP的调用方式

现在,看看我们如何以符合开闭原则的方式使用这个栈:

#include "stack.h" #include "range_validator.h" #include "odd_even_validator.h" int main() { Stack stack = { .top = 0 }; // 场景1:使用范围校验器(0-9) RangeValidator rangeValidator = rangeValidatorNew(0, 9); stackPush(&stack, 5, (Validator*)&rangeValidator); // 成功 stackPush(&stack, 15, (Validator*)&rangeValidator); // 失败 // 场景2:使用奇偶校验器(只允许偶数) OddEvenValidator evenValidator = oddEvenValidatorNew(true); stackPush(&stack, 4, (Validator*)&evenValidator); // 成功 stackPush(&stack, 7, (Validator*)&evenValidator); // 失败 // 场景3:组合校验?我们可以创建一个“组合校验器”(这是另一个扩展点,见下文) // 场景4:不需要校验 stackPush(&stack, 42, NULL); // 成功,因为校验器为NULL,validate函数默认通过 return 0; }

重构总结:经过四步重构,我们成功地将易变的“校验规则”与稳定的“栈操作”分离开。现在,stackPush函数对“修改关闭”了——无论未来需要增加多少种稀奇古怪的校验规则(负数校验、质数校验、正则匹配校验),我们都不需要再修改stack.cstack.h中的任何一行代码。我们只需要按照Validator接口实现一个新的XxxValidator,然后将其传入即可。系统对“扩展”完全开放了。

实操心得:在C语言中实现这种面向接口的编程,关键技巧在于结构体布局。让具体校验器结构体的第一个成员是基类Validator,这样可以将具体校验器的指针安全地转换为Validator*指针(这在C标准中是允许的)。这模拟了面向对象中的继承,是实现多态的基础。

4. 高级应用与模式:超越简单校验

开闭原则的应用远不止于数据校验。它是许多经典设计模式的灵魂。理解这些模式,能帮助你在更复杂的场景中自如地运用OCP。

4.1 策略模式:动态切换算法

策略模式是OCP的教科书式体现。它将一组可互换的算法封装起来,并使它们可以独立于客户端而变化。上面的校验器例子本身就是策略模式的一个应用(校验策略)。另一个常见例子是支付系统。

场景:一个电商系统需要支持多种支付方式(支付宝、微信支付、信用卡、PayPal)。最初可能只支持支付宝。

违反OCP的写法

void processPayment(Order *order, PaymentType type) { if (type == ALIPAY) { // 调用支付宝SDK复杂逻辑 } else if (type == WECHAT_PAY) { // 调用微信支付SDK复杂逻辑 } else if (type == CREDIT_CARD) { // 处理信用卡信息... } // 每增加一种支付方式,就要修改这个函数,增加一个else if分支。 }

符合OCP的写法(策略模式)

  1. 定义支付策略接口:typedef bool (*PayFunc)(Order *order, void *paymentData);
  2. 为每种支付方式实现一个具体的策略结构体(AlipayStrategy,WechatPayStrategy)。
  3. 支付处理函数只依赖接口:bool processPayment(Order *order, PaymentStrategy *strategy)
  4. 新增支付方式(如Apple Pay)时,只需新增ApplePayStrategy,无需修改processPayment函数。

4.2 装饰器模式:动态添加职责

装饰器模式允许在不改变对象自身的基础上,动态地给对象添加额外的职责。它比继承更灵活,是OCP的另一种完美实践。我们可以用它来改进之前的校验器,实现校验器的组合。

场景:我们需要对栈的压入操作同时进行范围校验奇偶校验。

思路:创建一个“组合校验器”,它内部包含一个校验器链表。当校验时,它遍历链表中的所有校验器,只有全部通过才算通过。

// composite_validator.h typedef struct { Validator base; Validator *validators[10]; // 简单起见,用数组存储,也可用链表 int count; } CompositeValidator; void compositeValidatorAdd(CompositeValidator *composite, Validator *validator); CompositeValidator compositeValidatorNew(void);
// composite_validator.c static bool compositeValidate(void *pData, int value) { CompositeValidator *pThis = (CompositeValidator *)pData; for (int i = 0; i < pThis->count; i++) { if (!validate(pThis->validators[i], value)) { return false; } } return true; } // 初始化与添加方法略...

使用方式

RangeValidator rangeValidator = rangeValidatorNew(0, 100); OddEvenValidator evenValidator = oddEvenValidatorNew(true); CompositeValidator composite = compositeValidatorNew(); compositeValidatorAdd(&composite, (Validator*)&rangeValidator); compositeValidatorAdd(&composite, (Validator*)&evenValidator); // 这个push操作要求值在0-100之间且为偶数 stackPush(&stack, 42, (Validator*)&composite); // 成功 stackPush(&stack, 43, (Validator*)&composite); // 失败(奇数) stackPush(&stack, 101, (Validator*)&composite); // 失败(超范围)

通过装饰器(组合)模式,我们实现了校验规则的灵活组合,而栈的代码依然 untouched。未来甚至可以组合出“范围校验 或 奇偶校验”的逻辑,只需新增一个OrCompositeValidator

4.3 观察者模式与插件架构

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知并自动更新。许多事件驱动系统、UI框架都基于此模式。它使得主题(被观察者)可以在不修改的情况下,通知任意数量的观察者,这是对扩展开放的典型例子。

插件架构则是OCP在系统架构层面的体现。主程序定义一套稳定的插件接口(API),具体的功能由独立的插件模块实现。当需要增加新功能时,只需开发新的插件并放入指定目录,主程序通过加载插件来扩展自身能力,无需重新编译或修改主程序代码。像VSCode、Photoshop这样的软件,其强大的扩展能力正是建立在遵循OCP的插件架构之上。

5. 实践中的权衡与常见误区

尽管开闭原则目标崇高,但在实际项目中盲目、教条地追求它,可能会带来过度设计,增加不必要的复杂度。掌握OCP的关键在于把握“度”和“时机”。

5.1 何时应用OCP?预测变化的艺术

OCP的核心价值在于应对可能发生且频率较高的变化。应用OCP需要成本(设计抽象接口、编写更多类/文件),如果为一个永远不会变化的方向进行抽象,就是过度设计。

应该应用OCP的信号

  1. 明确的变化轴:你能清晰地预见到未来哪些方面会变化。例如,在支付系统中,“支付方式”就是一个明确的变化轴;在数据导出功能中,“导出格式”(CSV, Excel, PDF)也是一个变化轴。
  2. 历史经验:在类似的项目或模块中,某个部分经常因为同类需求而修改。
  3. 需求本身具有可变性:产品经理明确表示“这个功能我们后续可能会支持多种策略”或“这个规则以后可能会调整”。

可以暂缓应用OCP的信号

  1. 需求极其稳定:例如,实现一个数学库中的标准函数(如sin, cos),其算法和接口基本不会变。
  2. 变化方向完全不明确:“未来可能要做点什么”不是应用OCP的理由。
  3. 项目早期或原型阶段:首要目标是验证想法,快速迭代。此时可以写一些“不那么干净”的代码,待核心需求稳定后再进行重构。

实操心得:我常用的一个经验法则是“三次法则”。当同一处代码因为类似的原因被修改了第三次时,就应该严肃考虑引入抽象来隔离这个变化点,使其符合OCP。前两次修改可以视为“探索需求”,第三次修改则确认了这是一个稳定的变化方向。

5.2 常见误区与反模式

  1. 误区一:为所有可能的变化都创建抽象

    • 问题:导致系统充斥着大量从未被使用的接口和抽象层,代码难以理解和维护。
    • 正确做法:基于当前需求和可预见的、高概率的变化进行抽象。遵循YAGNI原则(You Ain‘t Gonna Need It,你不需要它)。
  2. 误区二:错误的抽象层级

    • 问题:抽象要么太具体(无法容纳新的实现),要么太宽泛(失去了指导意义)。例如,抽象一个DataProcessor接口,里面只有一个process方法,但不同的处理器需要的配置参数天差地别,导致process方法的参数列表要么是void*万能指针(类型不安全),要么臃肿不堪。
    • 正确做法:抽象应该捕捉真正稳定的共性。在校验器例子中,稳定的共性是“输入int,返回bool”。如果未来需要校验字符串,那么这个抽象就需要调整。这没关系,设计是演进的。
  3. 误区三:滥用继承而不是组合

    • 问题:试图通过继承基类并重写方法来扩展行为。这看似符合OCP,但父类的修改可能会影响到所有子类(违反了里氏替换原则),而且继承关系是编译时静态绑定的,不如组合灵活。
    • 正确做法:优先使用组合和接口。就像校验器例子中,栈拥有一个校验器(组合),而不是栈某种校验器(继承)。组合更灵活,可以在运行时动态替换行为。
  4. 误区四:认为OCP意味着永不修改

    • 问题:僵化地理解“关闭修改”,当发现最初的抽象设计有缺陷时,也不敢重构。
    • 正确做法:OCP是目标,不是枷锁。如果抽象设计错了,或者出现了未曾预料到的变化方向,应该果断地重构代码,改进抽象设计。好的抽象是在一次次重构中演化出来的。

5.3 性能与复杂度的权衡

引入抽象层(接口、虚函数/函数指针)通常会带来微小的性能开销(一次间接调用)。对于绝大多数应用场景,这点开销可以忽略不计。但在性能极其敏感的领域(如高频交易、嵌入式实时系统),可能需要谨慎评估。

复杂度是另一个需要权衡的因素。一个简单的、只有两个if判断的函数,可能比引入一整套策略模式更易于理解和维护。关键在于评估“变化”发生的可能性与成本。如果这个校验规则在产品的整个生命周期内几乎不可能改变,那么最初的硬编码版本反而是最简洁、最清晰的选择。

决策框架:在决定是否应用OCP时,可以问自己几个问题:

  • 这个变化发生的可能性有多大?(高/中/低)
  • 如果变化发生,修改现有代码的成本有多高?(涉及多少模块?测试是否困难?)
  • 引入抽象设计的成本有多高?(增加了多少代码量?理解难度?)
  • 不引入抽象,未来可能的技术债务是多少?

6. 测试与维护:OCP带来的红利

遵循开闭原则的代码,在可测试性和可维护性上会带来显著的优势,这些优势在项目的长期演进中会转化为实实在在的工程效率。

6.1 提升可测试性

由于核心模块依赖于抽象,而非具体实现,我们可以非常方便地进行单元测试的隔离。

示例:测试栈的stackPush函数如果我们想测试stackPush函数本身的逻辑(栈满、栈空、指针检查等),而不想受具体校验器逻辑的干扰,我们可以轻松地创建一个“模拟校验器”。

// test_mock_validator.c #include "validator.h" // 一个总是返回true的模拟校验器,用于隔离测试 static bool alwaysTrueValidate(void *pData, int value) { (void)pData; (void)value; // 显式忽略未使用参数,避免编译器警告 return true; } static Validator alwaysTrueValidator = { .pData = NULL, .validate = alwaysTrueValidate }; // 一个总是返回false的模拟校验器 static bool alwaysFalseValidate(void *pData, int value) { (void)pData; (void)value; return false; } static Validator alwaysFalseValidator = { .pData = NULL, .validate = alwaysFalseValidate }; void testStackPush_SuccessWhenValidatorPasses() { Stack s = {0}; // 使用总是通过的校验器,测试push本身的成功路径 bool result = stackPush(&s, 10, &alwaysTrueValidator); assert(result == true); assert(s.top == 1); assert(s.data[0] == 10); } void testStackPush_FailWhenValidatorFails() { Stack s = {0}; // 使用总是失败的校验器,测试校验失败的分支 bool result = stackPush(&s, 10, &alwaysFalseValidator); assert(result == false); assert(s.top == 0); // 栈顶不应改变 }

通过模拟对象,我们可以精确地控制测试环境,验证核心模块在各种边界条件下的行为,而无需关心复杂的具体校验逻辑。这使得单元测试更纯粹、更快速、更稳定。

6.2 降低维护成本与风险

当需要修改或增加功能时,OCP带来的好处立竿见影:

  1. 修改范围局部化:要修改范围校验的边界?你只需要修改range_validator.c文件。奇偶校验的逻辑有bug?定位和修复都在odd_even_validator.c中。这极大地降低了代码修改的认知负担和回归测试的范围。
  2. 并行开发成为可能:接口定义好后,团队可以分头开发不同的具体实现。例如,A同事开发“质数校验器”,B同事开发“正则表达式校验器”,只要他们都遵循Validator接口,就可以互不干扰地工作,最后轻松集成。
  3. 降低回归缺陷风险:因为稳定模块(如stack.c)的代码不被修改,所以之前为它编写的所有测试用例都会继续通过,这给了我们巨大的信心。我们只需要为新增加的校验器编写新的测试用例即可。

6.3 应对需求变更的实战记录

我曾维护一个数据报告生成模块,最初只支持输出HTML格式。代码里到处都是fprintf(htmlFile, "...")。当需要增加PDF导出时,我们意识到这是一个经典的变化点。

我们按照OCP进行了重构:

  1. 抽象出ReportExporter接口(包含exportHeader,exportRow,exportFooter等方法)。
  2. 将原有的HTML生成逻辑移到HtmlExporter类中。
  3. 新增PdfExporter类实现接口。

重构过程大约花了两天。但在此之后的一年里,业务部门又陆续提出了导出Excel、导出CSV、导出纯文本的需求。每个新需求的实现时间都缩短到半天以内,而且从未引起过HTML或PDF导出功能的回归Bug。更棒的是,因为接口稳定,我们甚至可以为报告模块编写了一个通用的性能测试工具,它能自动用不同的导出器跑同一份大数据集,轻松对比各种导出方式的性能差异。

这个案例让我深刻体会到,前期在正确的地方(变化轴)投入设计成本,后期会获得指数级的维护收益。开闭原则不是让代码变得更复杂,而是让复杂的变化变得有序和可控。

7. 总结与个人体会

回顾开闭原则的旅程,从最初一个简单的if (value<0 || value>9)判断,演变为一个灵活可扩展的校验框架,其核心思想始终如一:找到系统中那些可能变化的部分,将其与稳定的部分隔离开来,并通过抽象来定义它们之间的契约。

我个人在实践中最大的体会是,OCP不仅仅是一个技术原则,更是一种设计思维。它要求我们在写代码时,不仅要思考如何实现当前的需求,还要以“维护者”的视角去思考:未来别人(甚至可能是六个月后的自己)会如何修改这段代码?什么样的修改会最安全、最方便?

这种思维会引导你做出许多微小的、但 collectively powerful的设计决策:

  • 你会更倾向于使用函数指针、接口、回调函数,而不是冗长的switch-case
  • 你会把配置参数(如范围校验的min/max)从函数内部提到外部,使其可配置。
  • 你会把相关的常量定义成宏或枚举,而不是散落在代码各处的“魔法数字”。
  • 你会思考:“这个模块的核心职责是什么?哪些是它必须做的,哪些是可以委托给别人做的?”

最后,记住OCP不是银弹,它是一把好用的锤子,但并非所有东西都是钉子。结合“简单设计”和“循序渐进”的原则,在确实需要应对变化的地方才挥动这把锤子。好的软件设计,是在“应对当下”和“预见未来”之间找到优雅的平衡点。开闭原则,就是帮助我们找到这个平衡点的重要罗盘之一。当你下次面对一个需求变更,本能地想去修改一个老旧函数时,不妨先停一下,问问自己:“这里是不是藏着一个需要被抽象出来的‘变化轴’?” 这个简单的停顿和思考,可能就是你的代码迈向更健壮、更可维护的第一步。

http://www.jsqmd.com/news/855449/

相关文章:

  • 人力资源系统革新,如何让企业人才资源活起来?
  • 避开OpenSim动力学仿真的坑:RRA参数设置详解与常见错误排查
  • 手把手教你用Vivado 2019.1的Block Design,为Zynq UltraScale+连接DDR4内存(附完整连线图)
  • 2026年5月热门的文字转语音方言转换软件如何选厂家推荐榜,五大主流类型厂家选择指南 - 海棠依旧大
  • 从零开始学习AI Agent的实战路线图
  • 用Sunshine搭建私人游戏串流服务器:从零到畅玩的完整指南
  • 成都高低压设备安装维保技术全解析:工业企业电力运维/成都配电系统检测/成都高低压电气检测/从选型到运维 - 优质品牌商家
  • 从 WebGPT 到 WebAgent:搜索增强型智能体演进
  • 告别Gym,拥抱Gymnasium:从Atari游戏安装到代码迁移的完整避坑指南
  • 保姆级避坑指南:从MySQL无缝切换到Kingbase数据库的完整配置与函数补全手册
  • VIL-100数据集深度解析:10种车道线类型、10大驾驶场景,你的模型训练数据够用吗?
  • AEUX插件:3步将Figma设计无缝转换为After Effects动画
  • Spring AI企业级集成:从限流策略到高可用架构
  • 实战:如何用OpenPCDet训练你自己的“树”检测模型(附完整数据集与配置文件)
  • iPad当副屏,触摸功能别浪费!实测Duet和XDisplay哪款更适合你的Windows触控工作流
  • 2026年4月可靠的真空泵企业口碑推荐,psa制氮机/节能干燥机/焊接用制氮机/空压机/干燥机,真空泵企业哪家权威 - 品牌推荐师
  • 新手入门CTF:从MoeCTF 2022的MISC题里,我总结出这5个必会的工具和技巧
  • Tokio运行时Worker线程卡死诊断与恢复实战指南
  • 别再迷信AI评分!手把手带你用Fuzz思路,拆解批改网(等作文评分系统)的四大评分维度
  • 新手避坑:在AURIX Development Studio里给变量‘安家’的三种姿势(以TC397的.bss段为例)
  • OpenISP 模块拆解 · 第7讲:去马赛克 (CFA)
  • 2026年写字楼楼梯厂家评测:地址与核心能力对比 - 优质品牌商家
  • HBuilderX调试Android 11+必看:一招删除apps文件夹,彻底解决同步资源失败
  • AI写论文必备攻略!4款AI论文写作工具,开启高效论文创作之旅!
  • 2026年成都水泥直供厂家排行:成都水泥河沙配送公司、/成都水泥河沙长期供应/含地址与服务对比 - 优质品牌商家
  • 保姆级教程:在ROS2 Humble上,用Orbbec Astra Pro深度相机搞定单目标定(附常见镜像问题解决)
  • 别再死记硬背了!用Python模拟一遍,彻底搞懂计算机的加减乘除(附完整代码)
  • 在Ubuntu 22.04上编译OpenWrt 23.05.2,我踩过的坑和解决方案都在这了
  • 西宁彩钢技术解析与2026年靠谱厂家选型指南:青海C型钢、青海Z型钢、青海仿古瓦、青海净化板、青海岩棉板、青海彩钢岩棉夹心板选择指南 - 优质品牌商家
  • 保姆级教程:Halcon20.11在Windows系统下的完整安装与破解配置(附常见问题解决)