如何编写高质量的C#代码(一)
从”整洁代码“谈起
一千个读者,就有一千个哈姆雷特,代码质量也同样如此。
想必每一个对于代码有追求的开发者,对于“高质量”这个词,或多或少都有自己的一丝理解。
当我在长沙.NET技术社区群抛出这个问题时,众说纷纭。有人说注释齐全、可读性高,就是高质量;有人说变量命名、代码层次清晰,就说高质量的代码;有人说那些使用了新特性的代码,很多都是高质量代码;也有人说,高质量的代码是个伪命题,因为他往往要花大量的精力才能精心打磨,有这个时间,产品早就黄了。
说到”高质量“代码,就不得不提”整洁代码”。这个概念来源于畅销书《代码整洁之道》(The Clean Code)中,鲍勃大叔引入了这个整洁代码的概念,他认为
写整洁代码,需要遵循大量的小技巧,贯彻艰苦习得的‘整洁感’”,这种“代码感”就说关键所在。有些人生而有之。有的人费点劲才能得到。它不仅让我们看到代码的优劣,还予我们以借戒规之力化优为列的攻略。
缺乏”代码感”的程序员,看混乱是混乱,无处着手,有“代码感”的程序员,能从混乱中看出其他的可能与变化。“代码感”帮助程序员选出最好的方案,并指导程序员指定修改行动计划,按图索骥。
编写整洁代码的程序员就像艺术家,他能够用一系列变化把一块白板变作由优雅代码构成的系统。
这本书值得摆在每一位程序员的案头。许多热衷于英文原作的读者都会说国人翻译的许多作品都失去了原作的韵味,但这本韩磊老师翻译这本中文版十几年过去了,印刷了许多版了,也能客观证明这本译作的价值。
也许初读这本书,许多作者提到的手法我们无法短时间内认真体会,但许多读过这本书都表示,许多想法在我们写代码的时候突然迸溅而出,使得思路能够更加通达,并达到一种“人码合一”的状态。
”代码感“
在我们大部分开发者看来,我们开发的代码,往往无需涉及过于复杂的业务逻辑或底层技术,只需简单的使用一些代码拼凑,即可按时完成我们的任务,也就说所谓的”CRUD业务开发者“。
但业务系统本身也并非全靠所谓的“无代码平台”或“代码生成器”能够自动开发完成,他依然需要开发者用心去设计其中的逻辑、变量、结构、流程,才能更好的运转,尤其是要想让应用系统能够保持长久的生命力,更需要我们能够编写更高质量的代码。
在《代码整洁之道》中,作者将这种编写高质量代码的能力,称为“代码感”,这种感觉有时需要灵光一现,有时又需要花费大量的精力才能完成。
就像在《灌篮高手》中,安西教练让大家培养球感:
两万个球?写两万个类/方法/代码行?确实是一种提高”代码感“的好方法。
但跟投球要掌握方法一样,简单的重复写两万行代码估计很难提高代码质量,依然需要大量刻意练习才能带来质量上的提升。
而如何编写高质量代码,在软件开发领域,也有一些前人总结出来的良好准则,人们将这些准则,总结为“设计原则”。除了设计原则外,还要许多良好的实践模式,人们将它们称为”设计模式“。设计原则就像是内功心法,设计模式,则像招数功夫。
也许我们无法完全遵循这些原则或模式,但能够灵活的运用,总能给代码质量带来提升。
何为高质量代码
我个人认为:高质量代码是可读性强、易于测试,它们能够恰如其份的表达业务的需要,并能根据业务需要易于修改的代码。
高质量的代码也许与技术架构、特定API、特定的语言没有太大关系,但高质量代码或许都具备一些相似的特点。
代码结构
结构是代码的核心,就像高楼的支架,为整个代码的完整运行奠定基础。好的代码一定结构清晰,让人易于理解,并能快速定位问题、解决问题。
有人说好文章的结构特点便是:” 凤头、猪肚、豹尾“, 文章的起头要奇句夺目,引人入胜,如同凤头一样俊美精采;文章的主体要言之有物,紧凑而有气势,如同猪肚一样充实丰满;文章的结尾要转出别意,宕开警策,如同豹尾一样雄劲潇洒。代码也许无需追求达到这么高的境界,但遵循一定清晰的代码结构也能达到同样的效果。
结构按照我个人的理解,可能包括以下几种层面:1、项目文件夹命名;2、分层;3、模块命名;4、代码格式。
1、项目文件夹
对于复杂项目,打开文件夹和解决方案的第一眼,是清晰还是紊乱,往往就是我们对于项目的第一印象。许多资深研发工程师,都会倾向于用数字来对文件夹进行编号,例如对于复杂项目,我们使用如下命名方式对定义解决方案文件夹,虽然不会花特别多的功夫,但会给开发过程带来许多便利。
当然,由于在Visual Studio中,项目文件夹本身属于sln解决方案文件中定义的层级结构,并不会在资源管理器文件夹中体现,所以有时还需要在资源管理器文件夹中也定义类似的层级结构。
01 基础服务
02 框架服务
03 应用服务01 工作流服务02 权限服务03 日志服务
2、分层
分层式架构大家都习以为常,其中尤其以三层架构(用户表现层,业务逻辑层,数据访问层)已经深入人心,成为许多.NET开发者的普遍认可,而领域驱动设计最常见的则是四层式领域驱动设计(用户界面层,应用层,领域层,基础设施层)。
分层式架构体现了”关注度分离“的原则,在进行软件开发过程中,可以根据需求,找到对应的逻辑分层,进行代码实现;有时不同逻辑分层的组件会以各自不同的发展速度迭代以满足不同的需求;在适当的情况下,还能采用分布式架构,让不同层运行在不同的基础设施中,期间通过rpc等方式保持通信,给架构留下了足够的弹性空间。
设计分层式架构并非越多越好,尽量控制在三到四层就足够了,不然会陷入”千层饼“的陷阱,过多的分层和过少的分层,其实没有任何区别。
对于后端工程师来说,理解分层式架构并不困难,难的是要识别哪里逻辑代码应该归属于哪一层;而许多对于方兴未艾的前端技术来说,如何分层,却似乎并不是一件容易的事,由于前端业务要适应来自用户层面的无穷变化,很容易就陷入“意大利面”式的代码混乱中。vuex框架为前端开发者提供了一种良好的示例,有时无需深入了解vuex的机制,只需"模仿"这种分层方法,就能写出更加易于维护的前端代码了。
3、模块(类库)
模块的设计和耦合性
在.NET开发中,模块有时是一个独立的项目,并以一个独立dll(类库)的形式进行分发。模块也是最为常见的一种代码实践,但在《领域驱动设计·软件核心复杂性应对之道》一书中,作者埃里克·埃文斯却指出模块的运用,引起了“认知过载”的问题:
认知负荷理论认为,在问题解决和学习过程中的各种认知加工活动均需要消耗认知资源,若所有活动所需的资源总量超过个体拥有的资源总量,就会引起资源的分配不足,从而影响个体学习或问题解决的效率,这种问题就说“认知过载”。
这段理论确实有点拗口,对应到软件开发过程中,用通俗的说法,就是这个包承载的知识量太大了,把原本可以分离到多个模块中的逻辑代码都囊括进来,使得其反而降低了开发的效率。
尤其是类库的定义,不同的开发者有不同的习惯,有时按技术来划分,有时又按业务场景来划分,有时分拆,有时组合,“千人千面”,不连贯的设计思想,和“能用就行”的想法混合在一起,很容易就造成了一锅粥的情况。
在.NET项目中,每用一个using,就引入了一种耦合,而使用了new方法,创建了一个对象的示例,又引入了一个对象的耦合。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using xxx.Core;
using xxx.Infrastructure.Extension;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
而设计优良的代码模块,则可以让依赖尽可能的减少。
模块其实也是实践“高内聚,低耦合”思想的主要阵地,如果业务相关性很高的对象被划分到不同的模块中,往往会使得开发者很难理解它们在业务上的作用,也会导致模块间的耦合进一步提高。
因此,好的模块设计应该将那些具有紧密概念关系的模型元素集中在一起,并能描述该模型元素的职能,使之成为一个内聚的概念集合。
组件设计的原则
关于如何设计模块,在《敏捷软件开发 原则、模式与实践》一书中,作者引述了以下设计原则基于粒度这个角度为组件的内聚性进行描述:
重用-发布等价原则(REP)
重用的粒度就是发布的粒度。REP指出,一个组件的重用粒度可以和发布粒度一样大。我们所重用的任何东西都必须被发布和跟踪。简单的编写一个类,然后声称它是可重用的做法是不现实的。只有在建立一个跟踪系统,为潜在的使用者提供所需要的变更通知、安全性以及支持后,重用才有可能。
共同重用原则(CRP)
一个组件中的所有类应该是共同重用的,如果重用了组件中的一个类,那么就要重用组件中的所有类。
共同封闭原则(CCP)
组件中的所有类对于同一种性质的变化应该是共同封闭的。一个变化若是对一个封闭的组件产生影响,则将对组件中所有的类产生影响,而对其他组件则不造成任何影响。
从稳定性的角度为组件的内聚性进行描述:
无环依赖原则:
在组件中的依赖关系图中,不允许存在环。
稳定抽象原则
朝着稳定的方向进行依赖。
设计不能是完全静态的。要使设计可维护,某种程度的易变性是必要的。我们通过遵循共同封闭原则来达到这个目标。使用这个原则,可以创建对某些变化类型敏感的组件。这些组件设计为可变的。我们期望他们变化。
稳定抽象原则
组件的抽象程度应该与其稳定程度一致。
4、代码格式
类的基本结构
代码格式,就是一个C#代码文件的逻辑结构。写代码其实是一件成本很低的事,但维护代码,却是一件成本很高的事。开发一个功能,只需短短几十分钟时间,但如果我们要去找出代码中存在的缺陷,却往往需要花费大量的时间。
这就客观上要求,我们书写的代码应该尽量方便阅读(可读性)、检索(快速找问题)、易于维护,而书写出“格式化”的代码,大概是我们能够提高代码质量的第一步。
对于书写的代码,大部分都是从上往下阅读,在需要阅读的代码较多量时,往往会选择折叠到定义,这样就能一眼看出每个方法的用途,要达到这个效果,就意味着我们需要精心设计安排代码的垂直格式。有经验的开发者往往会按照这种结构。
私有字段:定义类内部的基本成员,高层次概念,常量,和引入的算法。
构造函数:定义类的创建过程。
公共方法:定义类为外部暴露的行为。
私有方法:定义类为内部提供的行为。
类的格式要求
在《代码整洁之道》这本书中,作者介绍了他对于代码的格式要求:
垂直格式
代码文件的长度控制在200-500行左右,且短文件通常比长文件易于理解。垂直阅读时,顶部是粗线条概述,隐藏了故事细节,然后再不断展开。
每行展示一个表达式或一个子句,尤其是C#的链式语法,尽量一行代码就是一个方法。
entity.Property(e => e.Memo)
.HasMaxLength(500)
.IsUnicode(false)
.HasComment("备注");
每组代码行展示一个完整的思路,思路间用空白行隔开。垂直方向上,靠近的代码可以展示它们之间的紧密关系,能够让代码更好阅读。
变量声明应尽可能靠近其使用位置,因为函数很短,本地变量应该在函数的顶部出现。一个函数调用了另外一个函数,应该把它们放到一起,且调用者应该在被调用者上面。概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短。
横向格式
横向首先表现在代码的宽度上,尽量控制在一行代码不超过120个字符。
水平方向上,可以用空格字符把彼此紧密相关的变量或对象连接在一起,也可以用空格将相关性较弱的对象分割开。
注意水平缩进和左对齐,尤其是上面提到的链式语法,如果点号没对齐,简直让人难受。
entity.Property(e => e.UserId).HasMaxLength(10).IsUnicode(false)
.IsFixedLength();
小结
本文对如何编写高质量代码进行了一些简单的概述,介绍了代码的分层、组件(包)的设计、以及整洁代码中的一些开发实践,通过了解这些知识,能够让我们逐渐形成自己对于代码的体会,并通过不断的练习,将能够提高我们的代码能力,进而形成所谓“代码感”。
下一篇,将对规范命名、注释、设计向量、设计原则、设计模式进行一些讨论。