Статьи

Использование обобщенных ограничений типа — Как удалить код с Scala 2.8

Я люблю удалять код. Чем больше я удаляю, тем меньше площадь поверхности для укусов насекомых. Только сейчас я удалил кучу классов, ненужных по системе типов Scala 2.8.0. Рассмотрим этот набор абстракций, предназначенных для демонстрационных целей.

trait Instrument

// equity
case class Equity(name: String) extends Instrument

// fixed income
abstract class FI(name: String) extends Instrument
case class DiscountBond(name: String, discount: Int) extends FI(name)
case class CouponBond(name: String, coupon: Int) extends FI(name)

Хорошо, это иерархия инструментов (упрощенная), которая торгуется на бирже каждый день. Теперь мы моделируем торговлю ценными бумагами, которая обменивает инструменты и валюты.

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def calculateNetValue(..) = //..
def calculateValueDate(..) = //..
//..
}

В реальной жизни у торговли будет множество атрибутов. Но здесь они нам не нужны, так как наша единственная цель здесь — показать, как мы можем выбросить какой-то кусок кода 🙂

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

Рассмотрим один вариант использования, который рассчитывает начисленные проценты по сделке. Разница с другими методами заключается в том, что начисленные проценты применимы только к купонным облигациям, которые в соответствии с вышеуказанной иерархией являются подтипом FI. Как мы выражаем это ограничение в вышеупомянутой торговой абстракции? Нам нужно ограничить инструмент в методе.

Моя первоначальная реализация заключалась в том, чтобы сделать AccruedInterestCalculator отдельным классом, параметризованным с помощью Trade соответствующего инструмента.

class AccruedInterestCalculator[T <: Trade[CouponBond]](trade: T) {
def accruedInterest(convention: String) = //.. impl
}

и используйте его следующим образом ..

val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
new AccruedInterestCalculator(trd).accruedInterest("30U/360")

Введите Scala 2.8 и ограничения обобщенного типа.

До Scala 2.8 мы не могли специализировать Тип инструмента I для какого-либо конкретного метода в Trade, кроме того, что было указано в качестве ограничения при определении класса Trade. Поскольку расчет начисленных процентов действителен только для купонных облигаций, мы могли достичь желаемого эффекта только с помощью отдельной абстракции, как указано выше. Или мы можем прибегнуть к проверкам во время выполнения.

Scala 2.8 представляет ограничения обобщенного типа, которые позволяют вам делать именно это. У нас есть 3 варианта:

  • A =: = B, что означает, что A и B должны точно совпадать
  •  

  • A <: <B, который обязывает A соответствовать B
  •  

  • AA <% <B, что означает, что A должен быть видим как B

Predef.scala содержит эти определения. Обратите внимание, что в отличие от <: или>: ограничения обобщенного типа не являются операторами. Это классы, экземпляры которых неявно предоставляются самим компилятором для обеспечения соответствия ограничениям типа. Вот пример для нашего варианта использования.

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond): Int = {
//..
}
}

ev — это класс типов, предоставляемый компилятором, который гарантирует, что мы вызываем accruedInterest только для сделок CouponBond. Теперь вы можете сделать ..

val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
trd.accruedInterest("30U/360")

в то время как компилятор будет жаловаться на сделку с акциями ..

val eq = Equity("GOOG")
val trd = new Trade(2, "account-1", eq)
trd.accruedInterest("30U/360")

Теперь я могу выбросить свой класс AccruedInterestCalculator и все связанные с ним механизмы. Простое ограничение типа говорит нам многое и моделирует ограничения домена, и все это тоже во время компиляции. Yum!

Вы также можете использовать другие варианты, чтобы добиться большого эффекта при моделировании логики вашего домена. Предположим, у вас есть метод, который можно вызывать только для всех инструментов FI, вы можете кратко выразить ограничение, используя <: <..

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def validateInstrumentNotMatured(implicit ev: I <:< FI): Boolean = {
//..
}
}

Этот пост не о обсуждении всех возможностей обобщенных ограничений типов в Scala. Взгляните на эти
две
темы в StackOverflow и на эту информативную
суть Джейсона Заугга (@retronym в Twitter), чтобы узнать все подробности. Я только что показал вам, как я удалил часть своего кода, чтобы более кратко смоделировать мою реальную доменную логику, которая также быстро дает сбой во время компиляции.


Обновление: в ответ на комментарии относительно реализации Стратегии.

Стратегия отлично подходит для случаев, когда вы хотите иметь несколько реализаций алгоритма. В моем случае не было никаких изменений. Первоначально я держал это как отдельную абстракцию, потому что не мог ограничить тип инструмента в целом методе accruedInterest внутри торгового класса. Расчет accruedInterest — это обычная операция домена для сделки с CouponBond, поэтому trade.accruedInterest (..) выглядит как естественный API для контекста.

Теперь рассмотрим случай, когда стратегия расчета может меняться. Мы можем очень хорошо извлечь переменную часть из базовой реализации и смоделировать ее как отдельную стратегическую абстракцию. В нашем случае, скажем, расчет начисленных процентов будет зависеть от принципала сделки и даты сделки (опять же, для простоты демонстрации) .. следовательно, мы можем иметь следующий контракт и один пример реализации:

trait CalculationStrategy {
def calculate(principal: Int, tradeDate: java.util.Date): Int
}

case class DefaultImplementation(name: String) extends CalculationStrategy {
def calculate(principal: Int, tradeDate: java.util.Date) = {
//.. impl
}
}

Но как мы используем его в базовом API, который публикует класс Trade? Типы классов на помощь (однажды agian!) ..

class Trade[I <: Instrument](id: Int, account: String, instrument: I) {
//..
def accruedInterest(convention: String)(implicit ev: I =:= CouponBond, strategy: CalculationStrategy): Int = {
//..
}
}

и теперь мы можем использовать классы типов, используя нашу собственную конкретную реализацию ..

implicit val strategy = DefaultImplementation("default")

val cb = CouponBond("IBM", 10)
val trd = new Trade(1, "account-1", cb)
trd.accruedInterest("30U/360") // uses the default type class for the strategy

Теперь у нас есть лучшее из обоих миров. Мы реализуем ограничение домена для инструмента, используя обобщенные ограничения типов и используем классы типов, чтобы сделать стратегию расчета гибкой.

С http://debasishg.blogspot.com/2010/08/using-generalized-type-constraints-how.html