我将利用寒假的实践总结过去一年实习实践中获取到的经验教训,他们并不一定都是正面的实践经验,反而其中不少都是我在开发实践中发现的漏洞。这些问题有的来自于我个人,有的则是整个开发团队的问题。在实习实践中,二八定律无处不在,我甚至可以讲有我的实习中学到了两成有效的经验,另外八成是吸取的教训。

以软件工程视角审视团队的同时,我们应当意识到许多个人问题的本质是经验水平不足,要深刻理解在软件开发中熟练经验的价值。更要学会辩证看待技术经验积累和AI生成能力之间的关系,不可因AI废技能,也不能因为经验积累就拒绝AI。

背景:这是一个Python项目,其主体包括实时聊天和大模型消息判断。核心实现包括IM、大模型API调用、数据处理。

这当中的许多问题在实习前的我看来是完全不可能发生的,但到了开发环境中就会发现,一些看似“低级”的问题不仅仅会发生,而且是由团队实践的结构性问题导致的,这些问题不在工程实践中实际参与,是绝对无法理解的。

当然,我想我描述的这些问题对于真正专业的开发熟手来说,也是不可理解的(笑)。

01. 混乱的数据交换格式

实践问题

混乱的数据交换格式会造成严重的问题,导致开发人员无法理解函数方法或API的意义,轻则增加数据处理复杂程度,降低开发效率,重则产生意料之外的错误。这一问题还会导致项目远期维护程度降低,后面的开发人员参与其中会让屎山越来越高,到了后期再高水平的开发人员参与也不可能解决这种结构性的、基础性的问题了。

在该项目的具体开发实践中,数据交换格式的混乱体现在许多地方,包括:

  • IM消息格式混乱,没有统一封装和管理
  • 类与方法间数据交换格式不统一,最典型的就是字典和JSON的混用
  • API对外返回数据格式混乱,没有规范处理正确与错误信息格式

产生以上问题的原因包括:

  • 代码编写前没有充分设计,对数据内容估计不充分,初期设计格式不合理
  • 团队成员水平有限,重构效率低
  • 前后端沟通不充分,数据格式供需不一致
  • 代码层次结构设计差,别扭的复用导致数据处理复杂化

解决方案

  • 在代码开发前,充分设计需要的数据格式,仔细分类、应用消息模板统一管理并做好可能的字段预留
  • 数据格式设计要层次分明、简洁易读,符合相关领域最佳实践
  • 数据字段设计不仅要规定字段类型,还要规定字段格式化方式
  • 数据结构设计严格区分“用户数据”与“系统信息”

最佳实践样例

在本项目中,总结多方经验,我给出两个实践消息格式,分别是消息格式和API返回内容格式。
消息格式

{
  "id":log-uuid,
  "type":MessageType.CHAT_MESSAGE,
  "data":{},
  "sent_at":isotime
}
# 消息模板定义示意
def create_message(data, message_type=MessageType.CHAT_MESSAGE): 
	return { "id": str(uuid.uuid4()), # 使用 UUID 生成唯一的消息ID
	"type": message_type, 
	"data": data, 
	"sent_at": datetime.utcnow().isoformat() # 使用 ISO 8601 时间格式 
	}
  • id:用于唯一标识一条消息,对消息进行日志存档,便于错误排查等
  • type字段为多类消息格式,应设计为枚举类型,考虑聊天消息、服务消息、系统错误消息、大模型返回消息等类别,这样设计的可维护性更强,也可扩展
  • data为具体数据内容,该对象内部的具体信息格式还应该根据项目需求具体设计,保证typedata中具体数据格式的统一
  • sent_at为消息发送时间,统一格式化为某种时间格式,比如ISO,满足可能的消息排序、生成时间的标识需求
  • 不能再多了:如果把data看作消息体,那么其他信息就是消息头,冗余的消息头不一定会带来好处,把需要的必要信息都放上就可以了
  • 定义消息模板:要定义并使用统一的消息模板,如有必要还可以为具体的type编写数据结构

API返回格式

{
  "code":200,
  "message":"done",
  "data":[]
}
  • 这是常见的标准的API返回格式,包括状态码和状态信息,便于前端理解
  • data的设计就有讲究了,用的是数组。在实践中我们发现,很多时候前端需要的特性不仅是分割与标识,还有有序,那么数组就可以满足这一需求
  • 如果API涉及分页,应当编写分页信息

02. 疯狂地嵌套

实践问题

