第三章 接口与API设计—第19条:使用清晰而协调的命名方式

类、方法及变量的命名是Objective-C编程的重要环节。新手通常会觉得这门语言很繁琐,因为其语法结构使得代码读起来和句子一样。名称中一般都带有"in"、"for"、"with"等介词,其他编程语言则很少使用这些它们认为多余的字眼。以下面这段代码为例:

NSString *text = @"The quick brown fox jumped over the lazy dog";
NSString *newText = [text stringByReplacingOccurrencesOfString:@"fox" withString:@"cat"];

此代码用了比较啰嗦的方式来描述一个看上去如此简单的表达式。对于执行替换操作的那个方法,其名字居然有48个字符长。不过这样做的好处是,代码读起来像日常语言里的句子:
"将文本中出现的"fox"字符串替换为"cat",并返回替换后的新字符串。"(Take text and give me a new string by replacing the occurrences of the string 'fox' with the string 'cat')
这个句子准确描述了开发者想做的事。在命名不像Objective-C这般复杂的语言中,类似的程序可能会写成这样:

string text = "The quick brown fox jumped over the lazy dog";
string newText = text.replace("fox", "cat");

这样写有个问题,就是text.replace()的两个参数到底按何种顺序解读。是"fox"为"cat"所替换,还是"cat"为"fox"所替换呢?还有个疑问:replace函数是把所有出现的字符串都替换掉呢,还是只替换掉第一次找见的那个?其名称没能清除地表达出这两个意思。而Objective-C的命名方式虽然长一点,但是却非常清晰。
读者也会注意到,方法与变量名使用了"驼峰式大小写命名法"(camel casing)--以小写字母开头,其后每个单词首字母大写。类名也用驼峰命名法,不过其首字母要大写,而且前面通常还有两三个前缀字母(参见第15条)。在编写Objective-C代码时,大家一般都使用这种命名方式。如果你愿意,也可使用自己的风格来命名,不过按照驼峰命名法写出来的代码更容易为其他Objective-C开发者所接受。

方法命名
你要是写过C++或Java代码的话,应该会习惯那种较为简省的函数名,在那种命名方式下,若想知道每个参数的用途,就得查看函数原型。这会令代码难于读懂: 为了明白函数用法,你必须经常回过头来参照其原型。比方说,要写一个表示矩形的类。用C++代码可以这样定义此类:

class Rectangle {
public:
    Rectangle(float width, float height);
    float getWidth();
    float getHeight();
private:
    float width;
    float height;
};

不熟悉C++也没关系,你只要知道这个类包含名为width及height的两个实例变量就好。若想创建该类的实例,只有一种办法,就是以矩形尺寸为参数,调用其"构造器"(constructor),宽度与高度都有对应的存取方法。可以用下面这行代码来创建该类的实例:

Rectangle *aRectangle = new Rectangle(5.0f, 10.0f);

回顾这行代码时,并不能一下子看出5.0f和10.0f表示什么。你可能觉得这两个参数是矩形尺寸,可是到底宽度在先还是高度在先呢?要想确定这一点,还得去查函数定义才行。
Objective-C语言就不会有这个问题了,因为其方法名可以起得更长一些。熟悉C++的人可能会像下面这样把Rectangle改写为等价的Objective-C代码:

#import 

@interface EOCRectangle : NSObject

@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

- (id)initWithSize:(float)width :(float)height;

@end

写这个类的人显然知道,在Objective-C语言中,和C++构造器等效的东西是init-系列方法,于是将其命名为"initWithSize:"。这看上去很奇怪,你也许觉得语法有误,第二个冒号前面怎么没有字呢?实际上语法完全没错,之所以会觉得写错了,是因为它和改写前的C++构造器有着想同的问题: 使用此类的开发者还是不清楚每个变量的含义:

EOCRectangle *aRectangle = 
    [[EOCRectangle alloc] initWithSize:5.0f :10.0f];

下面这种命名方式就要好很多:

- (id)initWithWidth:(float)width andHeight:(float)height;

这么写是很长,然而这次绝对不会混淆每个变量的含义了:

