漫谈代码质量

很多的软件工程师或者说程序员,在拥有数万乃至数十万代码行的编程经验之后,仍然很难持久输出高质量的代码。他们可以在严格的规范和仔细的审查下,产出符合要求的代码。但是,一旦脱离了这些严格的规范和审查,就会出现代码质量下降的情况。这种情况不仅出现在初级程序员身上,也出现在职级更高、经验更丰富、解决问题能力更强的程序员身上。

在阅读了很多工程师提交的代码,并分析这些代码中的设计缺陷之后,我发现大家的失误或者问题,有许多雷同。很多的错误,只要阅读过一两本有关代码质量方面的书籍、博客,认真分析和学习过高质量的代码,理论上应该可以避免。那么,到底是什么原因导致这些错误普遍出现呢?

首先,我们必须要承认很多人并没有读过相关的著作,或者真正用心去学习和理解相关内容。比如《代码整洁之道》、《设计模式》、《重构:改善既有代码的设计》等。

其次,在这些书籍中,为了避免核心代码泄漏也好、为了更通俗易懂也好,一些典型的示例,并不能很好帮助我们理解相关内容。

再次,关于软件设计原则(主要是面向对象设计),公开的百科、博客,论述都过于“教条”,不够深入细致,也难以找到公开的工程示例来帮助理解。

总之,关于什么是好代码,好在哪里,很多人其实并没有清晰的答案。

基于此,有了这一篇分享。我从真实、稳定运行的代码仓库中,找出一些典型问题,结合理论,来尝试回答“什么样的代码是好代码?”设计原则如何在代码中应用?

代码整洁度

“代码本身就是软件的设计图纸” —— 《代码之美》

什么样的代码是好代码?

在若干个场合,曾经我都问过这个问题:“什么样的代码是好代码?” 典型的回答包括“可运行、功能正确、可扩展性好、可维护性好、可测试性好”等。这些维度,都是我们在设计和实现软件时,需要考虑的重要方面。有些维度,我们比较容易满足,比如“可运行”、“功能正确”等。但是,有些维度,我们比较难以满足,比如“可扩展性好”、“可维护性好”、“可测试性好”等。

那么从现实的角度,这些答案中哪个维度最重要?

从理论角度再次重复各大专家一致的回答:“可维护性好”是最重要的。也是现实中最难以满足的。

事实上,“可维护性”包含了很多子维度,比如“可扩展性”、“可测试性”等。然而今天我不是要再谈这些理论,最重要的是:“可维护性好”意味着其他工程师可以很容易地理解、修改和扩展我们的代码。而这其中最重要的,是“容易理解” —— 代码的可读性。

如果你认为这是PUA,是公司为了更容易的替换你,那么到此为止,你不必继续往下读了。

我们使用编程语言来书写代码,这跟我现在使用汉语来表达这篇分享的观点,本质上没有什么区别。当然,人类语言还可以表现的更丰富,我们从优美的诗歌、散文中感受到“意境”、“情感”、“思考”等。也会有“一千个人就有一千个哈姆雷特”的情况。

然而使用编程语言组织的代码,追求的是“准确(精准的正确)”、“无歧义(清晰)”、“无重复(不啰嗦)”。虽然我们也能从精炼的代码中感受到非凡的”代码艺术“,但这与一两句诗歌可以描述宏大的场景、蓬勃的情感、天马行空的意境是完全不同的。

所以代码更像是一些具有规范要求的应用文写作,而不是自由奔放的文学作品。对应用文的要求,也同样适用于代码:

  • 目的明确
  • 层次清晰
  • 语言精炼
  • 格式规范

在AI时代,好代码的含义有什么变化吗?

先说答案,没有变化!

因为在理解力这方面,大模型追求的就是人类的理解力。目前,大模型显著超过人类个体的,是其“记忆”和“索引记忆”的能力。而在理解和推理方面,只能说是接近人类的水平。虽然很多时候AI理解一个问题,表现的比人类更快速和准确,那是因为AI掌握了更多的经验和“现存知识”,如果是面对全新的问题,AI的理解和推理能力就会表现不佳。

另外训练大模型的“语料”,都是人类理解世界之后产生的,大模型目前只是人类的“学生”,而不是凭空出现的、在理解世界和真实方面完全不同的“物种”。

在AI时代,不管是人类写的代码,让AI维护;还是AI写的代码,让人类维护;或者是一个AI生成的代码让另一个AI维护,都仍然遵循以前的标准和规范。

