前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阅读开源框架,总结Java类的定义

阅读开源框架,总结Java类的定义

作者头像
张逸
发布2018-11-08 15:49:17
6710
发布2018-11-08 15:49:17
举报
文章被收录于专栏:斑斓斑斓

即使我们明白Java的类,也未必清楚该如何正确地定义一个Java类。阅读一些开源框架的源代码,会启发我们灵感,并给出好代码的规范,提炼设计原则与模式。

标签 | DDD

作者 | 张逸

阅读 | 33分钟

Java的类是自定义的引用类型,是对职责相关的行为与数据的一种封装,用以表现一种业务领域或者技术领域的概念。在不同的场景,类包含的成员可能有所不同,大体可以分为如下五类:

  • 数据类:可以视为是持有数据的容器,类的成员只包含了字段,以及与字段有关的get/set方法
  • 实体类:既包含了体现状态的字段,又包含了操作这些状态的方法
  • 服务类:只有方法(行为)没有字段(状态),可以理解为提供内聚职责的服务
  • 函数类:如果定义的公开方法只有唯一一个,可以理解为它封装的其实是一个函数,通常用匿名类或者Lambda表示
  • 工具类:只包含一系列静态方法,通常不支持对该类型的实例化

数据类

在Presto框架中定义的ClientSession可以认为是这样一种数据类。除了构造函数外,它只定义了字段与对应的get()方法(实际上,在框架的源代码中,在ClientSession类中还定义了一系列静态工厂方法,但本质上说,ClientSession还是一个数据类),用以持有客户端Session所必须的数据:

代码语言:javascript
复制
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展现了一种数据结构,因而它持有的字段与方法应该仅仅与队列操作与状态有关:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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); 
    }
}

服务类

只有方法没有状态的类定义是对行为的封装,行为的实现要么是通过操作内部封装的不可变私有数据,要么是通过操作传入的参数对象实现对状态的修改。由于参数传入的状态与服务类自身没有任何关系,因此这样的类通常也被视为无状态的类。以下代码是针对升级激活包的验证服务:

代码语言:javascript
复制
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类,就提供加载配置文件内容的服务:

代码语言:javascript
复制
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接口,提供了比较方法以用于支持对页面索引的排序。接口的定义为:

代码语言:javascript
复制
public interface PagesIndexComparator {
    int compareTo(PagesIndex pagesIndex, int leftPosition, int rightPosition);
}

Presto定义了该接口的实现类SimplePagesIndexComparator,该类就是一个函数类:

代码语言:javascript
复制
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中,对它的使用为:

代码语言:javascript
复制
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接口,它的实现就完全不同了。首先是该接口的定义:

代码语言:javascript
复制
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接口:

代码语言:javascript
复制
public final class IntBigArray {
    public void sort(int from, int to, IntComparator comparator)    {
        IntBigArrays.quickSort(array, from, to, comparator);
    }
}

但由于提供整型数据的比较逻辑相对简单,在Presto中并没有定义显式的函数类,而是使用了Lambda表达式:

代码语言:javascript
复制
groupIds.sort(0, groupByHash.getGroupCount(), (leftGroupId, rightGroupId) ->
                Long.compare(groupByHash.getRawHash(leftGroupId), groupByHash.getRawHash(rightGroupId)));

这里的Lambda表达式其实也可以理解为是一个函数类。

函数重用形式

还有一种特殊的函数类,它的定义形式与后面介绍的工具类非常相似,同样是定义了一组静态方法,但它的目的不是提供工具或辅助功能,而是将其视为函数成为被重用的单元。这时,需要用到Java 8提供的方法引用(method reference)语法。例如我们要对List<Apple>集合进行过滤,过滤条件分别为颜色与重量,这时可以在Apple类中定义两个静态方法:

代码语言:javascript
复制
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方法中传入这两个方法的引用:

代码语言:javascript
复制
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工具类:

代码语言:javascript
复制
public final class Preconditions {
    private Preconditions() {
    }

    public static void checkArgument(boolean expression) {
        if(!expression) {
            throw new IllegalArgumentException();
        }
    }
    
    //other util methods
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 逸言 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档