В этом цикле мы познакомимся с некоторыми способами использования типов в процессе проектирования. Можно сказать, что вдумчивое использование типов позволяет одновременно и сделать дизайн прозрачнее, и улучшить корректность.
Этот цикл сконцентрирован на «микро уровне» проектирования. Это значит, что он работает на низшем уровне отдельных типов и функций. Подходы высокоуровневого проектирования и связанные с ними решения об использовании функционального или объектно-ориентированного стиля мы обсудим в других циклах.
Многие из предложенных решений работают и в C#, и в Java, но легковесная природа типов в F# делает их использование гораздо более вероятным.
Основной пример
Чтобы продемонстрировать различные варианты использования типов, я буду работать над очень простым примером, а именно над типом Contact
(контакт).
type Contact =
{
FirstName: string;
MiddleInitial: string;
LastName: string;
EmailAddress: string;
// истина, если электронный адрес подтверждён
IsEmailVerified: bool;
Address1: string;
Address2: string;
City: string;
State: string;
Zip: string;
// истина, если адрес проверен внешней службой проверки адресов
IsAddressValid: bool;
}
Он выглядит очень просто и, я уверен, что мы все много раз видели нечто подобное. Что с ним можно сделать? Как его рефакторить, чтобы получить пользу от системы типов?
Создаём «атомарные» типы
Первое, что нужно сделать — исследовать, как именно происходит доступ к данным и их обновление. Скажем, можно ли обновить поле Zip
(почтовый индекс) без одновременного обновления Address1
(первая строка адреса)? С другой стороны, возможна ситуация, когда транзакция обновляет EmailAddress
(адрес электронной почты), но не FirstName
(имя).
Это наблюдение приводит нас к первому правилу:
Правило: Используйте записи или кортежи для группировки данных, которые должны быть согласованными (то есть «атомарными»), но не группируйте без необходимости не связанные между собой данные.
В данном случае очевидно, что у нас получается три набора данных: три составляющие имени, составляющие адреса и электронный адрес.
У нас также есть несколько дополнительных флагов, навроде IsAddressValid
(является ли адрес правильным?) и IsEmailVerified
(проверен ли электронный адрес?). Включать ли их в наборы или нет? В данном случае, конечно, да, поскольку флаги зависят от значений, к которым они относятся.
Например, при изменении EmailAddress
, одновременно стоит сбросить и флаг IsEmailVerified
.
В то же время PostalAddress
(почтовый адрес), очевидно, и сам по себе является полезным общим типом, без флага IsAddressFlag
. С другой стороны, флаг IsAddressValid
связан с адресом и должен обновляться, когда адрес меняется.
Поэтому, похоже, нам следует создать два типа. Один — это PostalAddress
вообще, и второй — адрес в контексте контакта, который можно назвать PostalContactInfo
(информация о почтовом адресе).
type PostalAddress =
{
Address1: string;
Address2: string;
City: string;
State: string;
Zip: string;
}
type PostalContactInfo =
{
Address: PostalAddress;
IsAddressValid: bool;
}
Помимо прочего, мы можем использовать опциональный тип, чтобы показать, что такие значения как MiddleInitial
(второе имя) на самом деле необязательны.
type PersonalName =
{
FirstName: string;
// используем "option" чтобы показать, что это поле необязательное
MiddleInitial: string option;
LastName: string;
}
Заключение
Со всеми этими изменениями мы получили такой код:
type PersonalName =
{
FirstName: string;
// используем "option" чтобы показать, что это поле необязательное
MiddleInitial: string option;
LastName: string;
}
type EmailContactInfo =
{
EmailAddress: string;
IsEmailVerified: bool;
}
type PostalAddress =
{
Address1: string;
Address2: string;
City: string;
State: string;
Zip: string;
}
type PostalContactInfo =
{
Address: PostalAddress;
IsAddressValid: bool;
}
type Contact =
{
Name: PersonalName;
EmailContactInfo: EmailContactInfo;
PostalContactInfo: PostalContactInfo;
}
Мы даже не написали ни одной функции, но код уже гораздо лучше представляет предметную область. Впрочем, это всего лишь начало.
Далее мы узнаем, как одновариатные объединения помогают придать примитивным типам семантическое значение.