即使我们明白Java的类,也未必清楚该如何正确地定义一个Java类。阅读一些开源框架的源代码,会启发我们灵感,并给出好代码的规范,提炼设计原则与模式。
标签
| DDD
作者
| 张逸
阅读
| 33分钟
Java的类是自定义的引用类型,是对职责相关的行为与数据的一种封装,用以表现一种业务领域或者技术领域的概念。在不同的场景,类包含的成员可能有所不同,大体可以分为如下五类:
数据类:可以视为是持有数据的容器,类的成员只包含了字段,以及与字段有关的get/set方法
实体类:既包含了体现状态的字段,又包含了操作这些状态的方法
服务类:只有方法(行为)没有字段(状态),可以理解为提供内聚职责的服务
函数类:如果定义的公开方法只有唯一一个,可以理解为它封装的其实是一个函数,通常用匿名类或者Lambda表示
工具类:只包含一系列静态方法,通常不支持对该类型的实例化
数据类
在Presto框架中定义的ClientSession可以认为是这样一种数据类。除了构造函数外,它只定义了字段与对应的get()方法(实际上,在框架的源代码中,在ClientSession类中还定义了一系列静态工厂方法,但本质上说,ClientSession还是一个数据类),用以持有客户端Session所必须的数据:
public class ClientSession { private final URI server; private final String use; private final String source; private final String clientInfo; private final String catalog; private final String schema; private final TimeZoneKey timeZone; private final Locale locale; private final Map<String , String > properties; private final Map<String , String > preparedStatements; private final String transactionId; private final boolean debug; private final Duration clientRequestTimeout; public ClientSession( URI server, String user, String source, String clientInfo, String catalog, String schema, String timeZoneId, Locale locale, Map<String , String > properties, String transactionId, boolean debug, Duration clientRequestTimeout) { this (server, user, source, clientInfo, catalog, schema, timeZoneId, locale, properties, emptyMap(), transactionId, debug, clientRequestTimeout); } public ClientSession( URI server, String user, String source, String clientInfo, String catalog, String schema, String timeZoneId, Locale locale, Map<String , String > properties, Map<String , String > preparedStatements, String transactionId, boolean debug, Duration clientRequestTimeout) { this .server = requireNonNull(server, "server is null" ); this .user = user; this .source = source; this .clientInfo = clientInfo; this .catalog = catalog; this .schema = schema; this .locale = locale; this
.timeZone = TimeZoneKey.getTimeZoneKey(timeZoneId); this .transactionId = transactionId; this .debug = debug; this .properties = ImmutableMap.copyOf(requireNonNull(properties, "properties is null" )); this .preparedStatements = ImmutableMap.copyOf(requireNonNull(preparedStatements, "preparedStatements is null" )); this .clientRequestTimeout = clientRequestTimeout; CharsetEncoder charsetEncoder = US_ASCII.newEncoder(); for (Entry<String , String > entry : properties.entrySet()) { checkArgument(!entry.getKey().isEmpty(), "Session property name is empty" ); checkArgument(entry.getKey().indexOf('=' ) < 0 , "Session property name must not contain '=': %s" , entry.getKey()); checkArgument(charsetEncoder.canEncode(entry.getKey()), "Session property name is not US_ASCII: %s" , entry.getKey()); checkArgument(charsetEncoder.canEncode(entry.getValue()), "Session property value is not US_ASCII: %s" , entry.getValue()); } } public URI getServer() { return server; } public String getUser() { return user; } public String getSource() { return source; } public String getClientInfo() { return clientInfo; } public String getCatalog() { return catalog; } public String getSchema() { return schema; } public TimeZoneKey getTimeZone() { return timeZone; } public Locale getLocale() { return locale; } public Map<String , String > getProperties() { return properties; } public Map<String , String > getPreparedStatements() { return preparedStatements; } public String getTransactionId() { return transactionId; } public boolean isDebug() { return debug; } public Duration getClientRequestTimeout() { return clientRequestTimeout; } @Override public String toString() { return toStringHelper(this ) .add("server" , server) .add("user" , user) .add("clientInfo" , clientInfo) .add("catalog" , catalog) .add("schema" , schema) .add("timeZone" , timeZone) .add("locale" , locale) .add("properties" , properties) .add("transactionId" , transactionId) .add("debug" , debug) .toString(); } }
这样包含数据或状态的对象通常会作为参数在方法调用之间传递,体现了诸如配置、视图模型、服务传输数据、协议数据等概念。除此之外,我们应尽量避免定义这样的对象去体现某种业务概念,因为基于“信息专家”模式,好的面向对象设计应该是将数据与操作这些数据的行为封装在一起。
实体类
这是最为常见的一种类定义,也是符合面向对象设计原则的,前提是定义的类必须是高内聚的,原则上应该满足单一职责原则。例如JDK定义的Vector展现了一种数据结构,因而它持有的字段与方法应该仅仅与队列操作与状态有关:
public
class Vector <E > extends AbstractList <E > implements List <E >, RandomAccess , Cloneable , java .io .Serializable { protected Object[] elementData; protected int elementCount; protected int capacityIncrement; public Vector (int initialCapacity, int capacityIncrement) { super (); if (initialCapacity < 0 ) throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); this .elementData = new Object[initialCapacity]; this .capacityIncrement = capacityIncrement; } public Vector (int initialCapacity) { this (initialCapacity, 0 ); } public synchronized void setSize (int newSize) { modCount++; if (newSize > elementCount) { ensureCapacityHelper(newSize); } else { for (int i = newSize ; i < elementCount ; i++) { elementData[i] = null ; } } elementCount = newSize; } public synchronized int size () { return elementCount; } public synchronized boolean isEmpty () { return elementCount == 0 ; } public boolean contains (Object o) { return indexOf(o, 0 ) >= 0 ; } public synchronized E firstElement ()
{ if (elementCount == 0 ) { throw new NoSuchElementException(); } return elementData(0 ); } public synchronized void insertElementAt (E obj, int index) { modCount++; if (index > elementCount) { throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount); } ensureCapacityHelper(elementCount + 1 ); System.arraycopy(elementData, index, elementData, index + 1 , elementCount - index); elementData[index] = obj; elementCount++; } public synchronized void addElement (E obj) { modCount++; ensureCapacityHelper(elementCount + 1 ); elementData[elementCount++] = obj; } public synchronized boolean removeElement (Object obj) { modCount++; int i = indexOf(obj); if (i >= 0 ) { removeElementAt(i); return true ; } return false ; } public synchronized void removeAllElements () { modCount++; for (int i = 0 ; i < elementCount; i++) elementData[i] = null ; elementCount = 0 ; } }
如下类的定义则体现了一种业务概念,方法changePriceTo()实际上表现的是一种业务规则,而它要操作的数据就是Product类自身持有的字段sellingPrice:
public class Product extends Entity <Identity > { private final List options; private Price sellingPrice; private Price retailPrice; public Product (Identity id, Price sellingPrice, Price retailPrice) { super (id); this .sellingPrice = sellingPrice; if (!sellingPriceMatches(retailPrice) { throw new PricesNotInTheSameCurrencyException("Selling and retail price must be in the same currency" ); } this .retailPrice = retailPrice; options = new List (); } public void
changePriceTo (Price newPrice) { if (!sellingPriceMatches(newPrice)) { throw new PricesNotInTheSameCurrencyException("You cannot change the price of this product to a different currency" ); } sellingPrice = newPrice; } public Price savings () { Price savings = retailPrice.minus(sellingPrice); if (savings.isGreaterThanZero()) return savings; else return new Price(0 m, sellingPrice.currency); } private bool sellingPriceMatches (Price retailPrice) { return sellingPrice.sameCurrency(retailPrice); } public void add (Option option) { if (!this .contains(option)) options.Add(option); else throw new ProductOptionAddedNotUniqueException(string.Format("This product already has the option {0}" , option.ToString())); } public bool contains (Option option) { return options.Contains(option); } }
服务类
只有方法没有状态的类定义是对行为的封装,行为的实现要么是通过操作内部封装的不可变私有数据,要么是通过操作传入的参数对象实现对状态的修改。由于参数传入的状态与服务类自身没有任何关系,因此这样的类通常也被视为无状态的类。以下代码是针对升级激活包的验证服务:
public class PreActivePackageValidator { public long validatePreActivePackage (ActiveManifest activeManifest) { validateSamePackageType(activeManifest); validateNoTempPackage(activeManifest); validateNoPackageRunning(activeManifest); validateAllPackagesBeenDownloaded(activeManifest); validateNoFatherPackageBakStatus(activeManifest); validatePackageNum(activeManifest); } private void validateSamePackageType (ActiveManifest activeManifest) { int packakeType = activeManifest.getPackageType(); for (UpagrdePackage pkg : activeManifest.getPackages()) { if (packageType != pkg.getPackageType()) { throw new PackagePreActiveException("pre active exist different type package" ); } } } }
服务类还可以操作外部资源,例如读取文件、访问数据库、与第三方服务通信等。例如airlift框架定义的ConfigurationLoader类,就提供加载配置文件内容的服务:
public class ConfigurationLoader { public Map <String , String > loadProperties() throws IOException { Map <String , String > result = new TreeMap<>(); String configFile = System.getProperty("config"
); if (configFile != null ) { result.putAll(loadPropertiesFrom(configFile)); } result.putAll(getSystemProperties()); return ImmutableSortedMap.copyOf(result); } public Map <String , String > loadPropertiesFrom(String path) throws IOException { Properties properties = new Properties(); try (Reader reader = new FileReader(new File(path))) { properties.load(reader); } return fromProperties(properties); } public Map <String , String > getSystemProperties() { return fromProperties(System.getProperties()); } }
函数类
可以将函数类理解为设计一个类,它仅仅实现了一个接口,且该接口只定义一个方法。使用时,我们会基于依赖倒置原则(DIP)从接口的角度使用这个类。为了重用的目的,这个类可以单独被定义,也可能体现为匿名类,或者Java 8中的Lambda表达式。
单独类形式
例如,在Presto中定义了PagesIndexComparator接口,提供了比较方法以用于支持对页面索引的排序。接口的定义为:
public interface PagesIndexComparator { int compareTo (PagesIndex pagesIndex, int leftPosition, int rightPosition ) ; }
Presto定义了该接口的实现类SimplePagesIndexComparator,该类就是一个函数类:
public class SimplePagesIndexComparator implements PagesIndexComparator { private final List sortChannels; private final List sortOrders; private final List sortTypes; public SimplePagesIndexComparator (List sortTypes, List sortChannels, List sortOrders) { this .sortTypes = ImmutableList.copyOf(requireNonNull(sortTypes, "sortTypes is null" )); this .sortChannels = ImmutableList.copyOf(requireNonNull(sortChannels, "sortChannels is null" )); this .sortOrders = ImmutableList.copyOf(requireNonNull(sortOrders, "sortOrders is null" )); } @Override public int compareTo (PagesIndex pagesIndex, int leftPosition, int rightPosition) { long leftPageAddress = pagesIndex.getValueAddresses().getLong(leftPosition); int leftBlockIndex = decodeSliceIndex(leftPageAddress); int leftBlockPosition = decodePosition(leftPageAddress); long rightPageAddress = pagesIndex.getValueAddresses().getLong(rightPosition); int rightBlockIndex = decodeSliceIndex(rightPageAddress); int rightBlockPosition = decodePosition(rightPageAddress); for (int i = 0 ; i < sortChannels.size(); i++) { int sortChannel = sortChannels.get(i); Block leftBlock = pagesIndex.getChannel(sortChannel).get(leftBlockIndex); Block rightBlock = pagesIndex.getChannel(sortChannel).get(rightBlockIndex); SortOrder sortOrder = sortOrders.get(i); int compare = sortOrder.compareBlockValue(sortTypes.get(i), leftBlock, leftBlockPosition, rightBlock, rightBlockPosition); if (compare != 0 ) { return compare; } } return 0 ; } }
我们看到SimplePagesIndexComparator类的逻辑相对比较复杂,构造函数也需要传入三个参数:List
sortTypes,List
sortChannels和List
sortOrders。虽然从接口的角度看,其实代表的是compare的语义,但由于逻辑复杂,而且需要传入三个对象帮助对PagesIndex进行比较,因而不可能实现为匿名类或者Lambda表达式。在Presto中,对它的使用为: