When transforming Java code to Ceylon code, sometimes I encounter some Java class constructors that confuse verification and initialization. Let's use a simple but artificial code example to illustrate what I want to explain.
Some bad code
Consider the following Java class. (Man, don't write such code at home)
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; }}Hey, I've warned it before, it's artificial. However, it is actually not uncommon to find something like this in actual Java code.
The problem here is that even if the verification of the input parameters (in the hidden parseDate() method) fails, we will still get an instance of Period. But the Period we get is not a "valid" state. Strictly speaking, what do I mean?
Well, if an object cannot respond meaningfully to a public operation, I would say it is in a non-valid state. In this example, getStartDate() and getEndDate() throw an IllegalStateException exception, which is a case that I don't think is "meaningful".
Looking at this example on the other hand, when designing Period, we have failed in type safety here. Unchecked exceptions represent a "voidance" in the type system. Therefore, a better Period type-safe design would be a non-use of unchecked exceptions - in this example, IllegalStateException is not thrown.
(In fact, in real code, I'm more likely to encounter a getStartDate() method that doesn't check for null , after this line of code it will cause a NullPointerException exception, which is even worse.)
We can easily convert the above Period class into a class of Ceylon form:
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 maybeStartDate); return maybeStartDate; } shared Date endDate { assert (exists maybeEndDate); return maybeEndDate; }}Of course, this code will also encounter the same problems as the original Java code. Two assert symbols shouted at us, there was a problem in the type safety of the code.
Make Java code better
How do we improve this code in Java? Well, here is an example of Java's criticized checked exceptions that are very reasonable to solve! We can slightly modify Period to throw a checked exception from its constructor:
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; }}Now, with this solution, we will not get a Period in a non-valid state. The code that instantiates the Period will be handled by the compiler to handle invalid inputs, which will catch a DateFormatException exception.
try { Period p = new Period(start, end); ...}catch (DateFormatException dfe) { ...}This is a nice, perfect, and correct use of checked exceptions, and unfortunately I rarely see Java code using checked exceptions like the one above.
Make Ceylon code better
So how about Ceylon? Ceylon has no checked exceptions, so we need to find a different solution. Typically, in a situation where Java calls a function and throws a checked exception, Ceylon calls the function and returns a union type. Because, the initialization of a class does not return any type except the class itself, we need to extract some mixed initialization/verification logic to make it a factory function.
//returns DateFormatError if the given //String does not represent a valid DateDate|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;}According to the type system, the caller is obliged to handle the DateFormatError:
value p = parsePeriod(start, end);if (is DateFormatError p) { ...}else { ...}Or, if we don't care about the actual problem with a given date format (which is possible, assuming that the initialization code we work on is missing that information), we can use Null instead of DateFormatError:
//returns null if the given String //does not represent a valid DateDate? 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;}The approach to using factory functions is excellent to say the least, as it generally has better isolation between validation logic and object initialization. This is especially useful in Ceylon, where the compiler adds some very strict restrictions to the object initialization logic to ensure that all areas of the object are assigned only once.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.