В одной из моих предыдущих публикаций (почти год назад) я указывал, как моделирование на основе типов приводит к сжатым доменным структурам, которые наследуют следующие достоинства:
- Меньшее количество кода для записи, поскольку статические типы инкапсулируют множество бизнес-ограничений
- Меньшее количество тестов, чтобы написать, так как компилятор пишет их неявно для вас
В недавней ветке в Твиттере я упоминал об одном комментарии, сделанном Мануэлем Чакраварти в одной из публикаций в блоге Micheal Feathers.
«Конечно, строгая проверка типов не может заменить строгую дисциплину тестирования, но она делает вас более уверенным, чтобы принять большие шаги. «
Это заявление отразилось на моих собственных чувствах к статической типизации, которые я уже довольно давно практикую с использованием Scala. Поскольку тема в Твиттере стала громче, Патрик Логан сделал интересный комментарий в моем блоге на эту тему.
Это интересно … это долгий путь к тому типу объяснения, которое я искал в отношении «программирования на основе типов» с богатыми системами типов, в отличие от «программирования на основе тестов» с динамическими языками.
Я все еще большой поклонник последнего и не полностью понимаю первое.
Я был бы заинтересован в вашем процессе «разработки типов» — без каких-либо «тестов» система типов может проверять «правильность типов» ваших типов, но откуда вы знаете, что это типы, которые вы на самом деле * хотите * сделать доказал звук?
и разговор стал немного длиннее, когда мы оба пытались разобраться в практических приемах и тонкостях, которые моделирование предметной области с ограничениями типов навязывает программисту. Один из вопросов, который поднял Патрик, касался того, какие тесты вы обычно предоставляете для такого кода.
Позвольте мне попытаться взглянуть на некоторые из реальных кодов, на которых я использовал эту практику. Когда у меня есть фрагмент кода, как это ..
/**
* A trade needs to have a Trading Account
*/
trait Trade {
type T
val account: T
def valueOf: Unit
}
/**
* An equity trade needs to have a Stock as the instrument
*/
trait EquityTrade extends Trade {
override def valueOf {
//.. calculate value
}
}
/**
* A fixed income trade needs to have a FixedIncome type of instrument
*/
trait FixedIncomeTrade extends Trade {
override def valueOf {
//.. calculate value
}
}
//..
//..
/**
* Accrued Interest is computed only for fixed income trades
*/
trait AccruedInterestCalculatorComponent {
type T
val acc: AccruedInterestCalculator
trait AccruedInterestCalculator {
def calculate(trade: T)
}
}
Мне нужно сделать проверки и написать модульные и функциональные тесты для проверки ..
- EquityTrade должен работать только на инструментах класса акций
- FixedIncomeTrade должен работать только с фиксированным доходом, а не с другими инструментами
- Для каждого метода в доменной модели, который использует инструмент или сделку, мне нужно проверить, соответствует ли переданный инструмент или сделка правильному типу, а также написать модульные тесты, которые проверяют то же самое. AccruedInterestCalculator принимает в качестве аргумента сделку, которая должна иметь тип FixedIncomeTrade, поскольку начисленные проценты имеют смысл только для сделок с облигациями. Метод AccruedInterestCalculator # calc () должен выполнить явную проверку для типа сделки, что заставляет меня писать модульные тесты, а также для действительных и недействительных вариантов использования.
Теперь давайте введем ограничения типов, которые предлагает статически типизированный язык с мощной системой типов.
trait Trade {
type T <: Trading
val account: T
//..as above
}
trait EquityTrade extends Trade {
type S <: Stock
val equity: S
//.. as above
}
trait FixedIncomeTrade extends Trade {
type FI <: FixedIncome
val fi: FI
//.. as above
}
//..
Как только мы добавим эти ограничения типов, наша модель предметной области станет более выразительной и неявно ограниченной многими бизнес-правилами … как, например, ..
- Торговля происходит только на Торговом счете
- EquityTrade имеет дело только с Акциями, а FixedIncomeTrade имеет дело исключительно с инструментами типа FixedIncome.
Рассмотрим этот более выразительный пример, который обходит ограничения домена прямо перед вами, без того, чтобы они были скрыты в логике процедурного кода в форме проверок во время выполнения. Обратите внимание, что в следующем примере все типы и значения, которые были оставлены абстрактными ранее, создаются при определении конкретного компонента. И вы можете только создать экземпляр с соблюдением правил домена, которые вы определили ранее. Насколько это полезно в качестве краткого способа написания краткой логики предметной области без необходимости написания какого-либо модульного теста?
object FixedIncomeTradeComponentRegistry extends TradingServiceComponentImpl
with AccruedInterestCalculatorComponentImpl
with TaxRuleComponentImpl {
type T = FixedIncomeTrade
val tax = new TaxRuleServiceImpl
val trd = new TradingServiceImpl
val acc = new AccruedInterestCalculatorImpl
}
Каждая проводка, которую вы делаете выше, статически проверяется на согласованность — следовательно, компонент FixedIncome, который вы создаете, будет соблюдать все правила домена, которые вы вставили в него через явные ограничения типов.
Хорошая часть заключается в том, что эти бизнес-правила будут применяться самим компилятором, и мне не придется писать какие-либо дополнительные явные проверки в базе кода. И компилятор также является инструментом тестирования — вы не сможете создать экземпляр FixedIncomeTrade с помощью инструмента, который не является подтипом FixedIncome.
Тогда как мы можем протестировать такие абстракции домена с ограниченными типами
Правило № 1: Типовые ограничения проверяются компилятором. Вы не можете создать экземпляр несовместимого компонента, который нарушает ограничения, которые вы включили в абстракции вашего домена.
Правило № 2: вам нужно писать тесты только для бизнес-логики, которые составляют процедурную часть ваших абстракций. Очевидно! Типы не могут помочь. Но если вы используете статически типизированный язык, получите максимум от абстракций, которые предлагает система типов. Существуют ситуации, когда вы обнаружите повторяющуюся процедурную бизнес-логику с небольшими изменениями, разбросанными по всей базе кода. Если вы работаете со статически типизированным языком, смоделируйте их в семейство типов. Ваши тесты для этой логики будут локализованы * только * внутри самого типа. Это верно и для динамически типизированных языков. Преимущество статической типизации заключается в том, что все случаи использования будут проверяться статически.компилятором. В статически типизированном языке вы думаете и моделируете в «типах». В динамически типизированных языках вы мыслите в терминах сообщений, которые должны обрабатывать абстракции.
Правило № 3: Но вам нужно создавать экземпляры ваших абстракций в тестах. Как ты это делаешь ? Очень скоро вы заметите, что основная часть ваших тестов загрязняется сложными экземплярами, использующими бетон val или инъекцию типа. Обычно я использую генераторы, которые предлагает ScalaCheck . ScalaCheck предлагает специальный генератор org.scalacheck.Arbitrary.arbitrary, который генерирует произвольные значения любого поддерживаемого типа. И когда у вас есть генераторы, вы можете использовать их для написания свойств, которые выполняют необходимое тестирование остальной логики вашего домена.