“代码整洁之道”

“代码质量与其整洁度成正比” —— 《代码整洁之道》

让我们先来看几组代码截图:

示例代码1示例代码2-1
示例代码2-2
示例代码3
这一组代码截图,分别来自于当前版本的某媒体库源码、重构中的代码、以及webrtc某版本的源码。我们先看一下AI(腾讯元宝)的评价:

AI对代码风格的评价
除此之外,从这组截图中,还有关于“命名”的差异:注意图二中的m_factorym_worker,在重构的代码中,它们被重命名为transportFactory_transportReceiveWorker_

虽然有时候我说话颇有些啰嗦,但是在代码命名方面,我非常抵制“重复”。然而,最重要的仍然是“准确、表意清晰”。所以我反对mediaengine::engine::MediaEngineImpl这样的命名,但同样不推荐m_factory和m_worker这种过于简化的命名。我们的目标是让命名在当前上下文中做到“自解释”,以减少阅读时的认知负担

我们再来看一组代码截图:
示例代码:组织良好的头文件包含
示例代码:随意的头文件包含

同样的,我们先看看AI(腾讯元宝)的评价:
在这里插入图片描述
AI的总结中有一点是错误的 —— 两份代码都是可以编译的。IDE的警告,是因为VSC中cpp_properties.json文件include路径的配置问题。也有一点是AI从代码截图中没有识别到的:两个工程在include路径配置上,图2十分随意,而图1则非常规范 —— 像java/python/node.js/rust等更现代语言的import语句默认格式一样规范。这种被各类编程语言不约而同采用的规范,解决了什么问题?为什么会成为事实上的标准?

在Poco库的代码规范中,明确规定了模块的目录结构:对外提供的头文件都放在$Module/include/Poco/$Module目录下。这是一个非常好的设计,我们应该在所有的项目中都采用这种规范。只有当我们都遵循这个规范,我们才能在组织头文件时,提供足够的可读性:无歧义、无冲突、依赖明确、层次分明。

是的,在这一个小节里,我所强调的其实是“代码风格”或者“代码规范”。它只是“代码质量”甚至“代码整洁”的一个维度而已。在有限的篇幅里,我无法详细的讨论"google c++ 代码规范"还是 "Poco代码规范"更好,也无意阐述对微软命名风格、gnu命名风格的偏见和对Poco命名风格的认同。上面所列举的示例代码,在以上这三种代码规范中也都没有对应的条目要求。我想要强调的是:回归我们制定代码规范的初心 —— 让代码更容易阅读和理解,不管对象是人还是人工智能。而对于阅读来说,“组织良好”的语言总是比杂乱的、无序的更容易理解

最后,放上我与元宝关于代码截图的对话,以证明我没有刻意引导它。"关于代码风格的对比"点击查看元宝的回答

“语法糖”、“新车轮”和“大杀器”

“喔,这有一颗香甜的糖果”

在代码整洁这一个小节里,我尽量避免讨论与设计相关的质量维度。这些内容,将在“代码设计”这一个小节里进行讨论。然而除了规范和风格,以及设计因素之外,还有一些个人的偏好或者习惯会对代码的整洁度和质量造成比较大的影响。比如对“语法糖”的滥用,比如特别偏爱造一个“新的轮子”,又比如走向另一个极端 —— 不管有无必要,集成庞大的、功能丰富的“大杀器”到自己的项目中。

我们先来看看“语法糖”的影响实例。

在C++的圈子里,一度有一种观点 —— “禁止在工程中使用template”。为什么会有这样的声音呢?让我们看两个头文件,它们都是对template的使用:
示例代码:参数化类型
示例代码:参数化行为-1
示例代码:参数化行为-2
我不知道有多少工程师能够很快的分辨这两种用法的区别。它们分别是参数化类型参数化行为/策略。图中的GuardHelper使用“参数化行为/策略”实现了“资源获取即初始化”(RAII)的模式。

从可读性的角度来看,BypassFilter非常清晰,它的行为也是完全可预测的。而GuardHelper则需要结合EnableDisable模板函数的泛化才能理解它的行为。

但这不是最关键的,也不足以让我们称其为“语法糖”。真正使其成为“语法糖”的原因,让我们看一段我和"Gemini 2.5 Pro"的对话:

