专栏名称: 逸言
文学与软件,诗意地想念。
目录
相关文章推荐
程序员的那些事  ·  趣图:我和 DeepSeek 互换角色 ·  昨天  
OSC开源社区  ·  2024年中国开源模型:崛起与变革 ·  昨天  
程序员小灰  ·  这款AI编程工具,将会取代Cursor! ·  2 天前  
51好读  ›  专栏  ›  逸言

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

逸言  · 公众号  · 程序员  · 2018-10-21 15:22

正文


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


服务类


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

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中,对它的使用为:







请到「今天看啥」查看全文