Использование статической проверки типов для защиты от бизнес-ошибок

13

Я большой поклонник статической проверки типов. Это мешает вам делать глупые ошибки, подобные этой:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

Но это не мешает вам делать глупые ошибки, подобные этой:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

Проблема возникает, когда вы используете один и тот же тип для представления явно разных видов информации. Я думал, что хорошим решением для этого было бы расширение Integerкласса, просто для предотвращения ошибок бизнес-логики, но не для добавления функциональности. Например:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Это считается хорошей практикой? Или есть непредвиденные инженерные проблемы в будущем с таким ограниченным дизайном?

J-боб
источник
5
Все a.SetAge( new Age(150) )еще не скомпилируется?
Джон Ву
1
Тип int в Java имеет фиксированный диапазон. Очевидно, что вы хотели бы иметь целые числа с пользовательскими диапазонами, например, Integer <18, 110>. Вы ищете уточняющие типы или зависимые типы, которые предлагают некоторые (неосновные) языки.
Теодорос Чатзигианнакис
3
То, о чем вы говорите, это отличный способ сделать дизайн! К сожалению, Java имеет настолько примитивную систему типов, что ее трудно сделать правильно. И даже если вы попытаетесь это сделать, это может привести к побочным эффектам, таким как снижение производительности или снижение производительности разработчика.
Эйфорическая
@JohnWu да, ваш пример все еще будет компилироваться, но это единственная точка отказа для этой логики. После того, как вы объявите свой new Age(...)объект, вы не сможете неправильно назначить его переменной типа Weightв других местах. Это уменьшает количество мест, где могут возникнуть ошибки.
J-боб

Ответы:

12

По сути, вы запрашиваете единичную систему (нет, не юнит-тесты, «юнит», как в «физической единице», например, счетчики, вольт и т. Д.).

В вашем коде Ageпредставляет время и Poundsпредставляет массу. Это приводит к таким вещам, как преобразование единиц измерения, базовые единицы измерения, точность и т. Д.


Были / есть попытки внедрить такую ​​вещь в Java, например:

Последние два, кажется, живут в этом github: https://github.com/unitsofmeasurement


C ++ имеет единицы через Boost


LabView поставляется с кучей модулей .


Есть другие примеры на других языках. (изменения приветствуются)


Это считается хорошей практикой?

Как вы можете видеть выше, чем более вероятно использование языка для обработки значений с единицами, тем более естественным образом он поддерживает единицы измерения. LabView часто используется для взаимодействия с измерительными приборами. Таким образом, имеет смысл иметь такую ​​функцию в языке, и ее использование, безусловно, будет считаться хорошей практикой.

Но в любом языке высокого уровня общего назначения, где спрос на такую ​​строгость низкий, это, вероятно, неожиданно.

Или есть непредвиденные инженерные проблемы в будущем с таким ограниченным дизайном?

Мое предположение будет: производительность / память. Если вы работаете с большим количеством значений, накладные расходы объекта на значение могут стать проблемой. Но как всегда: преждевременная оптимизация - корень всего зла .

Я думаю, что большая «проблема» - это привыкание людей, так как единица обычно неявно определяется следующим образом:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

Люди будут сбиты с толку, когда им придется передать объект как значение для чего-то, что, казалось бы, можно описать простым int, когда они не знакомы с системами единиц.

значение NULL
источник
«Люди будут сбиты с толку, когда им придется передать объект как ценность для чего-то, что, по-видимому, можно описать простым int...» - для которого мы руководствуемся принципом наименьшего сюрприза. Хороший улов.
Грег Бургхардт
1
Я думаю, что пользовательские диапазоны перемещают это из области библиотеки единиц в зависимые типы
jk.
6

В отличие от нулевого ответа, определение типа для «единицы измерения» может быть полезным, если целого числа недостаточно для описания измерения. Например, вес часто измеряется в нескольких единицах в пределах одной и той же системы измерения. Подумайте «фунты» и «унции» или «килограммы» и «граммы».

Если вам нужен более детальный уровень измерения, определите тип для единицы:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

Для чего-то вроде «возраста» я рекомендую рассчитать это во время выполнения на основе даты рождения человека:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}
Грег Бургардт
источник
2

То, что вы ищете, известно как помеченные типы . Они являются способом сказать «это целое число, которое представляет возраст», в то время как «это также целое число, но оно представляет вес» и «вы не можете назначить одно другому». Обратите внимание, что это идет дальше, чем физические единицы, такие как метры или килограммы: у меня в программе могут быть «высоты людей» и «расстояния между точками на карте», которые измеряются в метрах, но не совместимы друг с другом, так как я назначил один для другой не имеет смысла с точки зрения бизнес-логики.

Некоторые языки, такие как Scala, довольно легко поддерживают теговые типы (см. Ссылку выше). В других вы можете создавать свои собственные классы-обертки, но это менее удобно.

Проверка, например, проверка того, что рост человека является «разумным», является другой проблемой. Вы можете поместить такой код в ваш Adultкласс (конструктор или сеттеры) или внутри ваших классов с тегами type / wrapper. В некотором смысле, встроенные классы, такие как URLили UUIDвыполняют такую ​​роль (среди прочего, например, предоставляя служебные методы).

Использование теговых типов или классов-оберток на самом деле поможет улучшить ваш код, будет зависеть от нескольких факторов. Если ваши объекты просты и имеют мало полей, риск неправильного их назначения низок, и дополнительный код, необходимый для использования теговых типов, может не стоить усилий. В сложных системах со сложной структурой и множеством полей (особенно, если многие из них имеют один и тот же примитивный тип), это может оказаться полезным.

В коде, который я пишу, я часто создаю классы-обертки, если обхожу карты. Такие типы, как Map<String, String>сами по себе, очень непрозрачны, поэтому перенос их в классы со значимыми именами, например, очень NameToAddressпомогает. Конечно, с помеченными типами вы можете писать Map<Name, Address>и не нуждаться в обертке для всей карты.

Однако для простых типов, таких как Strings или Integer, я обнаружил, что классы-обертки (в Java) слишком неприятны. Обычная бизнес-логика была не так уж и плоха, но возникло множество проблем с сериализацией этих типов в JSON, их сопоставлением с объектами БД и т. Д. Вы можете написать мапперы и хуки для всех больших сред (например, Jackson и Spring Data), но дополнительная работа и обслуживание, связанные с этим кодом, компенсируют все, что вы получаете от использования этих оболочек. Конечно, у YMMV и в другой системе баланс может быть разным.

Михал Космульский
источник