程序员“起名”头痛根治指南
Posted 程序员的店小二
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序员“起名”头痛根治指南相关的知识,希望对你有一定的参考价值。
软件开发中一个著名的反直觉就是“起名儿”,这个看上去很平凡的任务实际上很有难度。身边统计学显示,越是有经验的程序员,越为起名头痛,给小孩起名儿都没这么费劲。
命名的困难可能来自于以下几个方面:
信息压缩:命名的本质是把类/方法的信息提炼成一个或几个词汇,这本身需要对抽象模型的准确理解和概括。
预测未来:类/方法的职责可能会在未来有变化,现在起的名字需要考虑未来可能的变动。
语言能力:缺少正确的语法知识,或是缺少足够的词汇量。本来英文就不是大部分中国人的母语,更甚者,计算机的词汇表不同于日常交流词汇表,有大量黑话。
不良设计:混乱的职责分布、不清晰的抽象分层、错误的实现,都会导致无法起出好的名字。在这个意义上,起名字其实是对设计的测试: 如果起不出名字来,很可能是设计没做好 -- 重新想想设计吧。
命名就像写作,会写字不等于会写作。而且,命名更多像是一门艺术[注](此处艺术的含义取自于 Knuth -- 命名会诉诸品味和个人判断。),不存在一个可复制的命名操作手册。
本文描述一些实用主义的、可操作的、基于经验的命名指南,并提供了一个代码词汇表,和部分近义词辨析。本文没有涉及讨论名字的形而上学,例如如何做更好的设计和抽象以利于命名,也没有涉及如何划分对象等,也无意讨论分析哲学。
命名原则
命名是一门平衡准确性和简洁性的艺术 -- 名字应该包含足够的信息能够表达完整的含义,又应该不包含冗余的信息。
准确 Precision
名字最重要的属性是准确。名字应该告诉用户这个对象/方法的意图 -- “它是什么” 和 “它能做什么”。 事实上,它是体现意图的第一媒介 -- 名字无法表现含义时读者才会阅读文档。
名字应该是有信息量的、无歧义的。以下一些策略可以增加名字的准确度:
可读
最基本的语法原理,是一个类(Class/Record/Struct/... 随你喜欢)应该是一个名词,作为主语。一个方法应该是动词,作为谓语。 换言之,类“是什么”,方法“做什么”, 它们应该是可读的,应该是 [Object] [Does ...] 式的句子。
可读是字面意思,意味着它应该是通顺的,所以应该:
避免 API 中使用缩写
就像是给老板的汇报中不会把商业计划写成 Busi Plan 一样,也不应该在公开 API 中使用一些奇怪的缩写。现在已经不是 1970 年了,没有一个方法不能超过 8 个字符的限制。把类/方法的名字写全,对读者好一点,可以降低自己被同事打一顿的风险。
creat 是个错误,是个错误,是个错误!
但是,首字母缩略词的术语是可行并且推荐的,如 Http, Id, Url。
以下是可用的、得到普遍认可的缩写:
configuration -> config
identifier -> id
specification -> spec
statistics -> stats
database -> db (only common in Go)
regular expression -> re/regex/regexp
未得到普遍认可的缩写:
request -> req
response -> resp/rsp
service -> svr
object -> obj
metadata -> meta
business -> busi
req/resp/svr 在服务名称中很常见。这非常糟糕。请使用全称。
再次说明:以上的说明是针对 API 名称,具体包括公开对象/函数名字、RPC/Web API 名字。在局部变量使用缩写不受此影响。
避免双关
对类/方法的命名,不要使用 2 表示 To, 4 表示 For。
func foo2Bar(f *Foo) *Bar // BADfunc fooToBar(f *Foo) *Bar // GOODfunc 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?
简而言之,名字应该是包含所有概念的分类的下确界。
简洁 Simplicity
名字长通常会包含更多信息,可以更准确地表意。但是,过长的名字会影响可读性。例如,“王浩然”是一个比“浩然·达拉崩吧斑得贝迪卜多比鲁翁·米娅莫拉苏娜丹尼谢莉红·迪菲特(defeat)·昆图库塔卡提考特苏瓦西拉松·蒙达鲁克硫斯伯古比奇巴勒·王”可能更好的名字。(来自于达啦崩吧)
在此,我提出一个可能会有争议的观点:所有的编程语言的命名风格应该是趋同的。不同于通常认为 Java 的命名会倾向于详尽,Go 的命名会倾向于精简,所有的语言对具体的“名字到底有多长”的建议应该是几乎一样的 -- 对外可见应该更详细,内部成员应该更精简。具体地:
public,如 public 类的名字、public 方法的名字 - 应该详细、不使用缩写、减少依赖上下文。通常是完整名词短语。
non-public,如类成员、私有方法 - 不使用缩写、可以省略上下文。下界是单词,不应该使用单字符。
local,如函数的局部变量 - 基本上是风格是自由的。不影响可读性的前提下,例如函数方法长度很短,可以使用单字符指代成员。
上述规则像是 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
一致 Consistency
另一个容易被忽略的命名的黄金原则是一致性。换言之,名字的选取,在项目中应该保持一致。遵守代码规范,避免这方面的主观能动性,方便别人阅读代码。通常情况下,一个差的、但是达成共识的代码规范,也会远好于几个好的、但是被未达成共识的规范。
这个图我能用到下辈子: 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 Stringclass BaseRuleClasses // For BaseRuleClassclass StringUtils // WORSE!class StringHelper // WORSE!
注 1: 客观来说,这适用于所有强制 OOP 的语言(所有强制把方法放在类里的语言)。但是除了 Java, 没有别的语言这么烦啦。
方法
方法通常是谓语(动词),或是 谓宾(动词+名词) 结构。注意以上语法中,动词都在最前端。例如:
class Expander String expand(String attribute); // 主-谓 String expandAndTokenizeList(String attribute, List<String> values); // 主-谓-宾
除此之外,有以下特例值得注意:
访问器 Getter
直接使用所 Get 的对象的名词形式,即 Foo()。不要使用 GetFoo()。
Java: 所有的 Getter 都需要一个 get 前缀是来自于过时的 Java Beans Specification,以及 Javaer 的思想钢印。
func Counts() int; // GOODfunc GetCounts() int; // BAD: UNNECESSARY.
断言 Predicate
断言函数指返回结果是布尔型(即真伪值)的函数。它们通常有以下命名格式:
系动词: 主-系-表
即 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.
情态动词: 主-助谓-谓-(宾/表)
情态动词也是常见的断言形式。常见的是以下三个:
should: 查询当前是否应该执行给定的实义动词。
can: 查询当前类所在状态是否可以执行给定的实义动词。某些情况下,也可以使用第三人称单数作为更简洁的代替。
must: 特殊形式。不同于前两者,会执行给定的实义动词。must 表示执行必须成功,否则会抛出不可恢复错误 (throw/panic)。类似于 C++ 中常见的 OrDie 后缀。
func Compile(s string) Regexp, error // Returns error upon failurefunc MustCompile(s string) Regexp // Panics upon failurefunc (r Regexp) CanExpand(s string) bool // Whether s is legal and can be expandedfunc (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.
表尝试: 主-maybe/try-谓-(宾/表)
上文 "must" 的反面,表示尝试性的执行,并且失败不会造成严重后果:
maybe 前缀用以表示指定的行为有前置条件,也在方法中执行。如果前置条件不满足,不会执行指定行为。通常不会出现在公开 API。
try 通常用于 Try-Parse Pattern,用于避免抛出异常。
void maybeExecute() if (!predicate()) return; // executestd::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)表示动作对应。或言“持续性动词”。
一阶逻辑 First-order logic, Predicate Logic
一阶逻辑量词也是常见的前缀:
all 表示所有对象满足给定要求
any 表示任意对象满足给定要求
none 表示没有任何对象满足给定要求
语法: <一阶量词><动词|形容词>
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)
介词
介词经常与某些动词固定搭配,因此,通常可以省略动词,而只使用介词作为方法名称。
to: 转换至另一对象,等价于 convertTo。to 会产生一个全新的对象,通常不持有对原对象的引用。
as: 返回某个视图,等价于 returnViewAs。一个“视图(View)” 通常是对原有对象的另一角度的抽象,通常会持有对原有数据的引用,而不会产生新的数据。
of/from/with:构造新对象,等价于 createOutOf/createFrom/createWith。见下文“工厂模式”。
on: 监听事件,等价于 actUpon。见下文“事件”。
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.
参考资料:
[[Effective Java]] Item 68: Adhere to generally accepted naming conventions
词汇表
下文按用途归类了常见动词和名词,并对同义近义词进行了辨析。
类/名词
类继承
Abstract/Base Impl Default
Java
Interface: 通常不需要额外的表示。不要加 I 前缀,或后缀 FooInterface。
Abstract class: 通常会添加 Abstract/Base 前缀以明确属性。这是因为 Interface/Impl 是常见的,Class 也是常见的,但是基于继承的抽象类是特殊的、应该予以避免的,应该给予特殊标记。
Implementation:
如果不实现接口,通常不需要任何特殊修饰符。
如果以 "is-a" 的方式实现了某个接口,那么通常实现会以 InterfaceNameImpl 的方式命名。
如果一个类实现了多个接口,那么通常这个类应该是以作为主要目标的接口为基础命名。例如 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
C++
C++ 的 interface 是通过抽象类不存在基类成员变量模拟。通常接口所有的成员函数都是公开纯虚函数。
使用 Impl 表示实现。
Abstract class: 通常会添加 Base 后缀以明确属性。这是因为 Interface/Impl 是常见的,Class 也是常见的,但是基于继承的抽象类是特殊的、应该予以避免的,应该给予特殊标记。
// levelDB// includes/db.hclass DB public: virtual ~DB(); // MUST! virtual Status Delete(const WriteOptions&, const Slice&) = 0;// db/db_impl.hclass DBImpl : public DB // rocksDB// Base classclass CacheShardBase
Go
Go 的 interface 从来不是用来做 "is-a" 定义的。Go 的 interface 契约通过 duck typing 满足。interface 应该在消费方定义,而非提供方。因此, interface Foo/struct FooImpl 不应该出现。
Go 也并没有抽象类,虽然可以将一个结构体嵌入到另一个结构体中。所以 Base/Abstract 也极少出现。
原则上,Go 的类关系更为简化,命名更强调意义优先,因此在命名时避免使用修饰性前后缀。
异常
Exception/Error
Java
所有的异常扩展应该以 Exception 为后缀。所有的错误应该以 Error 为后缀。 对异常和错误的区别请参见 https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html
public class IllegalArgumentException;public class OutOfMemoryError;
C++
C++ 的 exception 通常指语法特性,与 throw 对应,而 error 可以用来表示具体的异常错误。
// stdlibstd::exception;std::runtime_error
Go
所有的错误都是 error。因此,所有自定义的对 error 的扩展都以 Error 作为后缀。
os.PathError
测试
Test
Java/Go/C++ 均使用 Test 作为测试类的后缀。
模块
Module/Component
Module/Component 通常会在框架中使用。不同的语言/框架对于 Module/Component 有不同的定义。 在非框架代码中应该减少使用 Module/Componenet 等命名,因为可能与已有框架冲突,并且 Module/Componenet 过于宽泛而缺少实质意义。
Module/Component 是意义相近的词,都可以表示“模块”或者“组件”。两者虽然有细微的分别,但是框架通常都显式(即在文档中指定,或者通过框架语义约束)地把它们定义为框架语境下的某些结构层级。
总结,Module/Component 命名应该注意:
只应该在框架代码中使用。
Module/Component 应该在框架的语境中给出确切的定义。
服务
Service
Service 通常用于作为 C-S 架构下的服务提供者的名称的后缀,如:
HelloService
但除此之外,Service 可以表示任何长期存活的、提供功能的组件。例如:
BackgroundService // android 后台服务ExecutorService // 线程池执行服务,也是服务
BAD: 不要使用 Svr 缩写。使用全称。
容器
Holder/Container/Wrapper
Holder/Container/Wrapper 都表示“容器”,具有同一个意图:为一个类增加额外的数据/功能,例如:
添加某些语境下的元数据(Decorator 模式)
做适配以在另一个环境中使用(Adapter 模式)
通常的结构如下:
class ObjectHolder private final Object object; // other stuff ... public Object object() // Other methods
这三个词没有区别。在同一个项目中,应该保持一致。
控制类
Manager/Controller
Manager 和 Controller 是同义词。它们通常用来表示专门控制某些类的类。
这两个词有以下几个常见场景:
Manager 管理资源,如 DownloadManager, PackageManager。
Manager 通常特指管理对象的生命周期,从创建到销毁。
Controller 通常在某些架构中,尤其是 MVC (Model-View-Controller)。
即使如此,Manager/Controller 是无意义词汇,出现时充满了可疑的味道 -- 类应该管理它们自己。 Controller/Manager 多了一层抽象,而这很可能是多余的。 认真考虑是否需要 Manager/Controller。
辅助类
Util/Utility/Utils/Helper/ClassNames
辅助类是强制 OOP 的语言(i.e. Java) 所需要的特殊类。通常它们是一些辅助方法的合集。
Java
将与某个类型相关的辅助方法放在一个类中,并且以复数形式命名辅助类。如:
// Java std libpublic final class Strings public final class Lists public final class Collections
避免使用 Util/Utility/Utils/Helper。它们是无意义词汇。
C++
使用全局方法。如果担心命名污染,将之置入更细粒度的 namespace。
Go
使用全局方法。
函数式
Function/Predicate/Callback
Function 通常表示任意函数。 Predicate 表示命题,即通常返回类型为 bool。 Callback 指回调函数,指将函数作为参数传递到其它代码的某段代码的引用。换言之, Function 可以作为 Callback 使用。因此,Callback 在现代函数式编程概念流行后,通常很少使用。
Java
熟悉 Java 8 开始提供的函数式语义。如果可以使用标准库的语义,不要自己创建名词。 注意 Function 指单入参、单出参的函数。如果是多入参的函数,直接定义 FunctionalInterface 并且按用途命名,例如 OnClickListener.listen(Context, ClickEvent)。
// java.util.functionPredicate<T> // f(T) -> boolFunction<T, R> // f(T) -> RConsumer<T> // f(T) -> voidSupplier<T> // f() -> T
C++
first-class 函数的标准类型为 std::function。
C++ 表示命名函数对象的惯用法是 fun。Stdlib 中会缩写 function 为 fun,如 pmem_fun_ref,因此与 stdlib 一致,在代码中不要使用 fn 或是 func 。
Go
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
行为式
Adapter: 适配器
在 GoF 中 Adapter 本来是将一个类封装以可以被作为另一个类型被调用,这样调用方不需要额外改变代码。这种用途通常被内化到容器上,见上文[容器类]部分。
在现代,Adapter 更多地被作为 数据类 -> 数据类的转化,如常见的 pb -> pb:
class ProtoAdapter<S, T extends Message>
Decorator:装饰器
在 GoF 中 Decorator 本来是将一个类作为抽象类,通过组合+继承实现添加功能。实际上现代的编程实践中往往通过直接提供一个容器的封装提供装饰功能,见上文 [容器类]部分。 所以 GoF 式 Decorator 并不常见,除非像 Python 在语法层面提供了装饰器。在 Java 中类似的功能是注解。
Delegation:委派模式
GoF 中是非常基本的模式:由一个类负责接受请求,并把请求转发到合适的实例类中执行。
class RealPrinter class Printer RealPrinter printer;
Delegate 非常常见,也提供了两个名字,请注意区分:
Delegate 是被委任的对象。
Delegator 是委任对象。 所以,通常情况下 Delegator 在命名中会更常见,类似于 Dispatcher。Delegate 更多作为一个类型或是接口被实现。具体的选择参见 [编排] 部分。
Facade: 外观模式
GoF 中 Facade Pattern 通常是指为子系统提供一个更高层的统一界面,屏蔽子系统的独有的细节。 在现实中,Facade 通常用来为非常复杂的类/系统定义一个较为简化的界面,如:
// proto, extremely complicated TripResponsemessage 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 命名通常可以省略。仅当你的意图是明确告知用户这是关于某个类的外观时使用。
Proxy:代理模式
GoF 中代理模式用来添加一层抽象,以对实际类进行控制,并添加某些行为(如 lazy/memoized),或是隐藏某些信息(例如可见性或是执行远程调用)。
Proxy 与 Facade 的区别在于 Proxy 通常是为了额外的控制/记录等行为,而非只是为了更高的抽象/简化。
注意 Proxy 作为代码模式时,通常不应该出现在命名之中。使用具体的 Proxy 的目的作为命名,如 LazyCar 或是 TracedRuntime,而非 CarProxy 或是 RuntimeProxy。
Proxy 还有另一个含义就是真正的“代理”,如代理服务器。在这种情况下,使用 Proxy 是合适且应该的。这也是另一个为什么代理模式不应该用 Proxy 命名的原因。
Iterator: 迭代器
时至今日仍然最常见的模式之一。Interator 有以下两个术语,不要混淆:
Iterable: 迭代容器
Iterator: 迭代器
Visitor: 访问者模式
访问者模式用来遍历一个结构内的多个对象。对象提供 accept(Visitor) 方法,调用 Visitor.visit 方法。
即使如此,Visitor 应该并不常见,因为它可以简单地被函数式的写法替换:
class Car void accept(Consumer<Car> visitor); // No longer need to define Visitor class.
Observer/Observable: 观察者模式
Observer/Publisher/Subscriber/Producer/Consumer
时至今日最常见的模式之一。和事件驱动编程(Event-based)有紧密关系 -- Oberservable 发布消息,所有注册的 Obeserver 会接收消息。 Publisher/Subscriber 也是类似的,它们的区别在于 Observer 模式往往是强绑定的 -- 注册和分发通常在 Observable 类中实现; 而 PubSub 模式通常有专门的 Message Broker,即 Publisher 与 Subscriber 是完全解耦的。
PubSub 与 Producer/Consumer 的区别是:
Publisher/Subscriber: 在事件流系统中,表示 1:N 广播/订阅。
Producer/Consumer: 在整个流系统中,专指 1:1 生产/消费。Producer/Consumer 也是 Pub/Sub 系统的组件(广播也是一对一广播的)。
有些系统(Kafka)使用 Consumer Group 表示 Subscriber。
所有的消息注册的模式由三部分组成:
Notification: 消息本身
Sender:消息发送者/注册
Receiver: 消息接收者
关于命名参见 [事件] 部分。
Strategy:策略模式
Strategy/Policy
策略模式在 GoF 中用以指定某个行为在不同场景下的不同实现,作为“策略”。
Strategy 模式往往不是很显式。现代通常使用 Strategy 表示实际的“策略”,即对信息不同的处理策略,而不采取 Strategy 模式的含义。
在“策略”这个语义中,Strategy/Policy 没有区别。在同一个项目中,应该保持一致。
Command:命令模式
命令模式在 GoF 中以类开代表实际行动,将行动封装,以支持重复、取消等操作。
Command 在现代编程实践中可以通过简单的函数式方案替换,如:
Function<T, T> command; // Javastd::function<const T&(const T&)> command; // C++type Command func(T*) T* // Go
现代通常使用 Command 表示实际的“命令”,而不采取 Command 模式的含义。
Null Object 模式
Tombstone
Null Object 模式不在 GoF 当中。它是一个用来代替 null 的 object,对其所有的操作都会被吞掉。 Null Object 主要是为了避免空指针。 合理的零值,例如 go time.Time = 0,也可以理解为一种 Null Object。
通常会有一个专门的对象表示 Null Object。可以借用 Tombstone 表示 Null Object。
Object Pool 对象池模式
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 中使用。
另一个常见的场景是同一个方法有两种实现:同步阻塞和异步不阻塞的。在这种情况下,通常会命名异常不阻塞的方法为 synchronizedMethodAsync,例如:
public T exec();public Future<T> execAsync();
如果一个异步的方法并没有对应的同步方法,通常不需要加 Async 后缀。
在 Go 中,如果一个方法是意图在其它协程中异步执行,不需要加 Async 后缀。
缓存/惰性
Cached/Buffered Lazy Memoized
名词辨析:
Cached 表示获取的对象会被缓存,保留一段时间,在缓存期间不会重新获取。
Buffered 与 Cached 同义。
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 // Badclass CarEntity // Badclass CarInstance // Badclass Car // Goodclass MapKey class MapValue // OK. Couldn't be shortened.class LoggingMetricsData // Badclass LoggingMetricsValue // Badclass LoggingMetricsRecord // Badclass Logging Metrics // Goodclass 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
名词辨析:
Schema 借用数据库的概念,指数据的结构模式。
Index 借用数据库的概念,专指数据的索引。
Format/Pattern 通常是泛指的“模式/格式”概念。实际出现时,Format/Pattern 往往和字符串相关,如 Java 使用 Pattern 表示正则表达式。在非公共代码中,Format/Pattern 通常过于宽泛,应该考虑选用更细化的名词。