AI评价语法糖
所以,我想表达的观点是:语法本身无所谓“甜不甜”,关键是:错误的使用语法带来的便利,就像一颗带来短暂快乐的糖一样,让我们忽视了“疼痛”或者“苦涩”。让我们再看一段我和Gemini的对话:
在这里插入图片描述
Gemini确实总结的很好,我不必再作赘言。

“新的轮子”和“大杀器”

关于新轮子的例子,我不必贴出代码。

在我们的一个项目中,定义了一个my_refptr的模板类,基本上它就是std::shared_ptr的一个等价实现。它定义了get()reset()operator*()operator->()move()dynamic_cast()/const_cast()等方法。在我第一次阅读到这个部分的时候,我以为重造这么一个智能指针,或许是在引用计数改变的时候,需要打印一些调试信息。但实际上,并没有这个需求。我又为它找了一个“借口” —— 也许是为了支持更早的编译器和系统,或者一些定制的std库。然而,设计这个类的同学一度跟我讨论过,整个项目是否应该升级到C++17。

这个类本身并没有什么质量问题,它被设计的很好。

但事实上,它确实引入了一些问题,比如代码中同时存在std::shared_ptrmy_refptr,这会导致维护者额外的理解和学习成本。虽然并不是很大的代价,然而其收益却是基本为零的。所以,它有什么存在的价值呢?它只是引入了阅读上的“些许障碍”和“轻微的维护成本”,没有其它了。

不产生价值的代码,就是负担,对吗?

这是“造轮子”相对极端的一个例子。实际上,在我们的工作中,更多的情况可能是现有的某个库,或者某个框架,基本满足我们的需求,只是有一到两个小的需求未满足,很多时候,工程师也会选择再造一个新的。不是由于旧的代码难以维护,也不是由于不满足的需求非常难以在旧代码的基础上实现。很多时候,这源于工程师对技术的热情和探索欲,渴望亲手实现一个完整的模块。这种热情是宝贵的,但我们需要引导它,在动手之前先进行充分的评估,确保我们的努力能为项目带来真正的价值。

关于大杀器,我想谈的一个例子是这样的:我们需要在项目中支持基于RTSP的网络摄像头,像USB摄像头一样使用它。于是我们的一位工程师,将Live555集成到了我们的项目中。为此编写了conan脚本,并且解决了Live555在我们所有需要支持的平台上的编译问题。

你知道Live555有多庞大吗?它的核心模块,有189个源文件和156个头文件。然而我们的项目中需要使用到的,可能不到5%。你知道后续在其它项目中,为了实现一样的功能,我用多少行代码支持的吗?5个头文件、2个源文件,总计不到2000行代码(包括空行)。这已经不是“杀鸡用牛刀”能够形容的了,也许“洲际导弹打蚊子”才是更恰当的描述。

之所以把“造轮子”和“大杀器”放在一起谈,是因为它们刚好是两个极端。一个是放着现成的、合适的成熟模块不用,非要自己写一个新的;另一个是为了实现某个功能,以为可以少写一些代码,而将一个庞大的、功能丰富的“大杀器”集成到自己的项目中。实际上,为了封装Live555到项目中的代码比起我自己实现的rtsp模块,代码量也没有差多少了,实际付出的工作量更是远远超出。所以当我们面对“造一个”还是“用现有的”这样的选择时,一定要仔细评估、慎重选择。

代码设计

完整的软件设计,是从《需求规格说明书》出发,到输出所有设计文档之前的过程。包括了架构设计、模块设计、接口设计、数据设计、过程设计等。这其中的每一个部分,都有专门的著作论述了相关方法论。在这里,我想谈论的重点不是一个成熟的架构师如何做架构选型、如何使用工具预研预测、评估可行性与风险等等。还是想谈一些广大软件工程师普遍都会遇到的直接影响“代码质量”的设计问题。

设计原则解析

在我担任公司职级评委,评审同事的职级晋升材料时。在软件设计的维度,很多人的举例都会提到自己对某个设计原则的理解和应用。其中被提到最多的是SOLID原则中的“依赖倒置”原则。我想很大的原因是因为这个原则是最容易使用代码片段来举例证明的。然而,我们真的深刻理解这原则,并总是能恰到好处的应用它吗?

在通过实际的代码示例来阐述我的观察和观点之前,还是啰嗦一下,一起复述那些我们耳熟能详的内容。

