Я люблю удалять код. Чем больше я удаляю, тем меньше площадь поверхности для укусов насекомых. Только сейчас я удалил кучу классов, ненужных по системе типов 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