作者:marinewu,腾讯 PCG 客户端开发工程师
There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton
软件开发中一个著名的反直觉就是“起名儿”,这个看上去很平凡的任务实际上很有难度。身边统计学显示,越是有经验的程序员,越为起名头痛,给小孩起名儿都没这么费劲。
命名的困难可能来自于以下几个方面:
命名就像写作,会写字不等于会写作。而且,命名更多像是一门艺术[注](此处艺术的含义取自于 Knuth -- 命名会诉诸品味和个人判断。),不存在一个可复制的命名操作手册。
本文描述一些实用主义的、可操作的、基于经验的命名指南,并提供了一个代码词汇表,和部分近义词辨析。本文没有涉及讨论名字的形而上学,例如如何做更好的设计和抽象以利于命名,也没有涉及如何划分对象等,也无意讨论分析哲学。
命名是一门平衡准确性和简洁性的艺术 -- 名字应该包含足够的信息能够表达完整的含义,又应该不包含冗余的信息。
名字最重要的属性是准确。名字应该告诉用户这个对象/方法的意图 -- “它是什么” 和 “它能做什么”。 事实上,它是体现意图的第一媒介 -- 名字无法表现含义时读者才会阅读文档。
名字应该是有信息量的、无歧义的。以下一些策略可以增加名字的准确度:
最基本的语法原理,是一个类(Class/Record/Struct/... 随你喜欢)应该是一个名词,作为主语。一个方法应该是动词,作为谓语。 换言之,类“是什么”,方法“做什么”, 它们应该是可读的,应该是 [Object] [Does ...]
式的句子。
可读是字面意思,意味着它应该是通顺的,所以应该:
就像是给老板的汇报中不会把商业计划写成 Busi Plan 一样,也不应该在公开 API 中使用一些奇怪的缩写。现在已经不是 1970 年了,没有一个方法不能超过 8 个字符的限制。把类/方法的名字写全,对读者好一点,可以降低自己被同事打一顿的风险。
creat
是个错误,是个错误,是个错误!
但是,首字母缩略词的术语是可行并且推荐的,如 Http
, Id
, Url
。
以下是可用的、得到普遍认可的缩写:
未得到普遍认可的缩写:
req/resp/svr 在服务名称中很常见。这非常糟糕。请使用全称。
再次说明:以上的说明是针对 API 名称,具体包括公开对象/函数名字、RPC/Web API 名字。在局部变量使用缩写不受此影响。
对类/方法的命名,不要使用 2 表示 To, 4 表示 For。
func foo2Bar(f *Foo) *Bar // BADfunc fooToBar(f *Foo) *Bar // GOOD
func to(f *Foo) *Bar // Good if not ambiguous.
2/4 这种一般只有在大小写不敏感的场合才会使用,例如包名 e2e 比 endtoend 更可读。能区分大小写的场合,不要使用 2/4。
虽然不能完全符合语法(例如通常会省略冠词),但是,方法的命名应该尽量符合语法。例如:
class Car { void tireReplace(Tire tire); // BAD, reads like "Car's tire replaces"
void replaceTire(Tire tire); // GOOD, reads like "replace car's tire"
}
关于命名的语法见“语法规则”一章。
命名本质上是分类(taxonomy)。即,选择一个单一的分类,能够包含类的全部信息,作为名字。
考虑以下的角度:
例如,把大象装进冰箱,需要有三步 -- 打冰箱门打开,把大象放进去,把冰箱门关上。但是,这可以用单一的概念来描述:“放置”。
class Fridge {
public void openDoorAndMoveObjectIntoFridgeAndCloseDoor(Elephant elphant); // BAD public void put(Elephant elphant); // GOOD
}
避免使用过于宽泛的类别。例如,这世界上所有的对象都是“对象”,但显然,应该使用能够完整描述对象的、最细颗粒度的类别。
class Fridge {
public put(Elephant elephant); // GOOD. public dealWith(Elephant elephant); // BAD: deal with? Anything can be dealt with. How?
}
简而言之,名字应该是包含所有概念的分类的下确界。
名字长通常会包含更多信息,可以更准确地表意。但是,过长的名字会影响可读性。例如,“王浩然”是一个比“浩然·达拉崩吧斑得贝迪卜多比鲁翁·米娅莫拉苏娜丹尼谢莉红·迪菲特(defeat)·昆图库塔卡提考特苏瓦西拉松·蒙达鲁克硫斯伯古比奇巴勒·王”可能更好的名字。(来自于达啦崩吧)
在此,我提出一个可能会有争议的观点:所有的编程语言的命名风格应该是趋同的。不同于通常认为 Java 的命名会倾向于详尽,Go 的命名会倾向于精简,所有的语言对具体的“名字到底有多长”的建议应该是几乎一样的 -- 对外可见应该更详细,内部成员应该更精简。具体地:
上述规则像是 Go 的风格指南。但是,并没有规定 Java 不能这样做。事实上,Java 的冗长是 Java 程序员的自我束缚。即使在 Java 的代码里,也可以这样写:
public class BazelRuntime { public boolean exec(Command cmd) {
String m = cmd.mainCommand(); // YES, you can use single-letter variables in Java.
// ...
}
}
同样,在 Go 的代码中也不应该出现大量的无意义的缩写,尤其是导出的结构体和方法。
type struct Runtime {} // package name is bazel, so bazel prefix is unnecessarytype struct Rtm {} // BAD. DO NOT INVENT ABBREVIATION!
当然,由于语言特性,在命名风格上可能会有差异。例如,由于 Go 的导入结构体都需要加包前缀,所以结构名中通常不会重复包前缀;但 C++/Java 通常不会依赖包名。但是,上述的原则仍然是成立的 -- 可见度越高,应该越少依赖上下文,并且命名越详尽。
Google Go Style Guide 是唯一详尽讨论命名长度的风格指南,非常值得参考,并且不限于 Go 编程:
https://google.github.io/styleguide/go/decisions#variable-names
另一个容易被忽略的命名的黄金原则是一致性。换言之,名字的选取,在项目中应该保持一致。遵守代码规范,避免这方面的主观能动性,方便别人阅读代码。通常情况下,一个差的、但是达成共识的代码规范,也会远好于几个好的、但是被未达成共识的规范。
这个图我能用到下辈子: xkcd 927
但是仅符合代码规范是不够的。如同所有的语言,同一个概念,有多个正确的写法。
考虑以下的例子:
message Record {
int32 start_time_millis = 1; // OK int32 commited_at = 2; // Wait. Why not commit_time? Anything special?
int32 update_time = 3; // What unit? Also millis?
google.types.Timestamp end_time = 4; // WTF? Why only end_time is typed?
}
几种都是合理的(虽然不带单位值得商榷)。但是,如果在一个代码中出现了多种风格,使用起来很难预测。您也不想使用这样的 API 吧?
所以,在修改代码的时候,应该查看上下文,选择已有的处理方案。一致性大于其它要求,即使旧有的方案不是最好的,在做局部修改时,也应该保持一致。
另一个可考虑的建议是项目的技术负责人应该为项目准备项目的专有词汇表。
类应该是名词形式,通常由单个名词或名词短语组成。其中,主要名词会作为名词短语的末尾。例如 Thread, PriorityQueue, MergeRequestRepository。
ServiceOfBook
,也不是 BooksService
(省略 '),而是 BookService
。接口的命名规则和类相同。除此之外,当接口表示可行动类型时,可使用另一个语法,即 Verb-able
。例如:
public interface Serializable {
byte[] serialize();
}public interface Copyable<T> {
T copy();
}
public interface Closable {
void close();
}
(Go 通常不使用这个命令风格。只在 Java/C++ 中使用。)
只在 Java(注 1)中使用。一个类或概念所有的辅助方法应该聚合在同一个辅助类。这个类应该以被辅助类的复数形式出现。不推荐使用 Helper/Utils
后缀表示辅助类。尤其不推荐使用 Utils/Helpers
做类名,把所有的辅助方法包进去。如:
class Collections {} // For Collectionclass Strings {} // For String
class BaseRuleClasses {} // For BaseRuleClass
class StringUtils {} // WORSE!
class StringHelper {} // WORSE!
注 1: 客观来说,这适用于所有强制 OOP 的语言(所有强制把方法放在类里的语言)。但是除了 Java, 没有别的语言这么烦啦。
方法通常是谓语(动词),或是 谓宾(动词+名词) 结构。注意以上语法中,动词都在最前端。例如:
class Expander {
String expand(String attribute); // 主-谓 String expandAndTokenizeList(String attribute, List<String> values); // 主-谓-宾
}
除此之外,有以下特例值得注意:
直接使用所 Get 的对象的名词形式,即 Foo()
。不要使用 GetFoo()
。
Java: 所有的 Getter 都需要一个 get 前缀是来自于过时的 Java Beans Specification,以及 Javaer 的思想钢印。
func Counts() int; // GOODfunc GetCounts() int; // BAD: UNNECESSARY.
断言函数指返回结果是布尔型(即真伪值)的函数。它们通常有以下命名格式:
即 isAdjective()
或 areAdjective()
格式,表示是否具有某个二元属性。类似于 Getter,可以省略系语,只使用表语,即: adjective()
。
func IsDone() bool {} // OK-ish. But could be better.func Done() bool {} // GOOD. Why bother with is/are?
func CheckEnabled() bool { // BAD. Nobody cares if it is "checked". Just tell the user if it is enabled.
return enabled;
}
func Enabled() bool {} // GOOD.
情态动词也是常见的断言形式。常见的是以下三个:
func Compile(s string) Regexp, error // Returns error upon failurefunc MustCompile(s string) Regexp // Panics upon failure
func (r Regexp) CanExpand(s string) bool // Whether s is legal and can be expanded
func (r Regexp) Expands(s string) bool // Whether r expands s, i.e. r can expand s.
func (r Regexp) ShouldReset() bool // Whether the state requires reset. Does not perform de-facto reset.
func (r Regexp) Reset() // De-facto reset.
上文 "must" 的反面,表示尝试性的执行,并且失败不会造成严重后果:
void maybeExecute() {
if (!predicate()) {
return;
}
// execute
}std::unique_ptr<DateTime> ParseOrDie(std::string_view dateTime);
bool TryParse(string_view dateTime, DateTime* dateTime);
另一个常见场景是我们希望表示类拥有某些属性,但是使用助动词并不合适。如果前文描述,常见的选择是使用第三人称单数的静态动词(Stative verb)(注 1) 表示类满足给定断言。
func (l *List) Contains(e interface{}) boolfunc (r Regexp) Expands(s string) bool
注 1: 简单地说,静态动词是表示状态的动词,与动态动词(Dynamic verb)表示动作对应。或言“持续性动词”。
一阶逻辑量词也是常见的前缀:
语法: <一阶量词><动词|形容词>
class Stream {
// Returns whether all elements of this stream match the provided predicate.
boolean allMatch(Predicate<? super T> p);
// Returns whether any elements of this stream match the provided predicate.
boolean anyMatch(Predicate<? super T> p);
// Returns whether no elements of this stream match the provided predicate.
boolean noneMatch(Predicate<? super T> predicate)
}
介词经常与某些动词固定搭配,因此,通常可以省略动词,而只使用介词作为方法名称。
class Foo { public List<T> toList(); // Convert to (Construct a new instance of) a new List. Creates a new list.
public List<T> asList(); // Return a List as a different **view**. Holds reference of the original reference.
static Foo of(); // Construct Foo as a factory method.
static Foo from(Bar); // Construct Foo from Bar.
Foo with(Bar); // Construct a new Foo by replacing Bar with new Bar.
void onClick(ClickEvent e); // Act upon click event.
}
参考资料:
下文按用途归类了常见动词和名词,并对同义近义词进行了辨析。
Abstract/Base Impl Default
I
前缀,或后缀 FooInterface
。Abstract/Base
前缀以明确属性。这是因为 Interface/Impl 是常见的,Class 也是常见的,但是基于继承的抽象类是特殊的、应该予以避免的,应该给予特殊标记。{InterfaceName}Impl
的方式命名。class BazelBuilderImpl implements BazelBuilder, AutoClosable, Serializable
。Impl
后缀。Default
通常用来命名默认的实现,即其它实现如果不存在会 fallback 到的实现。如果所有的实现都是平等地位,那么不要使用 Default
命名。// https://github.com/bazelbuild/bazel with some fake examplespublic interface SkyFunction {}
public abstract class AbstractFileChainUniquenessFunction implements SkyFunction {}
public class DefaultSkyFunction implements SkyFunction {}
public class BazelModuleInspectorFunction implements SkyFunction {}
public interface VisibilityProvider {}
public final class VisibilityProviderImpl {}
Impl
表示实现。Base
后缀以明确属性。这是因为 Interface/Impl 是常见的,Class 也是常见的,但是基于继承的抽象类是特殊的、应该予以避免的,应该给予特殊标记。// levelDB
// includes/db.h
class DB {
public:
virtual ~DB(); // MUST!
virtual Status Delete(const WriteOptions&, const Slice&) = 0;
}// db/db_impl.h
class DBImpl : public DB {}
// rocksDB
// Base class
class CacheShardBase {}
interface Foo/struct FooImpl
不应该出现。Base/Abstract
也极少出现。Exception/Error
所有的异常扩展应该以 Exception
为后缀。所有的错误应该以 Error
为后缀。 对异常和错误的区别请参见 https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html
public class IllegalArgumentException;public class OutOfMemoryError;
C++ 的 exception
通常指语法特性,与 throw
对应,而 error
可以用来表示具体的异常错误。
// stdlib
std::exception;
std::runtime_error
所有的错误都是 error
。因此,所有自定义的对 error
的扩展都以 Error
作为后缀。
os.PathError
Test
Java/Go/C++ 均使用 Test 作为测试类的后缀。
Module/Component
Module/Component 通常会在框架中使用。不同的语言/框架对于 Module/Component 有不同的定义。 在非框架代码中应该减少使用 Module/Componenet 等命名,因为可能与已有框架冲突,并且 Module/Componenet 过于宽泛而缺少实质意义。
Module/Component 是意义相近的词,都可以表示“模块”或者“组件”。两者虽然有细微的分别,但是框架通常都显式(即在文档中指定,或者通过框架语义约束)地把它们定义为框架语境下的某些结构层级。
总结,Module/Component 命名应该注意:
Service
Service 通常用于作为 C-S 架构下的服务提供者的名称的后缀,如:
HelloService
但除此之外,Service 可以表示任何长期存活的、提供功能的组件。例如:
BackgroundService // Android 后台服务ExecutorService // 线程池执行服务,也是服务
BAD: 不要使用 Svr 缩写。使用全称。
Holder/Container/Wrapper
Holder/Container/Wrapper 都表示“容器”,具有同一个意图:为一个类增加额外的数据/功能,例如:
通常的结构如下:
class ObjectHolder {
private final Object object;
// other stuff ... public Object object() {}
// Other methods
}
这三个词没有区别。在同一个项目中,应该保持一致。
Manager/Controller
Manager 和 Controller 是同义词。它们通常用来表示专门控制某些类的类。
这两个词有以下几个常见场景:
DownloadManager
, PackageManager
。即使如此,Manager/Controller 是无意义词汇,出现时充满了可疑的味道 -- 类应该管理它们自己。 Controller/Manager 多了一层抽象,而这很可能是多余的。 认真考虑是否需要 Manager/Controller。
Util/Utility/Utils/Helper/{ClassName}s
辅助类是强制 OOP 的语言(i.e. Java) 所需要的特殊类。通常它们是一些辅助方法的合集。
将与某个类型相关的辅助方法放在一个类中,并且以复数形式命名辅助类。如:
// Java std lib
public final class Strings {}public final class Lists {}
public final class Collections {}
避免使用 Util/Utility/Utils/Helper
。它们是无意义词汇。
使用全局方法。如果担心命名污染,将之置入更细粒度的 namespace。
使用全局方法。
Function/Predicate/Callback
Function 通常表示任意函数。 Predicate 表示命题,即通常返回类型为 bool
。 Callback 指回调函数,指将函数作为参数传递到其它代码的某段代码的引用。换言之, Function 可以作为 Callback 使用。因此,Callback 在现代函数式编程概念流行后,通常很少使用。
熟悉 Java 8 开始提供的函数式语义。如果可以使用标准库的语义,不要自己创建名词。 注意 Function
指单入参、单出参的函数。如果是多入参的函数,直接定义 FunctionalInterface 并且按用途命名,例如 OnClickListener.listen(Context, ClickEvent)
。
// java.util.function
Predicate<T> // f(T) -> bool
Function<T, R> // f(T) -> R
Consumer<T> // f(T) -> void
Supplier<T> // f() -> T
first-class 函数的标准类型为 std::function
。
C++ 表示命名函数对象的惯用法是 fun
。Stdlib 中会缩写 function
为 fun
,如 pmem_fun_ref
,因此与 stdlib 一致,在代码中不要使用 fn
或是 func
。
Go 通常使用 Func
或是 Fn
表示函数类型。
type ProviderFunc func(config ConfigSource, source PassPhraseSource) (Provider, error)type cancelFn func(context.Context) error
在同一个项目中,应该保持一致。
作为参数时,函数不会特意标明 Fn
,而是遵从普通的参数命名方式:
func Sort(less func(a, b string) int)
换言之,函数是一等公民。
类/方法通常都按它们的行为模式来命名。恰好,设计模式就归类抽象了很多行为模式。所以设计模式提供了很多好的名字。
Factory: 工厂模式。通常,使用工厂方法即可,不需要一个额外的工厂类。只有当工厂特别复杂,或者工厂有状态时再考虑使用工厂类。
Builder:构建者模式。一般来说 Builder 都是作为 inner class,如
class Foo {
static class FooBuilder {}
}
在 GoF 中 Adapter 本来是将一个类封装以可以被作为另一个类型被调用,这样调用方不需要额外改变代码。这种用途通常被内化到容器上,见上文[容器类]部分。
在现代,Adapter 更多地被作为 数据类 -> 数据类的转化,如常见的 pb -> pb:
class ProtoAdapter<S, T extends Message> {}
在 GoF 中 Decorator 本来是将一个类作为抽象类,通过组合+继承实现添加功能。实际上现代的编程实践中往往通过直接提供一个容器的封装提供装饰功能,见上文 [容器类]部分。 所以 GoF 式 Decorator 并不常见,除非像 Python 在语法层面提供了装饰器。在 Java 中类似的功能是注解。
GoF 中是非常基本的模式:由一个类负责接受请求,并把请求转发到合适的实例类中执行。
class RealPrinter {}class Printer {
RealPrinter printer;
}
Delegate 非常常见,也提供了两个名字,请注意区分:
Delegate
是被委任的对象。Delegator
是委任对象。
所以,通常情况下 Delegator
在命名中会更常见,类似于 Dispatcher
。Delegate
更多作为一个类型或是接口被实现。具体的选择参见 [编排] 部分。GoF 中 Facade Pattern 通常是指为子系统提供一个更高层的统一界面,屏蔽子系统的独有的细节。 在现实中,Facade 通常用来为非常复杂的类/系统定义一个较为简化的界面,如:
// proto, extremely complicated TripResponse
message TripResponse {
// ...
// ...
string last_message = 3279;
}class TripResponseFacade {
private final TripResponse response;
Trip trip();
Endpoint source(); // Abstracted and processed
Endpoint target(); // Abstracted and processed
}
Facade 与 Adapter 的主要区别在于 Facade 的主要目的是为了简化,或者说更高层次的抽象,并且通常简化的界面不服务于专门的对接类。 Adapter 通常是为了一个特定的对接类实现。
注意 Facade
命名通常可以省略。仅当你的意图是明确告知用户这是关于某个类的外观时使用。
GoF 中代理模式用来添加一层抽象,以对实际类进行控制,并添加某些行为(如 lazy/memoized),或是隐藏某些信息(例如可见性或是执行远程调用)。
Proxy 与 Facade 的区别在于 Proxy 通常是为了额外的控制/记录等行为,而非只是为了更高的抽象/简化。
注意 Proxy 作为代码模式时,通常不应该出现在命名之中。使用具体的 Proxy 的目的作为命名,如 LazyCar
或是 TracedRuntime
,而非 CarProxy
或是 RuntimeProxy
。
Proxy 还有另一个含义就是真正的“代理”,如代理服务器。在这种情况下,使用 Proxy 是合适且应该的。这也是另一个为什么代理模式不应该用 Proxy 命名的原因。
时至今日仍然最常见的模式之一。Interator 有以下两个术语,不要混淆:
访问者模式用来遍历一个结构内的多个对象。对象提供 accept(Visitor)
方法,调用 Visitor.visit
方法。
即使如此,Visitor
应该并不常见,因为它可以简单地被函数式的写法替换:
class Car {
void accept(Consumer<Car> visitor); // No longer need to define Visitor class.
}
Observer/Publisher/Subscriber/Producer/Consumer
时至今日最常见的模式之一。和事件驱动编程(Event-based)有紧密关系 -- Oberservable 发布消息,所有注册的 Obeserver 会接收消息。 Publisher/Subscriber 也是类似的,它们的区别在于 Observer 模式往往是强绑定的 -- 注册和分发通常在 Observable 类中实现; 而 PubSub 模式通常有专门的 Message Broker,即 Publisher 与 Subscriber 是完全解耦的。
PubSub 与 Producer/Consumer 的区别是:
所有的消息注册的模式由三部分组成:
关于命名参见 [事件] 部分。
Strategy/Policy
策略模式在 GoF 中用以指定某个行为在不同场景下的不同实现,作为“策略”。
Strategy 模式往往不是很显式。现代通常使用 Strategy 表示实际的“策略”,即对信息不同的处理策略,而不采取 Strategy 模式的含义。
在“策略”这个语义中,Strategy/Policy 没有区别。在同一个项目中,应该保持一致。
命令模式在 GoF 中以类开代表实际行动,将行动封装,以支持重复、取消等操作。
Command 在现代编程实践中可以通过简单的函数式方案替换,如:
Function<T, T> command; // Javastd::function<const T&(const T&)> command; // C++type Command func(T*) T* // Go
现代通常使用 Command 表示实际的“命令”,而不采取 Command 模式的含义。
Tombstone
Null Object 模式不在 GoF 当中。它是一个用来代替 null 的 object,对其所有的操作都会被吞掉。 Null Object 主要是为了避免空指针。 合理的零值,例如 go time.Time = 0,也可以理解为一种 Null Object。
通常会有一个专门的对象表示 Null Object。可以借用 Tombstone
表示 Null Object。
Pool
对象池模式不在 GoF 当中。它是将一系列昂贵的对象创建好放在一个池子中,并使用户通过向池子申请对象,而不再自己手动地创建/销毁对象。最著名的池化的例子是线程池,即 ThreadPool。
Pool 通常用来表示对象池子,例如 ThreadPool, ConnectionPool
。
Arena
Arena 是指 Region-based memory management,是指一片连续的内存空间,用户在其中分配创建对象,管理内存。
Concurrent Synchronized Async
有时候我们需要特别标明一个类是线程安全的。通常这是特意为了与另一个线程不安全的实现做区分。典型的例子是 HashMap
和 ConcurrentHashMap
。如果一个类只是单纯是线程安全的,那么通常不需要在名字里特意说明,在文档里说明即可。
例如:
/** This class is designed to be thread safe. */
class SomeClassThreadSafe {}/** This class is immutable thus thread safe. */
class SomeClassImmutable {}
Concurrent
通常是用来说明该类是线程安全的前缀。Synchronized
是另一个在 Java 中可用的标明类是线程安全的前缀。但是,这通常说明这个类是通过 synchronized
机制来保证线程安全的,所以只在 Java 中使用。
另一个常见的场景是同一个方法有两种实现:同步阻塞和异步不阻塞的。在这种情况下,通常会命名异常不阻塞的方法为 {synchronizedMethod}Async
,例如:
public T exec();
public Future<T> execAsync();
如果一个异步的方法并没有对应的同步方法,通常不需要加 Async
后缀。
在 Go 中,如果一个方法是意图在其它协程中异步执行,不需要加 Async
后缀。
Cached/Buffered Lazy Memoized
名词辨析:
注意 Buffered 不应该与 Buffer 混淆。 Buffer 作为名词专指“缓冲区”。 注意 Cached 不应该与 Cache 混淆。 Cache 作为名词专指“缓存”。
Cached/Buffered 应该在项目中是一致的。 Cached/Lazy/Memoized 取决于对象是被获取的,还是创建的,还是计算获得的。
Mutable Immutable
Mutable 显式地声明一个类是可变的,Immutable 显式地声明一个类是不可变的。 通常情况下,类似于并发安全性,是否可变应该在类文档中说明,而不应该在类名中,显得臃肿。只有当一个类同时有可变/不可变版本时,可以使用 Class/ImmutableClass
。
Object Data Value Record Entity Instance
上面几个都可以用来表示一个表示数据的类。但是这些词是典型的“无意义词汇”,如果把它们从名字中删除,仍然可以表示完整意义,那么应该删掉。
class CarObject {} // Bad
class CarEntity {} // Bad
class CarInstance {} // Bad
class Car {} // Goodclass MapKey {}
class MapValue {} // OK. Couldn't be shortened.
class LoggingMetricsData {} // Bad
class LoggingMetricsValue {} // Bad
class LoggingMetricsRecord {} // Bad
class Logging Metrics {} // Good
class DrivingRecord {} // OK. Couldn't be shortened.
Statistics/Stats
表示“统计数据”。 Stats 是公认的可用的 Statistics 的缩写,Java/C++/Go 均可。
Storage Database Store DB
Cache
Verbs: - save/store/put
Storage/Database/Store/DB 都可以作为“存储服务”,即广义上的“数据库”(不是必须是完整的 DBMS)。 其中,在 C++/Go 中 DB 是常见且可接受的。在 Java 中通常使用全称。
项目内应该选择一个术语保持一致。
save/store/put
在数据库类中是同义词。同一个项目中应该保持一致。
Schema Index Format Pattern
名词辨析:
Hash/Digest/Fingerprint/Checksum
Hash/Digest 哈希是一种将任何数据映射到一个较小的空间的方法。映射通常被称为**哈希函数(Hash Function),映射值通常被称为摘要(Digest)**。
Hash(Data) = Digest
Checksum 出自编码论,可以理解为一种特殊的哈希函数,用来检查文件的完整性。换言之,如果一份数据出现了任何变动,Checksum 应该期待会改变。(但是 Checksum 实际上并不要求唯一性,见 Fingerpint)
Fingerprint 类似于 Checksum,但是 Fingerprint 通常更严格,它通常要求最少有 64-bit,使得任何两个文件只要不同,几乎(概率意义上接近 2^-64)不可能有同一份指纹,即唯一性。(但是 Fingerprint 的定义不要求密码安全性即 cryptographic)
所以 Checksum 只是作为文件变更校验,而 Fingerprint 可以作为数据的唯一标记。
在命名时,优先使用 Fingerprint/Checksum,或其它特定指定用途的术语。当以上均不合适时,回退到更泛化的概念,即 Digest。
Stream Source/Sink Pipe/Piped
流式编程通常有自己的专有词汇表。具体地:
原则是:选择你的团队里最常使用的流式处理系统所使用的词汇表。
State/Status
很讽刺地,很多人认为这两个词有区别,但是他们认为区别的点各不相同。见下文参考文献。笔者倾向于认为它们其实没什么本质区别。
鼓励使用 State 表示状态。因为 HTTP 和 RPC 已经占用了 Status 这个术语,为了避免误解,使用 State 表示自定义状态。
参考:
Num/Count/Size/Length/Capacity
动词是句子的精髓。选择精准的动词是代码可读性的关键。 本章对动作做了分类,并且提供了部分备选。如果动词有反义词,它们会被聚合在一个词条中。 本章的词汇有两种:
Producer/Provider/Supplier/Generator/Constructor/Factory Builder.build
Verbs:
创建/提供名词辨析:
动词辨析:
Consumer.accept/consume/poll
消费名词:
注意区分轮 xun 中文的歧义:
注意轮询是 poll 不是 pull,虽然后者直觉上是“拉取,但 poll 强制间断性地主动地采样/获取数据,是正式的计算机术语。
Verbs: - find/search/query
同义词。推荐在项目中保持一致。 具体地,这几个词实际上有细微的不一致。通常情况下它们可能有以下区分:
参考 https://stackoverflow.com/questions/480811/semantic-difference-between-find-and-search
Verbs: - copy/clone
同义词。遵循语言惯例。
Java 使用 clone。 Go/C++ 使用 copy。
Verbs: - add/append/put/insert/push
动词辨析:
add(E)
等同于 append
,add(index, E)
等同于 insert
。addAll
用于批量添加。对于自定义的可添加 api,应该贴近底层的标准库的数据结构所使用的动词。作为泛用的添加,使用 add。
Verbs: - set/update/edit
同义词。在代码 API 中使用 set,在 RPC API 中使用 update。
Verbs: - remove/delete/erase/clear/pop
动词辨析:
Scheduler/Dispatcher/Coordinator/Orchestrator/Delegator - Verb: schedule/dispatch/orchestrate
Scheduler/Dispatcher 均借用于操作系统概念。
名词辨析:
Validator/Checker/Verifier - Verb: validate/check/verify/assert
Validation/Verification 的明确区分来自于软件测试。
在程序中,不沿用这种区分。通常:
具体地:
public void process(String s, ComplicatedObject co) {
checkNotNull(s); // check
validateComplicatedObject(co); // validate
}@Test
public void testProcess() {
process("ss", co);
Truth.assertThat(...); // assert
verifyZeroInvocations(co); // verify
}
Task/Job/Runnable
Executor/Operator/Processor/Runner - Verb: exec/execute/do/process/run
名词辨析:
但是,推荐不做区分,认为它们都是同义词。使用 Task 或者 Job 作为类名。
名词辨析: Processor/Executor/Operator 是从计算机架构借用的概念。
但是,推荐不做区分,认为它们都是同义词。日常编程中,使用 Executor 作为 Job 执行器。
toggle/switch/enable/disable/turnOn/turnOff/activate/deactivate
二元状态的开启关闭。上述全是同义词。
在项目中保持统一。注意比起 toggle(bool)
和 switch(bool)
,更推荐分离的 enable/disable
。
Reader/Prefetcher/Fetcher/Downloader/Loader - Verb: read/get/fetch/load/retrieve Writer/Uploader - Verb: write/upload
Lifecycle: - open/close
名词辨析:
优先使用 read/fetch/download,当均不合适时,回退到 load。
Serializer - Verb: serialize/pack/marshal Deserializer - Verb: deserialize/unpack/unmarshal
动词辨析:
但是,不需要做这个区分。可以认为它们都是同义词。按语言惯例使用:
注意反序列化是 deserialize, 比 unserialize 更常见。 但 pack -> unpack, marshal -> unmarshal。
Applier/Converter/Transformer/Mapper - Verb: apply/convert/transform/map/to/translate
可以认为它们都是同义词。在项目中应该保持一致。 严格来说,Mapper 更多指同一数据的两种形式的双向映射,例如数据库存储和运行时对象。 在 Applier/Converter/Transformer 中,Applier 最为常见,因为源自设计模式。 Mapper 在框架中较常见。
Filter/Matcher - Verb: query/filter/match
可以认为它们都是同义词。 在项目中应该保持一致。
Event
Listener/Notifier Verbs: notify Observer/Observable Verbs: observe Handler Verbs: handle Publisher/Subscriber Publisher/Consumer
在 [Observer Pattner: 观察者模式] 中已经解释。
见 https://stackoverflow.com/questions/42471870/publish-subscribe-vs-producer-consumer
Regex/Pattern/Template
Pruner/Stripper/Trimmer Formatter/Prettier Resolver/Parser/Expander
- Verb: compile/parse/resolve/expand - Verb: format/split/separate/merge/join
通常,一个程序中有 20% 的代码在处理字符串。所以与文本相关的内容非常多。这里没有列出全部。
“模板”名词解析:
“修剪”动名词解析:
但是,Prune/Strip/Trim 在编程中通常认为是同义词。它们通常情况下:
语言可能会为之赋予特殊含义,例如在 Java 11 中,Trim 会清理掉所有的普通空格,而 Strip 会清理掉所有的 Unicode 空格。
“格式化”动名词解析:
“解析”动名词解析:
Lifecycle
Initializer/Finalizer
Verb: - init/setup/prepare - pause/resume - start/begin - end/terminate/stop/halt - destroy/release/shutdown/teardown
生命周期解析: 一个对象的生命周期,称为 Lifecycle,通常有以下流程:
Calculator
Verb: - compute/calculate/calc
使用 Calculator 而非 Computer 表示某个运算的执行器。Computer 虽然也是“计算器”,但是在代码语境下有歧义。
compute/calculate/calc 可以认为是同义词。如果是 Calculator,使用 calculate。其它情况下,使用 compute。
Option/Config/Configuration/Setting/Preference/Property/Parameter/Argument
Context/Environment
Info/Metadata/Manifest/Version
配置名词解析: 这个有类似的名词辨析,但是它们在编程时通常认为都是“配置”的同义词。它们还会出现在用户界面,尤其是 Settings/Options/Preferences。
在编程的角度,Option/Config/Configuration 是同义词,均表示配置。惯例使用 Options
作为子类定义一个类所需的配置,尤其是作为依赖注入时。
使用 Property 表示单个属性, 而且通常是 k-v 结构。换言之,Option/Config 通常由多个 Properties 组织。只有当 Property 是动态属性时,才定义特殊的 Property 类,否则,在 Option 中定义具体的域表示 Property。
struct Options {
int fur_layer_count; // Good
int fur_layer_count_property; // Bad! Property unnecessary struct ColorProperty {
int a;
int r;
int g;
int b;
} // Bad! Prefer Color.
ColorProperty color;
}
参数解析:
例如:
func foo(param string)foo(arg)
https://stackoverflow.com/questions/156767/whats-the-difference-between-an-argument-and-a-parameter
上下文名词辨析:
元数据辨析:
TrpcVersion
。如果一个类专指版本,使用 Version 是最精确合适的。