首选,我们回顾一下这些设计原则被提出的目的 —— 高内聚低耦合、提升可维护性和可扩展性。重点是第一句话,“高内聚低耦合”。这是几乎所有设计原则的目的。但是很多人也将其理解为设计原则。

重复一遍,"高内聚低耦合"不是一个具体的设计原则,而是软件设计的核心目标和评估标准。各种设计原则(如SOLID、KISS等)本质上都是为了帮助我们实现这一目标。

然后,让我们再罗列一下哪些常被提到的设计原则:

  • SOLID原则
    • 单一职责原则(Single Responsibility Principle) 一个类、函数或模块应该只有一个引起它变化的原因,即只承担一项明确的职责
    • 开闭原则(Open-Closed Principle) 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭
    • 里氏替换原则(Liskov Substitution Principle) 子类对象应该能够替换其父类对象,而不影响程序的正确性
    • 接口隔离原则(Interface Segregation Principle) 客户端不应该依赖它不需要的接口
    • 依赖倒置原则(Dependency Inversion Principle) 高层模块不应该依赖低层模块,它们都应该依赖于抽象
  • KISS原则(Keep It Simple, Stupid) 保持系统简单、直接、易于理解和使用
  • DRY原则(Don’t Repeat Yourself) 不要重复自己的代码,而是提取通用的代码到一个地方
  • YAGNI原则(You Aren’t Gonna Need It) 不要为未来可能不需要的功能而编写代码
  • 迪米特法则(Law of Demeter) 一个对象应该对其他对象有最少的了解,只与直接的朋友通信
  • 合成/聚合复用原则(Composition/Aggregation Reuse Principle) 优先使用合成/聚合关系来实现代码复用,而不是继承

然而有些原则你可以看到很明显的冲突,比如DRY和YAGNI:DRY原则​要求消除重复,但YAGNI原则​又警告不要过度设计,所以又有人提出了所谓的“Rule of Three 原则” —— 第一次出现某个功能时,直接实现;第二次重复时,先容忍重复;当第三次出现时,再进行抽象重构。

除了这种明显的冲突,还有一些没那么明显的“取舍”或者“平衡”。让我们一起来看几个例子:

依赖倒置原则

先看一张类图
UML1
这是基于Windows Media Foundation库,封装视频采集器的代码,由"Gemini 2.5 Pro"帮我生成的UML类图。图中的SourceReaderCallbackMF派生自Media Foundation库的IMediaSampleCallback接口,用于处理视频采集器的输出样本和事件。这个设计是一个非常典型的依赖倒置原则的应用。

让我们再看看“Gemini 2.5 Pro”对这个设计的高度评价:
AI对依赖倒置的肯定
看起来我是想跟大家介绍“依赖倒置”原则的经典应用?不,当我在重构版本实现这个封装时,我采用了有些不一样的做法。
uml2
这一段代码,在我重构项目时,也曾经与Gemini讨论过,当我简单让其对比原实现和重构实现的差异时,它告诉我原来的实现是一个更符合设计原则的实现,然而,最终我说服了AI(过程就不贴图了),从工程和质量的角度考虑,我的重构实现是更好的实践。

说服AI
在写这篇文字时,我让Gemini帮我生成重构代码的类图,它顺便又总结了一下:
AI对重构的评价
一些AI没有提到的点:

  • 两个设计都有很好的遵循CARP —— 组合大于继承,都没有直接从ISourceReaderCallback派生。某种程度上,这种选择也体现了单一职责原则。
  • 两种设计都遵循了依赖倒置,然而原来的设计,将依赖倒置原则的重要性置于比“封装”更重要的位置。这是一个错误的设计决策。而重构后的设计,满足了“隐藏细节”的要求,实现了更高的内聚性和更低的耦合度。对依赖倒置原则的应用,则体现在VideoCapture接口的设计上。
  • 重构后的设计,遵循了YAGNI原则:准确识别是否存在扩展或变更的可能,避免了过度设计。
CARP、SRP和KISS

在上一个例子中,谈到了两个版本的代码设计都遵循了CARP原则。然而,在同一个项目、同一位开发人员参与设计的另一些模块,却有一大批显著违背CARP原则的代码。为什么会出现这样的情况?我想根本原因还是在于对设计原则的理解不够深入,这也是我撰写本文的原因所在。

让我们看看这些代码(一些类的定义,为了排版好看,我把代码复制过来,而不是截图):

