惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
Scott Helme
Scott Helme
P
Palo Alto Networks Blog
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
量子位
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
AWS News Blog
AWS News Blog
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
P
Proofpoint News Feed
V
V2EX
Martin Fowler
Martin Fowler
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
SecWiki News
SecWiki News
罗磊的独立博客
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
6. Проектирование с помощью типов: Типизированные строки
markshevchen · 2026-05-05 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели929

Туториал

Перевод

В одном из предыдущих постов я писал, что для представления электронных адресов, почтовых индексов, состояний, и т. д., можно использовать не простые примитивные строки, а размеченные объединения с одним вариантом (SCDU, Single Case Descriminated Unions). Благодаря им можно сделать типы хорошо различимыми и добавить правила валидации.

В этом посте мы узнаем, как сделать этот подход ещё более точным и строгим.

Когда строка — не строка?

Вот простой тип PersonalName:

type PersonalName =
    {
    FirstName: string;
    LastName: string;
    }

Тип утверждает, что личное имя сотрудника — это string. Но разве на самом деле это всё? Существуют ли другие ограничения, которые можно наложить на строку?

Для начала, она не должна принимать значение null. Впрочем, это и так справедливо для F#.

Что насчёт длины? Допустимо ли иметь имя длиной 64К символов? Если нет, существует ли какая-то максимальная длина?

Может ли имя содержать переводы строки или табуляции? Может ли оно начинаться или заканчиваться пробелом?

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

Должны ли ограничения быть частью предметной области?

Мы можем сознавать, что существует какие-то ограничения, но действительно ли их надо делать частью предметной области (и соответствующих типов, которые от неё зависят)? Например, ограничение, что фамилия должна быть не длиннее ста символов, относится к конкретной реализации, а не к предметной области в целом.

Я мог бы сказать о различии между логической моделью и физической моделью. В логической модели часть ограничений может быть неважной, но в физической модели они, безусловно, важны. А когда мы пишем код, мы имеем дело с физической моделью.

Ещё одна причина поместить ограничения в модель, заключается в том, что она, зачастую, совместно используется несколькими приложениями. Скажем, личное имя может быть создано в приложении электронной коммерции, которое записывает его в таблицу БД, а затем отправляет в очередь сообщений системы CRM, которая, в свою очередь, вызывает службу шаблонов электронной почты и т. д.

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

Приходилось ли вам писать код, который проверяет длину строки перед записью в базу?

void SaveToDatabase(PersonalName personalName)
{
   var first = personalName.First;
   if (first.Length > 50)
   {
        // убедиться, что строка не слишком длинная
        first = first.Substring(0,50);
   }

   //сохранить в базу
}

Что делать, если строка оказалась слишком длинной? Молча её обрезать? Бросить исключение?

Лучший ответ — вообще не попадать в такую ситуацию. Поздно принимать решение на этапе, когда строка пишется в базу данных.

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

Но как убедиться, что строка прошла валидацию на всех возможных путях программы? Думаю, вы знаете ответ…

Моделируем типизированные строки

Ответ, конечно, в том, чтобы написать типы-обёртки и встроить ограничения в них.

Накидаем прототип, используя знакомые нам одновариантные объединения.

module String100 =
    type T = String100 of string
    let create (s:string) =
        if s <> null && s.Length <= 100
        then Some (String100 s)
        else None
    let apply f (String100 s) = f s
    let value s = apply id s

module String50 =
    type T = String50 of string
    let create (s:string) =
        if s <> null && s.Length <= 50
        then Some (String50 s)
        else None
    let apply f (String50 s) = f s
    let value s = apply id s

module String2 =
    type T = String2 of string
    let create (s:string) =
        if s <> null && s.Length <= 2
        then Some (String2 s)
        else None
    let apply f (String2 s) = f s
    let value s = apply id s

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

Примеры хорошей и плохой строки длины 2.

let s2good = String2.create "CA"
let s2bad = String2.create "California"

match s2bad with
| Some s2 -> // обновляем объект предметной области
| None -> // обрабатываем ошибку

Чтобы использовать значение String2, мы должны проверить его при создании.

Проблемы подобного дизайна

Одна их проблем заключается в том, что мы получаем много дублирующегося кода. Впрочем, в типичной предметной области всего несколько десятков строковых типов, так что дублей будет не очень много. Но мы всё же постараемся избавиться от дублей.

Другая проблема в том, что мы «сломали» сравнение строк, а это уже серьёзно. Типы String50 и String100 отличаются друг от друга, так что их нельзя сравнивать напрямую.

let s50 = String50.create "John"
let s100 = String100.create "Smith"

let s50' = s50.Value
let s100' = s100.Value

let areEqual = (s50' = s100')  // ошибка компилятора

Всё это усложняет работу со словарями и списками.

Рефакторинг

Воспользуемся тем, что в F# есть интерфейсы и создадим общий интерфейс для типов-обёрток, описав несколько стандартных методов.

