閱讀開源框架總結Java類的定義
Java的類是自定義的引用型別,是對職責相關 的行為與資料的一種封裝,用以表現一種業務領域或者技術領域的概念。在不同的場景,類包含的成員可能有所不同,大體可以分為如下五類:
- 資料類:可以視為是持有資料的容器,類的成員只包含了欄位,以及與欄位有關的get/set方法
- 實體類:既包含了體現狀態的欄位,又包含了操作這些狀態的方法
- 服務類:只有方法(行為)沒有欄位(狀態),可以理解為提供內聚職責的服務
- 函式類:如果定義的公開方法只有唯一一個,可以理解為它封裝的其實是一個函式,通常用匿名類或者Lambda表示
- 工具類:只包含一系列靜態方法,通常不支援對該型別的例項化
資料類
在Presto框架中定義的ClientSession
可以認為是這樣一種資料類。除了建構函式外,它只定義了欄位與對應的get()
方法(實際上,在框架的原始碼中,在ClientSession
類中還定義了一系列靜態工廠方法,但本質上說,ClientSession
還是一個數據類),用以持有客戶端Session所必須的資料:
public class ClientSession { private final URI server; private final String user; 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; // verify the properties are valid 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++; // Let gc do its work for (int i = 0; i < elementCount; i++) elementData[i] = null; elementCount = 0; } }
如下類的定義則體現了一種業務概念,方法changePriceTo()
實際上表現的是一種業務規則,而它要操作的資料就是Product
類自身持有的欄位sellingPrice
:
public class Product extends Entity<Identity> { private final List<Option> 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<Option>(); } 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(0m, 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(ActiveManifestactiveManifest) { validateSamePackageType(activeManifest); validateNoTempPackage(activeManifest); validateNoPackageRunning(activeManifest); validateAllPackagesBeenDownloaded(activeManifest); validateNoFatherPackageBakStatus(activeManifest); validatePackageNum(activeManifest); } private void validateSamePackageType(ActiveManifestactiveManifest) { 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<Integer> sortChannels; private final List<SortOrder> sortOrders; private final List<Type> sortTypes; public SimplePagesIndexComparator(List<Type> sortTypes, List<Integer> sortChannels, List<SortOrder> 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<Type> sortTypes
,List<Integer> sortChannels
和List<SortOrder> sortOrders
。雖然從介面的角度看,其實代表的是compare的語義,但由於邏輯複雜,而且需要傳入三個物件幫助對PagesIndex
進行比較,因而不可能實現為匿名類或者Lambda表示式。在Presto中,對它的使用為:
public class PagesIndexOrdering { private final PagesIndexComparator comparator; public PagesIndexOrdering(PagesIndexComparator comparator){ this.comparator = requireNonNull(comparator, "comparator is null"); } public PagesIndexComparator getComparator(){ return comparator; } /** * Returns the index of the median of the three positions. */ private int median3(PagesIndex pagesIndex, int a, int b, int c){ int ab = comparator.compareTo(pagesIndex, a, b); int ac = comparator.compareTo(pagesIndex, a, c); int bc = comparator.compareTo(pagesIndex, b, c); return (ab < 0 ? (bc < 0 ? b : ac < 0 ? c : a) : (bc > 0 ? b : ac > 0 ? c : a)); } }
匿名類形式
同樣在該框架下定義的IntComparator
介面,它的實現就完全不同了。首先是該介面的定義:
public interface IntComparator { /** Compares the given primitive types. * * @see java.util.Comparator * @return A positive integer, zero, or a negative integer if the first * argument is greater than, equal to, or smaller than, respectively, the * second one. */ int compare(int k1, int k2); }
在針對整型資料提供排序功能時,用到了IntComparator
介面:
public final class IntBigArray { public void sort(int from, int to, IntComparator comparator){ IntBigArrays.quickSort(array, from, to, comparator); } }
但由於提供整型資料的比較邏輯相對簡單,在Presto中並沒有定義顯式的函式類,而是使用了Lambda表示式:
groupIds.sort(0, groupByHash.getGroupCount(), (leftGroupId, rightGroupId) -> Long.compare(groupByHash.getRawHash(leftGroupId), groupByHash.getRawHash(rightGroupId)));
這裡的Lambda表示式其實也可以理解為是一個函式類。
函式重用形式
還有一種特殊的函式類,它的定義形式與後面介紹的工具類非常相似,同樣是定義了一組靜態方法,但它的目的不是提供工具或輔助功能,而是將其視為函式成為被重用的單元。這時,需要用到Java 8提供的方法引用(method reference)語法。例如我們要對List<Apple>
集合進行過濾,過濾條件分別為顏色與重量,這時可以在Apple
類中定義兩個靜態方法:
public class Apple { public static boolean isGreenApple(Apple apple) { return "green".equals(apple.getColor()); } public static boolean isHeavyApple(Apple apple) { return apple.getWeight() > 150; } }
這兩個方法實際上滿足函式介面Predicate<Apple>
的定義,因此可以在filter
方法中傳入這兩個方法的引用:
public List<Apple> filter(Predicate<Apple> predicate) { ArrayList<Apple> result = new ArrayList<>(); for (Apple apple : apples) { if (predicate.test(apple)) { result.add(apple); } } return result; } public List<Apple> filterGreenApples() { return filter(Apple::isGreenApple); } public List<Apple> filterHeavyApples() { return filter(Apple::isHeavyApple); }
此時Apple
類可以認為是一個函式類,但準確地說法是一系列可以被重用的函式的容器。與工具類不同的是,這些函式並不是被直接呼叫,本質上講,其實是作為“高階函式”被傳遞給其他方法而被重用。雖然說例項方法也可以採用這種方式而被重用,但靜態方法的呼叫會更加簡單。
工具類
在許多專案或開源專案中,隨處可見工具類的身影。無需例項化的特性使得我們使用工具類的方式時變得非常的便利,也不需要考慮狀態的維護。然而越是方便,我們越是要警惕工具類的陷阱——設計出臃腫龐大無所不能的上帝工具類。工具類仍然要遵循高內聚的原則,只有強相關的職責才能放到同一個工具類中。
在定義工具類時,通常有三類命名正規化:
-
名詞複數形式:工具類其實就是一系列工具方法的容器,當我們要針對某種型別(或物件)提供工具方法時,可以直接將工具類命名為該型別的複數形式,例如操作
Collection
的工具類可以命名為Collections
,操作Object
的工具類可以命名為Objects
,而與前置條件有關的工具類則被命名為Preconditions
。 -
以Util為字尾:這體現了工具(Utility)的語義,當我們在類名中看到
Util
字尾時,就可以直觀地瞭解到這是一個工具類。例如ArrayUtil
類是針對陣列的工具類,DatabaseUtil
是針對資料庫操作的工具類,UuidUtil
是針對Uuid的工具類。 -
以Helper為字尾:這種命名相對較少,但許多框架也採用這種命名方式來體現“輔助類”的含義。例如在Druid框架中,就定義了
JobHelper
、GroupByQueryHelper
等輔助類。
工具類是無需例項化的,因此在定義工具類時,儘可能將其宣告為final類,併為其定義私有的建構函式。例如Guava框架提供的Preconditions
工具類:
public final class Preconditions { private Preconditions() { } public static void checkArgument(boolean expression) { if(!expression) { throw new IllegalArgumentException(); } } //other util methods }