// 关于PipelineNode的设计
class BasePipelineNodeImpl : public IPipelineNode,
                             public InputSinkInterface,
                             public OutputSinkInterface,
                             public diagnostic::PipelineStructure<BasePipelineNodeImpl>{}
class VideoSourceNode : public BasePipelineNodeImpl{}
class VideoFrameSourceNode : public internal::IVideoFrameSourceEx,
                             public VideoSourceNode{}
class DesktopCaptureSourceNode : public IScreenCapturer,
                                 public VideoSourceNode,
                                 protected FRAME_SINK_INTERFACE{}
class CompositeCaptureSourceNode : public ICompositeCapture
                                 , public VideoSourceNode
                                 , public composite::VideoCompositeObserver{}

// 最核心的一个类定义
class AvcConnectionImpl : public ConnectionEvent
                        , public IAvcConnection
                        , public MaxMeRtpRtcpTransportObserver
                        , public webrtc::MediaBandwidthObserver
                        , public webrtc::KeyFrameRequestWithSsrcSender
                        , public Module
                        , public FeccChannelObserver{}

看看这些代码,你的第一印象是怎样的?如果你作为团队的新成员,当阅读这些代码时,你会有什么感想?

虽然多继承、多重继承,在经典著作《重构:改善既有代码的设计》中都没有被列为"Bad Smell"。但是从可维护性的角度来看,理解门槛会很高,对整个项目不是非常熟悉的人来说,维护这样的代码会是一个巨大的挑战。

看看AI对AvcConnectionImpl的评价:
AI评价一个庞大的类
然后我跟Gemini介绍了我的重构设计,让我们看看这一次对话:

我:我的重构大概是这样的:1. ConnectionImplAVC(等价于AvcConnectionImpl)只从ConnectionAVC(等价于IAvcConnection,是对外提供的接口)单一接口派生 2. 定义了MediaChannelAVC和 VoiceChannelAVC、VideoChannelAVC,将mlts::TransportObserver的实现作为MediaChannelAVC的内部类,使用适配器模式,并将OnRtpPacket/OnRtcpPacket定义为protected的纯虚函数,由VoiceChannelAVC/VideoChannelAVC分别实现具体的包处理逻辑 3. KeyFrameRequestObserver和MediaBandwidthObserver只对视频流有效,因此使用适配器模式封装在VideoChannelAVC内部 4. 去掉了ConnectionEvent/Module这两个接口,这两个接口基本上是无用的 5. FECC功能暂时没有实现,但思路和前述一致

Gemini:
在这里插入图片描述
在这里插入图片描述
Gemini总结的非常到位,作为一位追求效率和质量的工程师,我不会将这些对话复制出来稍作修改,然后假装成我的见解,虽然我是如此认同这些观点。我只会把截图贴出来,然后大声的告诉你:是的,这就是我想要说的。

关于动态性

为什么会谈到动态性呢?
在上一个小节中,提到了我移除了Module接口。让我们看看Module的定义:

class Module {
public:
    virtual ~Module() = default;

    /**
     * @brief QueryInterface function to expose inner interfaces or capabilities
     * of the module.
     *
     * @param riid Interface ID to be queried for. All IDs are defined in ModuleIID.h
     * @param out Pointer to the interface requested by the callee. todo: change this to weak pointer?
     * @return int Return value of the function call
     */
    virtual int QueryInternalInterface(decltype(IID_MODULE_BASE_INTERFACE) riid, void** out) {
        return -1;
    }
};

然后再看看我和Gemini关于Module的对话:

我:对于Module这种模仿COM/ATL的设计,在我们以对外提供一个RTC引擎作为目标的项目里,实在是太过于“过度设计”了,对吗?我记得哪怕是COM/ATL本身,在业内也是颇有诟病的。

Gemini:
在这里插入图片描述
在这里插入图片描述
关于Module过度设计的问题,我没有什么可以补充的。但是顺着这个事情,我们可以谈一谈关于动态性的问题。

首先,动态性看起来很酷,像是解决可扩展性的终极银弹。业界有许多因为极好的动态性设计而成功的软件项目,比如nginx。因为其支持在运行时动态的加载和卸载模块,形成了一个高度可扩展的系统乃至生态。

然而,要真正的驾驭动态性,我们需要对其有更多的理解 —— 动态性是分层的,每一层都有其特定的适用场景:

