When transforming Java code into Ceylon code, I sometimes encounter instances where Java class Constructors confuse validation with initialization. Let's use a simple but artificial code example to illustrate what I want to illustrate.
Some bad code
Consider the following Java class. (Man, don't write this 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 warned you earlier that it was man-made. However, finding something like this in real Java code is not uncommon in practice.
The problem here is that even if the validation fails for the input parameter (in the Hidden Parsedate () method), we will still get an period instance. But the period we get is not a "valid" state. Strictly speaking, what do I mean?
Well, if an object doesn't respond meaningfully to a common operation, I'd say it's in an inactive state. In this case, Getstartdate () and getenddate () throw a illegalstateexception exception, which I think is not "meaningful".
Looking at this example on the other hand, when designing period, we have a type-safe failure here. Unchecked exceptions represent a "vulnerability" in the type system. Therefore, a better period type-safe design would be an exception that does not use unchecked-in this case it means not to throw a illegalstateexception exception.
(In fact, in real code, I'm more likely to encounter a getstartdate () method that does not check for null, causing a NullPointerException exception after the line of code, which is even worse.) )
We can easily convert the period class above into a Ceylon form class:
Shared class Period (string start, String end) {
//returns null if the given string
//does not represent a valid Da Te
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
}
}
Of course, this code also encounters the same problem as the original Java code. Two assert symbols yell at us and there is a problem with the type safety of the code.
Make Java code better
How do we improve this code in Java? Well, here's an example of Java's much-maligned checked exceptions can be a very reasonable solution! We can slightly modify the 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 date FormatException {
...
}
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, using this solution, we will not get a period that is not in a valid state, and the instantiated period code will be handled by the compiler to handle invalid input situations, which will catch a dateformatexception exception.
try {
Period p = new Period (start, end);
...
}
catch (Dateformatexception DfE) {
...
}
This is an unusually good, perfect, and correct use of checked, unfortunately I rarely see Java code using checked exceptions like the above.
Make Ceylon Code better
So what about Ceylon? Ceylon has not checked for exceptions, so we need to find a different way to solve them. Typically, when a call to a function in Java throws a checked exception, Ceylon calls the function to return a union type. Because the initialization of a class does not return any type other than the class itself, we need to extract some mixed initialization/validation logic to make it a factory function.
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;
}
According to the type system, the caller is obligated to deal with Dateformaterror:
Value P = parseperiod (start, end);
if (is Dateformaterror p) {
...
}
else {
...
}
Or, if we don't care about the actual problem of a given date format (which is possible, assuming that the initialization code we're working on is missing that information), we can use NULL instead of 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 E Nddate = Parsedate (end))
then Period (StartDate, enddate)
else null;
Shared class Period (StartDate, EndDate) {
shared Date startdate;
Shared Date enddate;
}
At the very least, the method of using a factory function is excellent, because it is generally better isolated between validation logic and object initialization. This is particularly useful in Ceylon, where in Ceylon the compiler adds some very severe restrictions to the object initialization logic to ensure that all fields of the object are assigned only once.
The above is the entire content of this article, I hope to help you learn, but also hope that we support the cloud habitat community.