Какой правильный способ принудительного применения ограничений на значения класса case

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

Скажем, например, что мне нужно представлять диапазон, и что обе границы должны быть положительными. Непосредственная реализация:

case class Range(from: Int, to: Int)

Это, однако, не гарантирует, что оба from и to положительны, а to больше, чем from.

Мой первый инстинкт - реализовать его следующим образом:

case class Range(from: Int, to: Int) {
  require(from >= 0)
  require(to >= 0)
  require(to >= from)
}

Это, однако, делает конструктор Range небезопасным.

Существует ли общий шаблон, позволяющий упростить использование классов case, принудительное ограничение значений и избежать исключения исключений?

Ответы

Ответ 1

Это довольно субъективно, но я попробую.

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

Сравните это с поведением NullPointerException или ArithmethicException, которое по-прежнему происходит внутри Scala. Иногда просто не разумно программировать оборонительно, когда сталкивается с достаточно "безумием".

С другой стороны, если ваш класс заполнен значениями, у вас меньше контроля, т.е. они являются прямым следствием ввода пользователя, вам нужно просто взять больше контроля над своей конструкцией. Создайте объект-компаньон с функцией, которая возвращает Option или Either:

 object Range {
     def createSafe(from: Int, to: Int): Option[Range] = {
         if(from >= 0 && to >= 0 && to >= from)
            Some(Range(from, to))
         else
            None
     }
 }

Затем вы можете повторно использовать логику проверки в создании экземпляра вашего класса case, переопределив apply, как и предложил другой ответ.

 object Range {
    def createSafe ...
    def apply(from: Int, to: Int): Range = {
        createSafe(from, to).getOrElse(throw new IllegalArgumentException(...))
    }
 }

Ответ 2

Вы можете перегрузить оператор apply в сопутствующем объекте, чтобы добиться того, что вы хотели бы сделать:

object Range{
  def apply(from: Int, to: Int) ={
    val _from = Math.max(0, from)
    val _to = Math.max(0, to)
    if(_from < _to) new Range(_from, _to)
    else new Range(_to, _from)
  }
}

Заметьте, что использование to в качестве имени переменной может привести к некоторым "интересным" результатам.

Ответ 3

Вам нужно включить программирование типов. К сожалению, короткого ответа нет. Тем не менее, я недавно опубликовал серию статей по этой теме, которые, я думаю, могут помочь вам решить эту проблему: http://proseand.co.nz/2014/02/17/type-programming-shifting-from-values-to-types/