编程中的命名设计那点事-陈皓
在我开始设计系统的时候,我会花去很多时间去设计命名,因为好的命名和好的设计是分不开的。
In the beginning was the Word, and the Word was with God, and the Word was God
太初有道。道与神同在,道就是神。 (约翰福音第一章,第一节)
在设计过程中给类,方法和函数好的命名会带来好的设计,虽然这不是一定成立,但是如果坏的命名那一定不会给你带来好的设计。在设计过程,如果你发现你很难命名某一个模块,某个方法时,可能你真正遇到的问题不是难命名的问题,而是这个设计是否真的合理,你或许应该花更多的时间来重新设计一下你的模块。
好的命名不仅会带来好的设计,好的命名还提高了程序的可读性,降低代码维护的成本。另一方面,如果糟糕的命名会给代码带来一堵无形的墙,让你必须深入代码去研究代码具有的行为,增加你理解代码的时间。
为此我总结了几条关于命名的指导原则,希望这几条原则能为你的命名设计带来帮助,我使用的是C++的语法,当然这些原则也很容易扩展到其他语言中去。
类型命名(类,接口,和结构)
名字应该尽量采用名词Bad: Happy
Good: Happiness
不要使用类似名字空间的前缀Bad: SystemOnlineMessage
Good: System::Online:Message
形容词不要用太多,能描述清楚就行Bad: IAbstractFactoryPatternBase
Good: IFactory
在类型中不要使用Manager 或则 Helper 或则其他没意义的单词
如果你一定要在一个类型上加上Manager或Helper,那么这个类型要么就是命名的非常糟糕,要么就是设计的非常糟糕,如果是后则,那么这个类型就应该管理manage和帮助help一下自己了。Bad: ConnectionManager
XmlHelper
Good: Connection
XmlDocument, XmlNode, etc.
如果某个类不能通过简单的命名来描述它具有的功能,可以考虑用类比的方式来命名
Bad: IncomingMessageQueue
CharacterArray
SpatialOrganizer
Good: Mailbox
String
Map
如果你使用类比,你就应该一致的使用它们Bad: Mailbox,DestinationID
Good: Mailbox,Address
函数(方法和过程)
简洁Bad: list.GetNumberOfItems()
Good: list.Count()
不要太简洁Bad: list.Verify()
Good: list.ContainsNull()
避免缩写Bad: list.Srt()
Good: list.Sort()
对于完成某件事情的函数使用动词Bad: obj.RefCount();
Good: list.Clear();
list.Sort();
obj.AddReference();
对于返回布尔型的函数,使用类似提问的方式Bad: list.Empty();
Good: list.IsEmpty();
list.Contains(item);
对于只是返回属性,而不改变状态的函数则使用名词Bad: list.GetCount();
Good: list.Count();
不要在函数名字中重复参数的名称Bad: list.AddItem(item);
handler.ReceiveMessage(msg);
Good: list.Add(item);
handler.Receive(msg);
不要方法的名字中重复此方法的类的名称Bad: list.AddToList(item);
Good: list.Add(item);
不要在函数的名字中加入返回类型,除非函数名必须以返回类型进行区别Bad: list.GetCountInt();
Good: list.GetCount();
message.GetIntValue();
message.GetFloatValue();
不要名字中使用And 或则 Or
如果你使用一个连接词来连接函数名,那么这个函数肯定是做了太多的事情,更好的做法是将其分成更小的函数来处理(类似面向对象设计准则中的责任单一原则)。
如果你想确保是这是一个原子的操作,那么你应该用一个名字来描述这个操作或一个类来封装他Bad: mail.VerifyAddressAndSendStatus();
Good: mail.VerifyAddress();
mail.SendStatus();
这是一篇非常优秀的文章,我用我的语言在组织了一下,如果喜欢英文的读者可以点击这里阅读原文
优质代码的十诫
1.- DRY: Don’t repeat yourself.
DRY 是一个最简单的法则,也是最容易被理解的。但它也可能是最难被应用的(因为要做到这样,我们需要在泛型设计上做相当的努力,这并不是一件容易的事)。它意味着,当我们在两个或多个地方的时候发现一些相似的代码的时候,我们需要把他们的共性抽象出来形一个唯一的新方法,并且改变现有的地方的代码让他们以一些合适的参数调用这个新的方法。
DRY 这一法则可能是编程届中最通用的法则了,目前为止,应该没有哪个程序员对这一法则存有异议。但是,我们却能发现,一些程序在编写单元测试(unit testing)时忘记了这一法则:让我们相像一下,当你改变一个类的若干接口,如果你没有使用DRY,那么,那些通过调用一系例类的接口的unit test的程序,都需要被手动的更改。比如:如果你的unit test的诸多test cases中没有使用一个标准共有的构造类的方法,而是每个test case自己去构造类的实例,那么,当类的构造函数被改变时,你需要修改多少个test cases啊。这就是不使用DRY法则所带来的恶果。
2.- 短小的方法.
至少,我们有下面三个不错的理由要求程序员们写下短小的方法。
- 代码会变得更容易阅读。
- 代码会变得更容易重用(短方法可以减少代码间的耦合程度)
- 代码会变得更容易测试。
3.- 良好的命名规范
使用不错的统一的命名规范可以让你的程序变得更容易阅读和维护,当一个类,一个函数,一个变量的名字达到了那种可以“望文生义”的境界话,我们就可以少一些文档,少一些沟通。文章《编程中的命名设计那点事 》可以给你一些提示。
4.- 赋予每个类正确的职责
一个类,一个职责,这类规则可以参考一下类的SOLID 法则。但我们这里强调的不是一种单一的职责,而是一个正确的职责。如果你有一个类叫Customer,我们就不应该让这个类有sales 的方法,我们只能让这个类有和Customer有最直接关系的方法。
5.- 把代码组织起来
把代码组织起来有两具层次。
- 物理层组织:无论你使用什么样的目录,包(package)或名字空间(namespace)等的结构,你需要把你的类用一种标准的方法组织起来,这样可以方便查找。这是一种物理性质的代码组织。
- 逻辑层组织: 所谓逻辑层,主要是说,我们如果把两个不同功能的类或方法通过某种规范联系和组织起来。这里主要关注的是程序模块间的接口。这就是我们经常见到的程序模块的架构。
6.- 创建大量的单元测试
单元测试是最接近BUG的地方,也是修改BUG成本最低的地方,同样也是决定整个软件质量好坏的成败的地方。所以,只要有可能,你就应该写更多的,更好的单元测试案例,这样当你未来有相应代码改变的时候,你可以很简单知道你代码的改变是否影响了其它单元。
7.- 经常重构你的代码
软件开发是一种持续的发现的过程,从而让你的代码可以跟上最新的实际需求的变化。所以,我们要经常重构自己的代码来跟上这样的变化。当然,重构是有风险的,并不是所有的重构都是成功的,也不是我们随时都可以重构代码。下面是两个重构代码的先要条件,以避免让你引入更多的BUG,或是把本来就烂的代码变得更烂。
- 有大量的单元测试来测试。正如前面所说,重构需要用大量的单元测试来做保障和测试。
- 每次重构都不要大,用点点滴滴的小的重构来代替那种大型的重构。有太多的时候,当我们一开始计划重构2000行代码,而在3个小时后,我们就放弃这个计划并把代码恢复到原始的版本。所以,我们推荐的是,重构最好是从点点滴滴积累起来的。
8.- 程序注释是邪恶的
这一条一定是充满争议的,大多数程序员都认为程序注释是非常好的,是的,没错,程序注释在理论上是非常不错的。但是,在实际过程序当中,程序员们写出来的注释却是很糟糕的(程序员的表达能力很有问题),从而导致了程序注释成为了一切邪恶的化身,也导致了我们在阅读程序的时,大多数时候,我们都不读注释而直接读代码。所以,在这里,我们并不是鼓励不写注释,而是——如果你的注释写得不够好的话,那么,你还不如把更重要的时间花在重构一下你的代码,让你的代码更加易读,更加清楚,这比会比注释更好。
9.- 注重接口,而不是实现
这是一个最经典的规则了。接口注重的是——“What”是抽象,实现注重的是——“How”是细节。接口相当于一种合同契约,而实际的细节相当于对这种合同契约的一种运作和实现。运作是可以很灵活的,而合同契约则需要是相对需要稳定和不变的。如果,一个接口没有设计好而需要经常性的变化的话,那我们可以试想一下,这代来的后果,这绝对会是一件成本很大的事情。所以,在软件开发和调设中,接口是重中之重,而不是实现。然而我们的程序员总是注重于实现细节,所以他们局部的代码写的非常不错,但软件整体却设计得相对较差。这点需要我们多多注意。
10.- 代码审查机制
所有人都会出错,一个人出错的概率是很大的,两个人出错的概率就会小一些,人多一些,出错的概率就会越来越小。因为,人多了,就能够从不同的角度看待一个事情,虽然这样可能导致无效率的争论,但比起软件产品release后出现问题的维护成本,这点成本算是相当值得的。所以,这就是我们需要让不同的人来reivew代码,代码审查机制不但是一种发现问题的最有效的机制,同时也是一种可以知识共享的机制。当然,对于Code Review来说,下面有几个基本原则:
- 审查者的能力一定要大于或等于代码作者的能力,不然,代码审查就成了一种对新手的training。
- 而且,为了让审查者真正负责起来,而不是在敷衍审查工作,我们需要让审查者对审查过的代码负主要责任,而不是代码的作者。
- 另外,好的代码审查应该不是当代码完成的时候,而是在代码编写的过程中,不断地迭代代码审查。好的实践的,无论代码是否完成,代码审核需要几天一次地不断地进行。
(我以我个人的语言叙述本文,并加入了我个人的经历,所以,请你在转载时请注意作者和出处,并且,请勿用于商业用途)
整洁代码的4个提示
虽然这样的文章非常的多,并且,就算是对于编程新手来说,也是非常的简单和显而见,但是,在我们进行Code Review过程中,我们还是能够看到那些非常混乱的代码,所以,有些时候,你会在想,是不是这样的规则太多了,导致我们的程序员记不住。虽然我们在以前的文章中一遍又一遍的说过(比如:《优质代码的十诫》),千言万语总结一下,无论你用什么样的语言,最最基本的编程原则就是下面这四条。
1 – 简短的方法
简单才会易读,简单才会容易,简单才能重用,简单才能保证质量。把一件事搞复杂,是一件简单的事;而把一件事变简单,这则是一件复杂的事。KISS-Keep it Simple Stupid是一种哲学,Do one thing, Do it best也是一种哲学。这些都是在告诉我们,做设计,做产品,不要把所有的东西一下子都考虑进来,否则将会让你的事情变成一团糟,剪不断理还乱,就是这样道理。把复杂的事情,困难的事情,逐步细化,分解成一个一个简单而单一的事情,然后再把他们拼装起来完成一个复杂的事情,是我们如何完成一个巨大并复杂的项目的通用方法。
编程也是这个道理,维护代码的成本会比你创造代码的成本要大得多,所以,一个简短的方法不但可以有利于阅读,维护,重用,同样在进行排错调试测试的时候也能起到巨大的帮助。比如,对于一个简单的方法或函数,单元测试,功能测试,性能测试、代码覆盖,质量保证都能变得相当简单,而这些众多的质量优良的方法最终组成了那质量过硬的最终产品,并让我们在以后的代码不断改进中继续充当重要的作用。
2 – 选择望文知意的直观的变量名和函数名
无论是变量名还是方法名,都不能太长或是太短。一个好的命名,应该是“自解释的”,直观的,望文知意的。通常来说,一个好的命名应该是知道这个变量/方法要干什么事情,比如GetComputerName(),isAdmin等等,对于变量名来说,通过其名字,我们可以知道这个变量的类型(整型,浮点,指针,……),种类(全局,成员,局部,静态,……)。关于命名的事情,可以查看《编程命名中的7+1个提示》和《编程中的命名设计那点事》查看更多的内容。
3 – 只写有意义的注释
代码写得好的话,是不需要注释的。与其花费大量的时候去写注释,还不如把这些时间花在代码重构上,简洁/易读的代码比详细的注释更有意义。另外,如果你需要使用你的注释来生成文档,那么也不需要太过复杂,这通常用来做API的文档,这个时候,关键不在于你是如何实现的,而是在于告诉别人完成什么样的事并如何使用之。总之,一句话,如果你的代码足够的简单和清楚,你是不需要写注释的。
4 – 让你的代码可读
你的代码并不只是让编译器去阅读的,你的代码更应该是让你的同事和其它人阅读的。所以,一定要遵守团队内部的那些最中规中矩的编程规范或代码风格,千万不要在代码中使你的小聪明或是偷懒或是hack代码,那样做的结果只会有两个,一个是你的代码会被后人骂得一无是处,另一个就是当你在以后维护你的代码时无异于搬起石头砸了自己的脚。编码坚持最基本的两个原则—— KISS 和DRY,剩下的就是顺从于自然。
编程命名中的7+1个提示
前几天Neo写过《编程中的命名设计那点事》,这里也有另外一篇和程序命名的文章,可以从另一个角度看看。
1.- 变量应该是尽可能的望文知意。千万不要使用教材中的命名方式。
- 好的变量: daysDateRange, flightNumber, carColor.
- 坏的变量: days, dRange, temp, data, aux…
在我们的日常工作中,有很大数量的开发人员喜欢使用短的变量名,而不是有含义的变量名。这主要是因为我们大学教科书的那些示例所造成的,人都是先入为主,所以,教科书中的那些很抽象,带着演示的变量命名影响了我们一代又一代的程序员,并影响了他们很多年。虽然那些短的,教材式的变量名,可能会让你少打一些字,但其实,这是非常非常不好的。因为软件的维护成本远远大于了软件的开发成本,如果你不取一个好的一点的变量名,那么当进行代码评审时,当进行bug fixing时,当进行代码重构时,当进行代码维护时,你的某个变量名可能会让你一头雾水,不知道所措,还可以会让你走入陷阱,造成更大的时间成本。所以,一个可阅读的代码必然和那些不错的变量名分不开,而这也能让你的软件间接上有更好的质量。
2.- 变量名不要太长,尽可能地简短
只有简单和简短的变量名才是容易阅读的。因为你的变量名一定会用于程序语句中,所以,为了让你的程序语句看起来的简短,你的变量名也应该短一点,不然写出来的一个表达式就会显得很复杂。
当然,在有些时候,一个有含义的变量名和一个简短的变量名可能存在一些冲突。这相当锻炼我们的语言能力——如果有最精炼的词语来表达最丰富的含义。如果实在做不到,那么,取一个有含义的变量名要比取一个简短的变量名更好一些。不管怎么样,我们希望即简短又有丰富的含义,但如果不能两全,那有含义优先级更高一些。
- 坏的变量:howLonDoesItTakeToOpenTheDoor, howBigIsTheMaterial…
- 好的变量:timeToOpenTheDoor, MaterialSize.
3.- 可以使用缩写,但需要有一些注释
有一些时候,我们需要使用一些缩写来命名变量,比如:用usr来表示user,用gp来表示group,用conf来表示configuration,用cwd来表示current working directory,用ptr来代码point to reference,等等,等等。缩写一般要用在大家可以看得懂的,而不是为了缩写而缩短一个单词,当然,如果你把缩写后的变量名加上注释,那就更加稳妥了。关于一些约定俗成的缩写,可参看本文的附录一。
4.- 使用合适的匈牙利命名规则
这里有一篇非常不错的英文文章告诉你 《什么是合适的匈牙利命名 》,这篇文章同时还告诉你如何去用他。基本上来说,匈牙利命名法主要是为变量加上某种前缀以标识这个变量的类型,或是一种方法的功能。其基本原则是:变量名=属性+类型+对象描述。
比如:在描述类型方面:指针p,函数fn,长整型 l,布尔b,浮点型(有时也指文件)f,双字 dw,字符串 sz,短整型 n,双精度浮点 d,无符号 u……等等。关于更多的命名规范,请参见附录二。
注意,匈牙利命名也是有不好的地方的,比如你要把一个整形改成一个浮点型,你除了要改变这个变量的类型,你还要改变这个变量的名字。这是相当麻烦的。而且,在某些时候,这种前缀式的命名可以反而让你不知所措。另外,在C++中,有了类以后,这种命名方法就显得不容易去实施了。所以,合适地使用匈牙利命名方式背后的思想是很关键的。
5.- 不要使用反逻辑来命名
- 好的命名: IsEnabled.
- 坏的命名: IsNotEnabled.
在阅读的时候,我们更喜欢正向的逻辑,而不是反向逻辑。这一规则不单单的命名,在条件语句中,我们也是要尽量不要使用这种反面的逻辑。如:if (! (isAdmin || isUser)),这样的语句很不符合人读代码的习惯,写成这样会更好一些——if (!isAdmin && !isUser)。
6.- 保持一致性
保持所有代码的一致性。使用相同的命名规则。这外世界上没有最好的命名规范。但有一点是可以确认的,那就是在一个代码库中,应该使用一致的命名规则,即使这个规则不那么好,但整个团队使用一致的就是好的。
7.- 附和应用程序的领域术语
在不同的领域中,不同的观念会有非常特别和不同的意思。例如:单词“order”并不总是意味着“次顺”,有些时候,其意味着“订单”,有些时候,意味着“命令”,有些时候,意为着“规则”。所以,在某个领域中,某些单词会有不同的含义,所以,这需要我们的命令去附和这些领域。
黄金法则- 花一些时间去思考去权衡一下你的变量名
当你设计好一个的变量名一个函数名的时候,别着急去使用他,停下来,想一想,这个变量名是否合适,是否还有更好的?也许你正在使用的是一个很不好的变量名。有些时候,需要我们权衡利弊一下,可能还要去和同事讨论一下。
总之,变量名是编程的第一步,第一步走好了,后面才走得好。试想,无论是你或你的同事在使用一些好的变量名编程是一件多么轻松的事啊。
附录:部分的缩写规范
完整单词 | 缩写 |
A | |
average | avg |
B | |
back | bk |
background | bg |
break | brk |
buffer | buf |
C | |
color | cr,clr |
control | ctrl |
D | |
data | dat |
delete | del |
document | doc |
E | |
edit | edt |
error | err |
escape | esc |
F | |
flag | flg |
form | frm |
G | |
grid | grd |
I | |
increment | inc |
information | info |
initial | init |
insert | ins |
image | img |
L | |
lable | lab |
length | len |
list | lst |
library | lib |
M | |
manager | mgr,mngr |
message | msg |
O | |
Oracle | Ora |
P | |
panorama | pano |
password | pwd |
picture | pic |
point | pt |
position | pos |
prn | |
program | prg |
S | |
server | srv |
source | src |
statistic | stat |
string | str |
Sybase | Syb |
T | |
temp | tmp |
text | txt |
U | |
user | usr |
W | |
window | win,wnd |
附录二、匈牙利命名法
a Array 数组 b BOOL (int) 布尔(整数) by Unsigned Char (Byte) 无符号字符(字节) c Char 字符(字节) cb Count of bytes 字节数 cr Color reference value 颜色(参考)值 cx Count of x (Short) x的集合(短整数) dw DWORD (unsigned long) 双字(无符号长整数) f Flags 标志(一般是有多位的数值) fn Function 函数 g_ global 全局的 h Handle 句柄 i Integer 整数 l Long 长整数 lp Long pointer 长指针 m_ Data member of a class 一个类的数据成员 n Short int 短整数 p Pointer 指针 s String 字符串 sz Zero terminated String 以0结尾的字符串 tm Text metric 文本规则 u Unsigned int 无符号整数 ul Unsigned long (ULONG) 无符号长整数 w WORD (unsigned short) 无符号短整数 x,y x, y coordinates (short) 坐标值/短整数 v void 空
有关项目的全局变量用g_开始,类成员变量用m_,局部变量若函数较大则可考虑用l_用以显示说明其是局部变量。
前缀 类型 例子 g_ 全局变量 g_Servers C 类或者结构体 CDocument,CPrintInfo m_ 成员变量 m_pDoc,m_nCustomers
VC常用前缀列表:
前缀 类型 描述 例子 ch char 8位字符 chGrade ch TCHAR 16位UNICODE类型字符 chName b BOOL 布尔变量 bEnabled n int 整型 nLength n UINT 无符号整型 nLength w WORD 16位无符号整型 wPos l LONG 32位有符号整型 lOffset dw DWORD 32位无符号整型 dwRange p * 内存模块指针,指针变量 pDoc lp FAR* 长指针 lpDoc lpsz LPSTR 32位字符串指针 lpszName lpsz LPCSTR 32位常量字符串指针 lpszName lpsz LPCTSTR 32位UNICODE类型常量指针 lpszName h handle Windows对象句柄 hWnd lpfn (*fn)() 回调函数指针 lpfnAbort
Windows对象名称缩写:
Windows对象 例子变量 MFC类 例子对象 HWND hWnd; CWnd* pWnd; HDLG hDlg; CDialog* pDlg; HDC hDC; CDC* pDC; HGDIOBJ hGdiObj; CGdiObject* pGdiObj; HPEN hPen; CPen* pPen; HBRUSH hBrush; CBrush* pBrush; HFONT hFont; CFont* pFont; HBITMAP hBitmap; CBitmap* pBitmap; HPALETTE hPalette; CPalette* pPalette; HRGN hRgn; CRgn* pRgn; HMENU hMenu; CMenu* pMenu; HWND hCtl; CStatic* pStatic; HWND hCtl; CButton* pBtn; HWND hCtl; CEdit* pEdit; HWND hCtl; CListBox* pListBox; HWND hCtl; CComboBox* pComboBox;