Java和Ceylon对象的构造和验证

作者:shanshuiss 时间:2022-04-05 04:28:37 

当变换Java代码为Ceylon代码时,有时候我会遇到一些Java类构造器混淆了验证与初始化的情形。让我们使用一个简单但是人为的代码例子来说明我想阐述的意思。

一些坏代码

考虑下面的Java类。(伙计,不要在家里写这样的代码)


public class Period {
private final Date startDate;
private final Date endDate;
//returns null if the given String
//does not represent a valid Date
private Date parseDate(String date) {
 ...
}
public Period(String start, String end) {
 startDate = parseDate(start);
 endDate = parseDate(end);
}
public boolean isValid() {
 return startDate!=null && endDate!=null;
}
public Date getStartDate() {
 if (startDate==null)
  throw new IllegalStateException();
 return startDate;
}
public Date getEndDate() {
 if (endDate==null)
  throw new IllegalStateException();
 return endDate;
}
}

嘿,我之前已经警告过,它是人为的。但是,在实际Java代码中找个像这样的东西实际上并非不常见。

这里的问题在于,即使输入参数(在隐藏的parseDate()方法中)的验证失败了,我们还是会获得一个Period的实例。但是我们获取的那个Period不是一个“有效的”状态。严格地说,我的意思是什么呢?

好吧,假如一个对象不能有意义地响应公用操作时,我会说它处于一个非有效状态。在这个例子里,getStartDate() 和getEndDate()会抛出一个IllegalStateException异常,这就是我认为不是“有意义的”一种情况。

从另外一方面来看这个例子,在设计Period时,我们这儿出现了类型安全的失败。未检查的异常代表了类型系统中的一个“漏洞”。因此,一个更好的Period的类型安全的设计,会是一个不使用未检查的异常—在这个例子中意味着不抛出IllegalStateException异常。

(实际上,在真实代码中,我更有可能遇到一个getStartDate() 方法它不检查null ,在这个代码行之后就会导致一个NullPointerException异常,这就更加糟糕了。)

我们能够很容易地转换上面的Period类成为Ceylon形式的类:


shared class Period(String start, String end) {
//returns null if the given String
//does not represent a valid Date
Date? parseDate(String date) => ... ;
value maybeStartDate = parseDate(start);
value maybeEndDate = parseDate(end);
shared Boolean valid
 => maybeStartDate exists
 && maybeEndDate exists;
shared Date startDate {
 assert (exists maybeStartDate);
 return maybeStartDate;
}
shared Date endDate {
 assert (exists maybeEndDate);
 return maybeEndDate;
}
}

当然了,这段代码也会遇到与原始Java代码同样的问题。两个assert符号冲着我们大喊,在代码的类型安全中有一个问题。

使Java代码变得更好

Java里我们怎么改进这段代码呢?好吧,这儿就是一个例子关于Java饱受诟病的已检查异常会是一个非常合理的解决方法!我们可以稍微修改下Period来从它的构造器中抛出一个已检查的异常:


public class Period {
private final Date startDate;
private final Date endDate;
//throws if the given String
//does not represent a valid Date
private Date parseDate(String date)
  throws DateFormatException {
 ...
}
public Period(String start, String end)
  throws DateFormatException {
 startDate = parseDate(start);
 endDate = parseDate(end);
}
public Date getStartDate() {
 return startDate;
}
public Date getEndDate() {
 return endDate;
}
}

现在,使用这个解决方案,我们就不会获取一个处于非有效状态的Period,实例化Period的代码会由编译器负责去处理无效输入的情形,它会捕获一个DateFormatException异常。


try {
Period p = new Period(start, end);
...
}
catch (DateFormatException dfe) {
...
}

这是一个对已检查异常不错的、完美的、正确的使用,不幸的是我几乎很少看到Java代码像上面这样使用已检查异常。

使Ceylon代码变得更好

那么Ceylon怎么样呢?Ceylon没有已检查异常,因而我们需要寻找一个不同的解决方式。典型地,在Java调用一个函数会抛出一个已检查异常的情形中,Ceylon会调用函数返回一个联合类型。因为,一个类的初始化不返回除了类自己外的任何类型,我们需要提取一些混合的初始化/验证的逻辑来使其成为一个工厂函数。


//returns DateFormatError if the given
//String does not represent a valid Date
Date|DateFormatError parseDate(String date) => ... ;
shared Period|DateFormatError parsePeriod
 (String start, String end) {
value startDate = parseDate(start);
if (is DateFormatError startDate) {
 return startDate;
}
value endDate = parseDate(end);
if (is DateFormatError endDate) {
 return endDate;
}
return Period(startDate, endDate);
}
shared class Period(startDate, endDate) {
shared Date startDate;
shared Date endDate;
}

根据类型系统,调用者有义务去处理DateFormatError:


value p = parsePeriod(start, end);
if (is DateFormatError p) {
...
}
else {
...
}

或者,如果我们不关心给定日期格式的实际问题(这是有可能的,假定我们工作的初始化代码丢失了那个信息),我们可以使用Null而不是DateFormatError:


//returns null if the given String
//does not represent a valid Date
Date? parseDate(String date) => ... ;
shared Period? parsePeriod(String start, String end)
=> if (exists startDate = parseDate(start),
  exists endDate = parseDate(end))
 then Period(startDate, endDate)
 else null;
shared class Period(startDate, endDate) {
shared Date startDate;
shared Date endDate;
}

至少可以说,使用工厂函数的方法是优秀的,因为通常来说在验证逻辑和对象初始化之间它具有更好的隔离。这点在Ceylon * 别有用,在Ceylon中,编译器在对象初始化逻辑中添加了一些非常严厉的限制,以保证对象的所有领域仅被赋值一次。

标签:Java,Ceylon
0
投稿

猜你喜欢

  • java实现sftp客户端上传文件以及文件夹的功能代码

    2023-02-14 22:07:28
  • 使用Jitpack发布开源Java库的详细流程

    2021-12-18 06:55:58
  • JavaWeb中获取表单数据及乱码问题的解决方法

    2021-11-21 11:03:53
  • 关于SpringCloudStream配置问题

    2023-08-08 20:12:00
  • Java汉字转拼音工具类完整代码实例

    2021-07-09 21:32:18
  • 浅谈MyBatis通用Mapper实现原理

    2022-11-18 18:45:16
  • CentOS 7系统下配置自定义JDK的教程

    2022-02-27 13:46:26
  • 如何使用Spring AOP的通知类型及创建通知

    2022-03-19 19:32:51
  • Mybatis插件之自动生成不使用默认的驼峰式操作

    2023-11-19 01:20:03
  • 【Redis缓存机制】详解Java连接Redis_Jedis_事务

    2023-05-23 19:59:55
  • Mybatis-Plus之ID自动增长的设置实现

    2022-10-27 00:09:47
  • Java中Steam流的用法详解

    2021-12-16 14:18:50
  • SpringMVC的执行过程浅析

    2021-05-31 20:51:11
  • Java关于MyBatis缓存详解

    2021-11-01 00:40:20
  • 利用java操作Excel文件的方法

    2021-12-13 03:03:49
  • java商城项目实战之购物车功能实现

    2023-11-02 21:17:43
  • @Configuration与@Component作为配置类的区别详解

    2023-03-09 19:50:15
  • SpringBoot整合Shiro实现登录认证的方法

    2022-03-23 01:12:19
  • Mybatis结果集自动映射的实例代码

    2023-07-09 02:13:58
  • 浅析JAVA中的内存结构、重载、this与继承

    2023-09-24 19:15:07
  • asp之家 软件编程 m.aspxhome.com