module WrappedString =

    /// Интерфейс, который поддерживают все типы-обёртки
    type IWrappedString =
        abstract Value : string

    /// Создать завёрнутое опциональное значение
    /// 1) Сперва привести входные данные к каноническому виду
    /// 2) Если проверка прошла успешно, вернуть Some с результатом конструктора
    /// 3) Если проверка прошла неудачно, вернуть None
    /// Значения null не являются корректными
    let create canonicalize isValid ctor (s:string) =
        if s = null
        then None
        else
            let s' = canonicalize s
            if isValid s'
            then Some (ctor s')
            else None

    /// Применить функцию к завёрнутому значению
    let apply f (s:IWrappedString) =
        s.Value |> f

    /// Вернуть завёрнутое значение
    let value s = apply id s

    /// Проверить на равенство
    let equals left right =
        (value left) = (value right)

    /// Сравнить
    let compareTo left right =
        (value left).CompareTo (value right)

Ключевая функция — это create, которая получает конструктор и создаёт значения только в случае успешной валидации.

С помощью интерфейса создавать новые типы намного проще:

module WrappedString =

    // ... код из примера выше ...

    /// Перед конструированием приводим строку к каноническому виду:
    /// * ковертируем все пробельные символы в пробелы
    /// * обрезаем строку слева и справа
    let singleLineTrimmed s =
        System.Text.RegularExpressions.Regex.Replace(s,"\s"," ").Trim()

    /// Функция валидации, основанная на длине строки
    let lengthValidator len (s:string) =
        s.Length <= len

    /// Строка длины 100
    type String100 = String100 of string with
        interface IWrappedString with
            member this.Value = let (String100 s) = this in s

    /// Конструктор строк длины 100
    let string100 = create singleLineTrimmed (lengthValidator 100) String100

    /// Конвертирует завёрнутую строку в строку длины 100
    let convertTo100 s = apply string100 s

    /// Строка длины 50
    type String50 = String50 of string with
        interface IWrappedString with
            member this.Value = let (String50 s) = this in s

    /// Конструктор строк длины 50
    let string50 = create singleLineTrimmed (lengthValidator 50)  String50

    /// Конвертирует завёрнутую строку в строку длины 50
    let convertTo50 s = apply string50 s

Теперь для каждого вида строк нам нужно:

  • создать тип (String100)

  • реализовать для него IWrappedString

  • написать публичный конструктор (string100).

(В примере выше я также написал полезную функцию convertTo, чтобы конвертировать типы друг в друга.)

Тип — это обычный тип-обёртка, с которыми мы уже сталкивались.

Реализация метода Value из IWrappedString может занимать две строки:

member this.Value =
    let (String100 s) = this
    s

но я предпочитаю короткий однострочный вариант:

member this.Value = let (String100 s) = this in s

Функция string100 тоже очень простая. Она вызывает функцию приведения к каноническому виду — singleLineTrimmed, функцию валидации, которая проверяет длину, и функцию-конструктор — String100. Последняя — это конструктор единственного варианта в типе-объединении. Не путайте её с самим типом, который носит то же имя.

let string100 = create singleLineTrimmed (lengthValidator 100) String100

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

module WrappedString =

    // ... код из примера выше ...

    /// Текст длины 1000 с переводами строк
    type Text1000 = Text1000 of string with
        interface IWrappedString with
            member this.Value = let (Text1000 s) = this in s

    /// Конструктор с поддержкой переводов строки длины 1000
    let text1000 = create id (lengthValidator 1000) Text1000

Экспериментируем с модулем WrappedString

Чтобы разобраться, как работает модуль, поэкспериментируем с ним в интерактивном режиме:

let s50 = WrappedString.string50 "abc" |> Option.get
printfn "s50 is %A" s50
let bad = WrappedString.string50 null
printfn "bad is %A" bad
let s100 = WrappedString.string100 "abc" |> Option.get
printfn "s100 is %A" s100

// проверка на равенство с помощью функции модуля возвращет true
printfn "s50 is equal to s100 using module equals? %b" (WrappedString.equals s50 s100)

// проверка на равество с помощью метода класса Object вовзвращает false
printfn "s50 is equal to s100 using Object.Equals? %b" (s50.Equals s100)

// прямое сравнение не компилируется
printfn "s50 is equal to s100? %b" (s50 = s100) // ошибка компилятора

Напишем несколько вспомогательных функций для работы с типом Map, который использует простые строки.

module WrappedString =

    // ... код из примера выше ...

    /// вспомогательные функции для словарей
    let mapAdd k v map =
        Map.add (value k) v map

    let mapContainsKey k map =
        Map.containsKey (value k) map

    let mapTryFind k map =
        Map.tryFind (value k) map

Примеры вызова:

let abc = WrappedString.string50 "abc" |> Option.get
let def = WrappedString.string100 "def" |> Option.get
let map =
    Map.empty
    |> WrappedString.mapAdd abc "значение для ключа abc"
    |> WrappedString.mapAdd def "значение для ключа def"

printfn "Найден ли ключ abc в словаре? %A" (WrappedString.mapTryFind abc map)

let xyz = WrappedString.string100 "xyz" |> Option.get
printfn "Найден ли ключ xyz в словаре? %A" (WrappedString.mapTryFind xyz map)

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

Используем новые строковые типы в предметной области

Пользуясь полученными знаниями, поменяем определение типа PersonalName.

module PersonalName =
    open WrappedString

    type T =
        {
        FirstName: String50;
        LastName: String100;
        }

    /// создаём новое значение
    let create first last =
        match (string50 first),(string100 last) with
        | Some f, Some l ->
            Some {
                FirstName = f;
                LastName = l;
                }
        | _ ->
            None

Мы создали модуль для типа и добавили функцию create, которая превращает пару строк в PersonalName.

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

В данном случае мы используем простой подход, создавая при неудачной валидации опциональный тип со значением None.

Применение:

let name = PersonalName.create "John" "Smith"

Можно добавить в модуль несколько полезных функций.

Например, функцию fullName которая возвращает склеенные вместе имя и фамилию.

Чтобы её написать, нужно принять несколько решений.

  • Возвращать простую строку или типизированную? Преимущество последней в том, что вызывающая сторона точно знает, какой длины будет строка, что совместимо с другими подобными типами.

  • Возвращая типизированную строку (например String100), что мы будем делать, если склеенная строка окажется слишком большой? (Она может содержать до 151 символа, исходя из длины строк для имени и фамилии). Мы могли бы или вернуть опциональный результат или принудительно её укоротить.

Реализация всех трёх вариантов:

module PersonalName =

    // ... код из примера выше ...

    /// склеиваем вместе имя и фамилию
    /// возвращаем простую строку
    let fullNameRaw personalName =
        let f = personalName.FirstName |> value
        let l = personalName.LastName |> value
        f + " " + l

    /// склеиваем вместе имя и фамилию
    /// возвращаем None, если строка слишком длинная
    let fullNameOption personalName =
        personalName |> fullNameRaw |> string100

    /// склеиваем вместе имя и фамилию
    /// укорачиваем строку, если она слишком длинная
    let fullNameTruncated personalName =
        // вспомогательная функция
        let left n (s:string) =
            if (s.Length > n)
            then s.Substring(0,n)
            else s

        personalName
        |> fullNameRaw  // склеить
        |> left 100     // укоротить
        |> string100    // завернуть
        |> Option.get   // здесь всеегда будет значениеok)

Какой именно подход к реализации fillName выбрать, решать вам. Но важно понять ключевой принцип такого стиля типо-ориентированного дизайна: все решения надо принимать заранее, при написании кода. Нельзя откладывать их на потом.

Иногда это сильно раздражает, но мне кажется, что в целом это правильный подход.

Новый взгляд на старые типы: электронный адрес и почтовый индекс

Мы можем использовать модуль WrappedString для повторной реализации типов EmailAddress и ZipCode.

module EmailAddress =

    type T = EmailAddress of string with
        interface WrappedString.IWrappedString with
            member this.Value = let (EmailAddress s) = this in s

    let create =
        let canonicalize = WrappedString.singleLineTrimmed
        let isValid s =
            (WrappedString.lengthValidator 100 s) &&
            System.Text.RegularExpressions.Regex.IsMatch(s,@"^\S+@\S+\.\S+$")
        WrappedString.create canonicalize isValid EmailAddress

    /// Конвертирует любую завёрнтую строку в EmailAddress
    let convert s = WrappedString.apply create s

module ZipCode =

    type T = ZipCode of string with
        interface WrappedString.IWrappedString with
            member this.Value = let (ZipCode s) = this in s

    let create =
        let canonicalize = WrappedString.singleLineTrimmed
        let isValid s =
            System.Text.RegularExpressions.Regex.IsMatch(s,@"^\d{5}$")
        WrappedString.create canonicalize isValid ZipCode

    /// Конвертирует любую завёрнутую строку в ZipCode
    let convert s = WrappedString.apply create s

Другие применения типизированных строк

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

Один из примеров, которые приходят на ум — экранирование и разэкранирование строк в веб-приложениях.

Скажем, вы хотите вывести строку в HTML. Надо ли её экранировать? Если она уже экранирована, её надо оставить в покое, но если нет, строку надо экранировать.

Это может стать проблемой. Джоэл Спольски обсуждал решение, основанное на именах переменных, но в F# мы, конечно, хотим решение, основанное на типах.

Решение, основанное на типах, вероятно, будет использовать тип для «безопасных» строк HTML (скажем HtmlString), тип для безопасных строк JavaScript (JsString) и тип для безопасных строк SQL (SqlString). Эти строки можно использовать в одном коде, не опасаясь случайно их перепутать.

Я не стану приводить здесь решение (тем боле, что вы, вероятно, всё равно используете Razor), но если вам интересно, вы можете прочитать о подходе, приянятом в Haskell и его версии, портированной на F#.

Обновление

Многие читатели просили меня рассказать, как гарантировать создание типизированных строк вроде EmailAddress только с помощью валидирующего конструктора. Я написал git-фрагмент, который содержит несколько работающих способов.