放以前我肯定会说,这怎么可能呢,我注意着呢。但在实践场景中还真不是注意不注意的问题,一不小心你就会发现iffortrydict,全都混在一堆去了,写着写着就发现代码比火箭还尖。

不好写、不好读、不好维护。

解决方案

对于if-else一个非常有效的方法是,根据单一职责原则拆分方法,将冗余嵌套拆分为多个模块后,使用配置化的方式替代判断逻辑。

对于错误捕捉,要注意错误的处理方式和层次,并且将错误的具体处理逻辑进行独立。关于具体的异常处理我会在不久后写一篇文章专门分析和理解。

提前返回也可以解决嵌套严重的问题,使用提前return避免大量的if-else嵌套,多层if配合独立处理方法解决问题。

03. 不合理的抽象层次设计及对象方法

面向对象程序设计是计算机学生的必修课,在课堂中我们认为类的抽象是理所当然的,学生就是学生,教师就是教师,类的区分易如反掌。但在实际的开发场景中我们发现,真正的对象抽象是一门艺术,设计的好,可以让项目长远发展,设计的不好,那就等于给项目提前判死刑。抽象能力是要在大量的项目开发中积累的,而对于新手,这就是噩梦。

面向对象七大原则念起来朗朗上口,但是实践操作起来简直是另一回事。

实践问题

例如,在IM场景中,我们有消息(Message)、会话(Conversation)、用户(User)、摘要(Summary)等内容,还会随着项目的扩展增加新的类,会产生各种各样的问题,比如功能重叠、外键混乱、依赖交叉等等问题。

从上面的描述你也能看出来,其实最大的问题指向了一个原则:单一职责。而设计类和方法时要关注的内容又不一样,一旦脑子不清醒就会造出一坨来。

以消息为例,消息包括基本的ID、内容、发送者、接收者、发送时间等等属性,需要具备的方法包括历史记录、获取指定消息等。进一步细化到历史记录这一功能,在这一设计过程中,不同的功能可能会对消息这一实体的能力提出不同的要求,例如历史记录数量、内容、格式化方法,或者通过ID、时间等不同信息获取消息。有的我们需要写在消息中解决,有的需要在需求一方解决,这一权衡过程往往不那么容易。

解决方案

首先要提出的原则是要随着项目的进行不断重构代码,不要怕删了重来,因为我们并不是又一二十年工作经验的老手,我们设计的类或方法很难达到一劳永逸的境界,就算是老手,也需要根据需求重构他们的代码。然后我们回到上面的案例,讲一讲具体的方案。

我们要解决的第一个问题是将所有关于历史记录的功能都写在消息类中,还是将一些责任委托给外部组件(如历史记录管理器)?

事实上历史记录和消息记录虽然密切相关,但在面对的场景上却并不一致,在一个项目中需要获取历史记录的方法可能非常多,历史记录需要应付的场景也是复杂的,将历史记录单独提出来作为一个类是更好的做法,符合单一职责原则。

当然,在这个过程中避免过度设计、合理分配职责并不是一件很容易的事,特别是方法和内容都是动态变化的。设计这个东西还是需要大量积累才能掌握,三两句话并不能说清楚。

04. 共用分支实在离谱

Git的使用和管理是每个技术团队必须面对的问题,它是团队开发的基础,但刚出校门的同学们对此可能并不太熟悉,而在工作中我们发现,一些老同志也有些离谱的操作,就比如共用分支。

实践问题

我所在的团队对git的要求主要有两方面:

  1. 分支用人名缩写+功能概述的方式命名,即name/feature
  2. 提交代码时使用变基git base而不是合并
    这些要求没问题,可是这里的实践实在令人难绷。我是在一个快下班的下午发现的这个问题,有人在我的分支上直接增改代码,并且是与我的功能毫不相干的功能。在我发现那个问题之后他还这么干了很多次,甚至我委婉提醒后也没用。

更离谱的是,后来根据前端开发和项目演示的需求,我签出了release分支,专门用于推送到测试环境,那位同志要么直接改release,要么把他的开发分支直接推到k8s,而且这位同志还是个入行很长时间的人,虽然不知道有多长时间是作为开发者的,而他的理由只有一个:方便。

解决方案

培训不到位是这个问题的根源。

让开发规则成为文化深入团队,这东西是说不了的,得在实践中要求和培养,团队必须要有一个专业素养足够,权威足够的人来推进这些规则的落地,不然都是白搞。