最近新入职一家初创公司, 团队不大,工作了一段时间后发现团队中的代码情况很糟糕。团队一共两个产品线,五个开发人员, 每个人一个代码风格,每个人有每个人的代码风格本无可厚非, 但是如果代码没有经过合理的组织,只是机械的堆积功能,那对于以后功能迭代和需求变更会是很灾难性的。所以我打算将几年前看的一本书《代码整洁之道》—— 英文名叫《clean code》 重新再读一遍,也为了不让自己跑偏了。
1、首先思考几个问题:
- 我们读代码读的是什么??
- 我们写出的代码的作用是什么??
- 什么是好代码,什么是烂代码??
下面是我对这三个问题的思考:
- 我们在读别人的代码时,首先要知道的是这段代码或是函数是做什么的,其次为了完成函数的功能,它里面又分为哪几个步骤完成,最后才是这几个步骤的具体细节。比如下面这段代码:
public ProjectMsg addProject(ProjectMsg projectMsg) { checkInsertParam(projectMsg); UserLoginBO userLoginBO = GlobalHolder.getUser(); AccountUserExtBean accountUserExtBean = (AccountUserExtBean) GlobalHolder.getUserInfoExtParams(); projectMsg = buildProjectMsg(projectMsg, userLoginBO, accountUserExtBean); ProjectMember projectMember = buildProjectMember(projectMsg, userLoginBO, accountUserExtBean); projectMsgMapper.insert(projectMsg); projectMemberMapper.insert(projectMember); return projectMsg; }
从函数名我们可以知道,方法是在添加project数据,而它的实现分为三步:检查参数合法性; 构建project信息 、侯建projectMember信息; 最后新增数据。如果我们将方法的所有实现都放在一起,那就是下面这段代码:
public ProjectMsg addProject(ProjectMsg projectMsg) { AssertsUtil.isFalse(ProjectFlag.validateValue(projectMsg.getFlag()), ResultStatusEnum.PROJECT_FLAG_NOT_VALIDATE); if (projectMsg.getFlag().intValue() == ProjectFlag.PERSONAL_MEMBER.getValue().intValue()) { AssertsUtil.isNull(projectMsg.getSpaceId(), ResultStatusEnum.PERSONAL_SPACE_CANNOT_BO_NULL); } else { AssertsUtil.isNull(projectMsg.getWorkspaceId(), ResultStatusEnum.PROJECT_SPACE_CANNOT_BO_NULL); } AssertsUtil.isBlank(projectMsg.getName(), ResultStatusEnum.PROJECT_NAME_CANNOT_BO_NULL); AssertsUtil.isFalse(ProjectStatus.validate(projectMsg.getStatus()), ResultStatusEnum.PROJECT_STATUS_NOT_VALIDATE); UserLoginBO userLoginBO = GlobalHolder.getUser(); AccountUserExtBean accountUserExtBean = (AccountUserExtBean) GlobalHolder.getUserInfoExtParams(); projectMsg.setCreatorId(userLoginBO.getUid()); projectMsg.setFlag(accountUserExtBean.getUserType()); projectMsg.setTanentId(accountUserExtBean.getUserTenantId()); projectMsg.setOwnerId(userLoginBO.getUid()); projectMsg.setOwner(userLoginBO.getNickName()); projectMsg.setOwnerTanentMemberId(accountUserExtBean.getUserTenantMemberId()); projectMsg.setSpaceId(projectMsg.getWorkspaceId() == null ? projectMsg.getSpaceId() : projectMsg.getWorkspaceId()); projectMsg.setDelStatus(AssetsDelStatusEnum.NORMAL.getStatus()); projectMsg.setProjectId(MogicId.nextId(MogicIdEnum.PROJECT_INFO.getBizType())); ProjectMemberRoleRelation projectMemberRoleRelation = new ProjectMemberRoleRelation(); projectMemberRoleRelation.setId(MogicId.nextId(MogicIdEnum.PROJECT_MEMBER_INFO.getBizType())); projectMemberRoleRelation.setProjectId(projectMsg.getProjectId()); projectMemberRoleRelation.setTenantId(accountUserExtBean.getUserTenantId()); projectMemberRoleRelation.setWorkspaceId(projectMsg.getSpaceId()); projectMemberRoleRelation.setUserTenantMemberId(accountUserExtBean.getUserTenantMemberId()); projectMemberRoleRelation.setUserId(projectMsg.getCreatorId()); projectMemberRoleRelation.setRoleId(RoleEnum.PROJECT_OWNER.getRoleId()); projectMemberRoleRelation.setMemberType(ProjectMemberType.SPACE_MEMBER.getValue()); projectMemberRoleRelation.setMemberName(userLoginBO.getNickName()); projectMemberRoleRelation.setMemberAvatar(userLoginBO.getPortrait()); projectMemberRoleRelation.setPhone(userLoginBO.getPhone()); projectMemberRoleRelation.setStatus(WorkspaceMemberRoleRelationStatusEnum.NORMAL.getStatus()); projectMemberRoleRelation.setGmtLastActive(new Date()); projectMemberRoleRelation.setGmtJoin(new Date()); projectMemberRoleRelation.setGmtInvite(new Date()); projectMemberRoleRelation.setDelStatus(AssetsDelStatusEnum.NORMAL.getStatus()); projectMsgMapper.insert(projectMsg); projectMemberMapper.insert(projectMember); return projectMsg; }
虽然代码逻辑并不复杂,但是看着这么一大堆代码,肯定就没有了读的心情了。
-
我们写出的代码,主要是为了阅读。而代码编译后的二进制文件,才是为了给机器执行。所以我们在写代码时,要像写文章一样,组织好语言、逻辑清晰,不要一写就是上百行,让人看了不知道写的是什么。
-
至于什么是好代码,我觉得在功能实现的基础上,越容易让人阅读和理解的代码就是好代码。最好是能让人看待你的主函数,就能知道你要做什么,分了那几部来完成。这里我最欣赏spring源码中的refresh方法,将复杂的容器刷新过程,用十几行代码描述的清晰明了:
@Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); finishRefresh(); } catch (BeansException ex) { destroyBeans(); cancelRefresh(ex); throw ex; } finally { resetCommonCaches(); contextRefresh.end(); } } }
这里就不对spring源码进行过多的赘述了。我们要学习的是它组织代码的方式。
代码作者应尽力写出易于理解的代码。 我们想把代码写得让别人能一目尽览,而不必殚精竭虑的研究。
2、什么是整洁代码
对于整洁代码的定义没有一个统一的答案。
《clean code》的作者在书中引用这么几段话:
我喜欢优雅而高效的代码。代码逻辑应当直接了当,让缺陷难以隐藏;尽量减少依赖关系,使之便于维护;
依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱出来。
整洁的代码只做好一件事。
我可以列出我留意到的整洁代码的所有特点,但其中有一条根本性的,整洁的代码总是看起来像是某位
特别在意它的人写的,几乎没有改进的余地。代码的作者什么都想到了,如果你企图改进它,总会回到
原点,赞叹某人留给你的代码---全身心投入的某人留下的代码。
总结了一下作者对于整洁代码的标准: 优雅、在意、没有代码重复、提高表达力、提早构建简单抽象
3、有意义的命名
在刚开始学习编程时,就被告知对变量命名要见名知意。我觉得只是见名知意还不够,命名还要足够简洁、易懂,对于同一个含义的变量,在不同函数、实体中的命名要尽量统一。
3.1 名副其实
我想大部分程序员都知道变量命名要名副其实,但是有多少人愿意花费时间和精力去做这件事。选个好名字要花时间,但以后省下来的时间会比这时花掉的更多。
函数、变量或类的名称应该告诉你,它为什么存在,它做什么事,应该怎么用。如果名称要靠注释来补充,那就不算是名副其实了。
3.2 避免误导
如: 如果一个变量不是List类型, 不要用 ***List来命名该变量
提防使用不同之处较小的名称:如 XYZControllerEfficientHandingOfString 和 ZYZControllerForEfficientStorageOfString , 要想看出来这两个词之间的差异,需要花上一点时间。
3.3 做有意义的区分
1、我们经常会为了满足编译器和解释器的需要对同一作用范围内的两个不同变量的命名以数字结尾进行区分, 如:
这样的区分对于变量的理解并没有什么意义,只是为了通过编译器和解释器对代码的理解。我们可以根据两个变量的来源或作用,对变量名进行修改:
这样我们就可以知道一个是加入的project列表信息,一个是创建的project列表信息 。
2、废话是另一种没有意义的区分。 如: product、productInfo 和 productData 这三个变量名从代表的意义上看不出什么区别。 同样的对于函数名:
上面的三个函数名,也区分不出来该调哪个函数。
3.4 使用读得出来的、常见的、简单的名称
3.5 避免使用编码
3.6 类名 -- 类名不应当是动词
3.7 方法名 -- 方法名应当是动词或动词短语
3.8 别用双关语
避免将同一个单词用于不同目的。 同一个属于用于不同概念,基本就是双关语了。
3.9 使用解决方案领域的名称
只有程序员才会读我们的代码, 所以尽量使用计算机科学相关的术语、算法明、模式名等。
3.10 使用所涉问题领域的名称
如果不能使用程序员熟悉的术语来命名, 可以采用所涉问题领域的专业术语来命名。
3.11 添加有意义的语境
多数名称不能自我说明。我们需要提供良好的命名的类、函数或 名称空间来防止名称,给读者提供语境。如果还是没有说明,需要给名称添加前缀进行说明了。
假设有一堆变量: firstName、lastName、street、city、state、zipCode等。这些变量在一起可以用来描述一个地址信息。但是如果某个单独变量出现在一个函数中,如state单独出现,我们就不能确定这里的state是代表地址的一部分信息,还是其它什么含义。 这时可以添加前缀addrState、addrFirstName、addrLastName等, 以此提供语境说明。