EOCRectangle *aRectangle = 
    [[EOCRectangle alloc] initWithWidth:5.0f andHeight:10.0f];

虽说使用长名字可令代码更为易读,但是Objective-C新手还是难于习惯这种详尽的方法命名风格。不要吝于使用长方法名。把方法名起的稍微长一点,可以保证其能准确传达出方法所执行的任务。然而方法名也不能长得太过分了,应尽量言简意赅。
以EOCRectangle类为例。好的方法名应该像这样:

- (EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle
- (float)area

而下面这种命名方式咋不好:

- (EOCRectangle*)union:(EOCRectangle*)rectangle // Unclear
- (float)calculateTheArea // Too verbose

清晰的方法名从左至右读起来好似一段文章。并不是说非得按照那些命名规则来给方法起名,不过这样做可以令代码变得更好维护,而且也能使其他人更易读懂。
NSString这类就展示了一套良好的命名习惯。下面列出几个方法及其命名缘由:

  • +string
    工厂方法(factory method),用于创建新的空字符串。方法名清晰地描述了返回值的类型。
  • +stringWithString
    工厂方法,根据某字符串创建出与之内容相同的新字符串。与创建空字符串所用的那个工厂方法一样,方法名的第一个单词也指明了返回类型。
  • +localizedStringWithFormat:
    工厂方法,根据特定格式创建出新的"本地化字符串"(localized string)。返回值类型是方法名的第二个单词(string),因为其前面还有个修饰语(localized)用来描述其逻辑含义。此方法的返回值依然是"字符串"(string),只不过是一种经过本地化处理的特殊字符串。
  • -lowercaseString
    把字符串中的大写字母都转为小写。该方法不会像修改接收此消息的字符串本身,而是要新创建一个字符串,此做法也符合方法名中应该包含返回值类型这一规范,然而描述返回值类型的单词(string)前面还有个定语(lowercase)。
  • -intValue
    将字符串解析为整数。由于返回值是int,所以方法名以这个词开头。通常情况下我们不会像这样来简写返回值的类型,比如string不简写为str,然而由于"Integer"(整数)一词的简称"int"本身就是返回值的类型名称,所以此处这么做是合理的。为了把方法名凑足两个词,所以加了后缀"Value"。只有一个词的名字通常用来表示属性。由于int不是字符串对象的属性,所以要加Value以限定其含义。
  • -length
    获取字符串长度(也就是其字符个数)。这个方法只有一个词,因为实际上length也是字符串的一个属性。这个属性可能不是由实例变量来实现的,然而即便如此,它也依然是字符串中的属性。此方法若是命名为stringLength就不好了。string一词多余,因为该方法的接收者肯定是个字符串。
  • -lengthOfBytesUsingEncoding
    若字符串是以给定的编码格式(ASCII、UTF8、UTF6
    等)来编码的,则返回其字节数组的长度。此方法与length相似,所以其命名原因也和刚才说的一样。此外,该方法还需一个参数。该参数紧跟着方法名中描述其类型的那个名词(encoding)。
  • -getCharacters:range:
    获取字符串中给定范围内的字符。其他语言里的获取方法也许会以get开头,但Objective-C中一般不这么做,然而此处例外,该方法用get作其前缀。原因在于,调用此方法时,要在其首个参数中传入数组,而该方法所获取的字符正是要放到这个数组里面。此方法的完整签名为:
  • -(void)getCharacters:(unichar *)buffer range:(NSRange)aRange
    首个参数buffer应该指向一个足够大的数组,以便容纳所请求范围内的那些字符。此方法要通过其参数来返回(这种参数通常称为"输出参数'(out-parameter)), 而不通过返回值来返回,从内存管理的角度看,这样做更好。所有内存管理事宜均由方法调用者处理,而不是先在此方法中创建一个数组,然后再又调用者释放。第二个参数前有个描述其类型的名词(range),如果还有其他参数,也应该在方法名中提到其类型。有时参数名前面还会加介词,例如,此方法可以命名为"getCharacters:inRange:"。当需要特别强调众参数中的某一个时,通常会这样命名。
  • -hasPrefix:
    判断本字符串是否以另一个字符串开头。由于返回值是Boolean类型,所以为了读起来像个句子,这种方法的名称中通常都包括has("是否有")一词。例如:
[@"Effective Objective-C" hasPrefix:@"Effective"] == YES

要是把方法名直接写成"prefix",读起来就不这么顺了。反之,若将其叫成"isPrefixWith:"则听上去冗长而别扭。

  • -isEqualToString:
    判断两字符串是否相等。其返回值和"hasPrefix:"一样,都是Boolean型,为了便于述说,方法名用is开头。还有个地方也会用到is这个前缀词,那就是Boolean型的属性。比方说,有个属性叫做enabled,则其两个存取方法应该分别起名为"setEnabled:"与isEnabled。

给方法命名时的注意事项可总结成下面几条规则。

  • 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
  • 应该把表示参数类型的名词放在参数前面。
  • 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个动词。
  • 不要使用str这种简称,应该用string这样的全称。
  • Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
  • 将get这个前缀留给那些借由"输出参数"来保存返回值的方法,比如说,把返回值填充到"C语言式数组"(C-style array)里的那种方法就可以使用这个词做前缀。

类与协议的命名
应该为类与协议的名称加上前缀,以避免命名空间冲突(参见第15条),而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。例如,在NSArray的子类中,有一个用于表示可变数组的类,叫做NSMutableArray,mutable这个词放在array前面,用以表明这是一种特殊的array(数组)。
下面以iOS的UI库UIKit为例,演示类与协议的命名惯例:

  • UIView(类)
    所有"视图"(View)均继承于此类。视图是构造用户界面的基本单元,它们负责绘制按钮、文本框(text field)、表格等控件。这个类的名字无须解释即可自明题意(self-explanatory)、开头的两个字母"UI"是UIKit框架的通用前缀。
  • UIViewController(类)
    视图类(UIView)负责绘制视图,然而却不负责指定视图里面应该显示的内容。这项工作由本类,也就是"视图控制器"(view controller)来完成。其名称从左至右读起来很顺。
  • UITableView(类)
    这是一种特殊类型的视图,可以显示表格中的一系列条目。所以,它在超类(UIView)名称中的View一词前面加了Table这个修饰词,用以和其他类型的视图相区隔。在超类名称前加修饰语是一种常用的命名惯例。本类也可以叫做UITableView,不过这个名字无法完整传达出"视图"这个概念。开发者必须查看接口声明方能确定这一点。比方说,想创建一个专门用来显示图像的表格视图,那么就可以将这个继承自UITableView的子类命名为EOCImageTableView。不过这时要加上自己的前缀EOC,而不是沿用超类的前缀UI(UIKit框架中的类以UI为前缀)。这么做的原因在于,你不应该把自己的类放到其他框架的命名空间里面,那些框架以后也许会新建同名的类。
  • UITableViewController(类)
    正如UITableView是一种特殊的view(视图)一样,UITableViewController也是一种特殊的view controller(视图控制器),它专门用于控制表格视图。因此,其命名方式与UITableView类似。
  • UITableViewDelegate(协议)
    此协议定义了表格视图与其他对象之间的通信接口,命名时,把定义"委托接口"(delegate interface)的那个类名(UITableView)放在前面,后面加上Delegate一词,这样读起来顺口。(第23条详述了"委托模式"(Delegate pattern)。)

说了这么多,其中最重要的一点就是,命名方式应该协调一致。而且,如果要从其他框架中继承子类,那么务必遵循其命名惯例。比方说,要从UIView类中继承自定义的类,那么类名末尾的词必须是View。同理,若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上Delegate一词。如果能坚持这种命名习惯,那么在稍后回顾自己的代码或他人使用你所写的代码时,很容易就能理解其含义。

要点

  • 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
  • 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
  • 方法名里不要使用缩略后的类型名称。
  • 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。

你可能感兴趣的:(第三章 接口与API设计—第19条:使用清晰而协调的命名方式)