閱讀開源框架,總結Java類的定義

即使我們明白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;

        // 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(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<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
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章