层次 动态性 核心思想 典型设计 适用场景
1 运行时多态 动态派发 虚函数 标准的OOP,同一继承体系下的行为差异
2 运行时类型识别 动态查询类型 dynamic_cast 需要调用派生类特有功能,是对多态的补充
3 可插拔行为 动态组合 策略/组件模式 避免继承爆炸,实现灵活配置和行为组合
4 动态代码加载 动态扩展 插件架构 (DLL/SO) 允许第三方独立扩展程序功能
5 元编程/反射 动态检查/修改结构 模拟反射/脚本绑定 序列化、属性编辑器、需要极高灵活性的框架

Module最大的问题,是尝试去满足一个不存在的需求;其次,是使用了错误的方式,试图用一种手动、非类型安全的方式去模拟层次2和层次4的部分能力。

而在这个项目中,还有所谓的“插件”机制,然而实现的是似而非。白白增加了代码的复杂度,同时也增加了维护的成本。其根本原因,就在于设计者并没有对动态性有足够的认知和理解,只是觉得”高大上“,或者可以作为自己简历上的一个亮点,就不伦不类的套用了这个概念。请原谅我有些过于直白和尖锐,我并不是要抨击这些人,因为毕竟这或者也是我的”来时路“,可能只是我选择性的遗忘了类似的经历。我只是希望,通过坦诚的交流,让更多的工程师能够在职业生涯更早的阶段认识到这些问题。

谈谈接口设计

原本我应该在上一个小节结束有关代码设计的讨论。因为接口设计不是我通过一个小节就能讲清楚,或者给出别具一格的见解。只是在被我这篇文章大量引用为素材的这个项目中,其对外的接口设计上,存在一些或许大家都不太注意的问题,其中的一些问题,我认为值得拿出来谈一谈。

在这个项目对外的接口中,有一个Engine对象,它实现了对整个SDK的配置功能、一大串的工厂方法以创建不同的对象,还有一些辅助的调试开关。这些工厂方法,主要用于创建不同的Connection/Track,还有一个用于创建PipelineNode的工厂对象,此外还有一个用于创建AudioMixer对象。

我们来分析一下这个设计。Gemini对它的评价是:它是一个典型的**“上帝工厂”(God Factory)或“万能接口”(Kitchen Sink Interface)**,这违反了多项重要的设计原则。

它违反了以下几个设计原则:

  1. 严重违反了接口隔离原则(Interface Segregation Principle, ISP):上帝工厂暴露了太多的接口,这使得客户端代码依赖于它实现的所有功能。
  2. 单一职责原则(Single Responsibility Principle, SRP):它同时承担了引擎配置、连接工厂、媒体轨道工厂、设备管理工厂、工具类工厂等多种职责。
  3. 开闭原则(Open-Closed Principle, OCP):这种设计是对扩展关闭的。每次需要添加一个新的“可创建”的顶级对象时,都必须回到 “IMediaEngine.h” 这个核心文件并修改它,向其中添加一个新的纯虚函数。这正是“对修改开放”的反面教材。

除此之外,还有一个更不容易引入注意的问题:Connection有一个PublishTrack的方法,需要传递一个Track对象指针到Connection中。然而,在Connection的实现中,会调用dynamic_cast将传入的接口对象指针转换为内部实现的指针,如果不满足,则调用失败。你发现这个地方的设计问题了吗?

Gemini的总结是:这个设计确实存在严重的问题,它在API层面做出了一个承诺,但在实现层面又偷偷地违背了这个承诺,导致接口既不安全也不诚实。

其详细分析如下:
接口设计的问题
违反的设计原则和影响如下:
接口设计违背的设计原则
那么,如何才能既满足“只支持内部实现”,又不违背设计原则呢?

其实非常简单:只需要将Track对象在内部创建和管理声明周期,而在Connection接口中提供GetTrack的方法,然后在Track接口中定义足够的配置、控制方法,如果有必要,将Publish定义到Track接口中。

看看Gemini对这个设计的评价:
AI肯定重构思路1
AI肯定重构思路2
AI多少有点过于恭维了,我并不想炫耀,只是为了偷懒,可以少打一些字,同时依然能够讲清楚为什么现有的设计不好,应该如何改善。

Ending

在最后,我想对读到此处的每一个人,表示感谢。希望您觉得阅读这篇如此长的长文,对得起您花费的时间。在AI开始冲击初级工程师岗位的时代,希望这篇文章能够帮助到一些人,更早的摘掉“初级”的标签。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