Java的类是自定义的引用类型,是对职责相关的行为与数据的一种封装,用以表现一种业务领域或者技术领域的概念。在不同的场景,类包含的成员可能有所不同,大体可以分为如下五类:
- 数据类:可以视为是持有数据的容器,类的成员只包含了字段,以及与字段有关的get/set方法
- 实体类:既包含了体现状态的字段,又包含了操作这些状态的方法
- 服务类:只有方法(行为)没有字段(状态),可以理解为提供内聚职责的服务
- 函数类:如果定义的公开方法只有唯一一个,可以理解为它封装的其实是一个函数,通常用匿名类或者Lambda表示
- 工具类:只包含一系列静态方法,通常不支持对该类型的实例化
数据类
在Presto框架中定义的ClientSession
可以认为是这样一种数据类。除了构造函数外,它只定义了字段与对应的get()
方法(实际上,在框架的源代码中,在ClientSession
类中还定义了一系列静态工厂方法,但本质上说,ClientSession
还是一个数据类),用以持有客户端Session所必须的数据:
public class ClientSession |
这样包含数据或状态的对象通常会作为参数在方法调用之间传递,体现了诸如配置、视图模型、服务传输数据、协议数据等概念。除此之外,我们应尽量避免定义这样的对象去体现某种业务概念,因为基于“信息专家”模式,好的面向对象设计应该是将数据与操作这些数据的行为封装在一起。
实体类
这是最为常见的一种类定义,也是符合面向对象设计原则的,前提是定义的类必须是高内聚的,原则上应该满足单一职责原则。例如JDK定义的Vector
展现了一种数据结构,因而它持有的字段与方法应该仅仅与队列操作与状态有关:
public class Vector<E> |
如下类的定义则体现了一种业务概念,方法changePriceTo()
实际上表现的是一种业务规则,而它要操作的数据就是Product
类自身持有的字段sellingPrice
:
public class Product extends Entity<Identity> { |
服务类
只有方法没有状态的类定义是对行为的封装,行为的实现要么是通过操作内部封装的不可变私有数据,要么是通过操作传入的参数对象实现对状态的修改。由于参数传入的状态与服务类自身没有任何关系,因此这样的类通常也被视为无状态的类。以下代码是针对升级激活包的验证服务:
public class PreActivePackageValidator { |
服务类还可以操作外部资源,例如读取文件、访问数据库、与第三方服务通信等。例如airlift框架定义的ConfigurationLoader
类,就提供加载配置文件内容的服务:
public class ConfigurationLoader { |
函数类
可以将函数类理解为设计一个类,它仅仅实现了一个接口,且该接口只定义一个方法。使用时,我们会基于依赖倒置原则(DIP)从接口的角度使用这个类。为了重用的目的,这个类可以单独被定义,也可能体现为匿名类,或者Java 8中的Lambda表达式。
单独类形式
例如,在Presto中定义了PagesIndexComparator
接口,提供了比较方法以用于支持对页面索引的排序。接口的定义为:
public interface PagesIndexComparator { |
Presto定义了该接口的实现类SimplePagesIndexComparator
,该类就是一个函数类:
public class SimplePagesIndexComparator |
我们看到SimplePagesIndexComparator
类的逻辑相对比较复杂,构造函数也需要传入三个参数:List<Type> sortTypes
,List<Integer> sortChannels
和List<SortOrder> sortOrders
。虽然从接口的角度看,其实代表的是compare的语义,但由于逻辑复杂,而且需要传入三个对象帮助对PagesIndex
进行比较,因而不可能实现为匿名类或者Lambda表达式。在Presto中,对它的使用为:
public class PagesIndexOrdering { |
匿名类形式
同样在该框架下定义的IntComparator
接口,它的实现就完全不同了。首先是该接口的定义:
public interface IntComparator { |
在针对整型数据提供排序功能时,用到了IntComparator
接口:
public final class IntBigArray { |
但由于提供整型数据的比较逻辑相对简单,在Presto中并没有定义显式的函数类,而是使用了Lambda表达式:
groupIds.sort(0, groupByHash.getGroupCount(), (leftGroupId, rightGroupId) -> |
这里的Lambda表达式其实也可以理解为是一个函数类。
函数重用形式
还有一种特殊的函数类,它的定义形式与后面介绍的工具类非常相似,同样是定义了一组静态方法,但它的目的不是提供工具或辅助功能,而是将其视为函数成为被重用的单元。这时,需要用到Java 8提供的方法引用(method reference)语法。例如我们要对List<Apple>
集合进行过滤,过滤条件分别为颜色与重量,这时可以在Apple
类中定义两个静态方法:
public class Apple { |
这两个方法实际上满足函数接口Predicate<Apple>
的定义,因此可以在filter
方法中传入这两个方法的引用:
public List<Apple> filter(Predicate<Apple> predicate) { |
此时Apple
类可以认为是一个函数类,但准确地说法是一系列可以被重用的函数的容器。与工具类不同的是,这些函数并不是被直接调用,本质上讲,其实是作为“高阶函数”被传递给其他方法而被重用。虽然说实例方法也可以采用这种方式而被重用,但静态方法的调用会更加简单。
工具类
在许多项目或开源项目中,随处可见工具类的身影。无需实例化的特性使得我们使用工具类的方式时变得非常的便利,也不需要考虑状态的维护。然而越是方便,我们越是要警惕工具类的陷阱——设计出臃肿庞大无所不能的上帝工具类。工具类仍然要遵循高内聚的原则,只有强相关的职责才能放到同一个工具类中。
在定义工具类时,通常有三类命名范式:
- 名词复数形式:工具类其实就是一系列工具方法的容器,当我们要针对某种类型(或对象)提供工具方法时,可以直接将工具类命名为该类型的复数形式,例如操作
Collection
的工具类可以命名为Collections
,操作Object
的工具类可以命名为Objects
,而与前置条件有关的工具类则被命名为Preconditions
。 - 以Util为后缀:这体现了工具(Utility)的语义,当我们在类名中看到
Util
后缀时,就可以直观地了解到这是一个工具类。例如ArrayUtil
类是针对数组的工具类,DatabaseUtil
是针对数据库操作的工具类,UuidUtil
是针对Uuid的工具类。 - 以Helper为后缀:这种命名相对较少,但许多框架也采用这种命名方式来体现“辅助类”的含义。例如在Druid框架中,就定义了
JobHelper
、GroupByQueryHelper
等辅助类。
工具类是无需实例化的,因此在定义工具类时,尽可能将其声明为final类,并为其定义私有的构造函数。例如Guava框架提供的Preconditions
工具类:
public final class Preconditions { |