При портировании кода Java на Ceylon я иногда сталкиваюсь с классами Java, где конструктор смешивает проверку с инициализацией . Давайте проиллюстрируем, что я имею в виду, с помощью простого, но очень надуманного примера.
Какой-то плохой код
Рассмотрим этот класс Java. (Старайтесь не писать такой код дома, детки!)
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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 не IllegalStateException .
(На самом деле, на практике в реальном коде я с большей вероятностью сталкиваюсь с getStartDate() который не проверяет null , а фактически приводит к NullPointerException ниже, что еще хуже.)
Мы можем легко перевести вышеуказанный класс Period на Цейлон:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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 чтобы выдать проверенное исключение из его конструктора
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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 , обязан компилятором обрабатывать случай неверного ввода, catch где-то DateFormatException .
|
1
2
3
4
5
6
7
|
try { Period p = new Period(start, end); ...}catch (DateFormatException dfe) { ...} |
Это хорошее, отличное и правильное использование проверенных исключений, и, к сожалению, я редко нахожу код Java, который использует проверенные исключения, подобные этому.
Делаем код Цейлона лучше
Как насчет Цейлона? У Цейлона нет проверенных исключений, поэтому нам придется искать другое решение. Как правило, в тех случаях, когда Java будет вызывать использование функции, которая выдает проверенное исключение, Ceylon будет вызывать использование функции, которая возвращает тип объединения. Поскольку инициализатор класса не может возвращать какой-либо тип, кроме самого класса, нам необходимо извлечь некоторую смешанную логику инициализации / проверки в функцию фабрики.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
//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;} |
Система типов заставляет вызывающего иметь дело с DateFormatError :
|
1
2
3
4
5
6
7
|
value p = parsePeriod(start, end);if (is DateFormatError p) { ...}else { ...} |
Или, если мы не заботимся о реальной проблеме с данным форматом даты (вероятно, учитывая, что исходный код, из которого мы работали, потеряли эту информацию), мы могли бы просто использовать Null вместо DateFormatError :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
/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;} |
По крайней мере, можно утверждать, что подход использования фабричной функции лучше, так как в целом он обеспечивает лучшее разделение между логикой проверки и инициализацией объекта. Это особенно полезно в Цейлоне, где компилятор налагает некоторые довольно жесткие ограничения на логику инициализации объекта, чтобы гарантировать, что все поля объекта назначаются ровно один раз.
Резюме
В заключении:
- Попробуйте отделить валидацию от инициализации, где это целесообразно.
- Логика валидации обычно не принадлежит конструкторам (особенно не в Цейлоне).
- Не создавайте объекты в «недопустимых» состояниях.
- «Недопустимое» состояние иногда может быть обнаружено путем поиска сбоев безопасности типов.
- В Java разумной альтернативой является конструктор или фабричная функция, которая выдает проверенное исключение.
- В Цейлоне фабричная функция, которая возвращает тип объединения, является разумной альтернативой.
| Ссылка: | Цейлон: постройка объекта и проверка от нашего партнера JCG Гэвина Кинга в блоге команды Ceylon Team . |