Comments 89
И что?
Суть статьи напомнить об одной из важных идей программирования, которая заключается в использовании конкретных типов для конкретных нужд. Вместо всеобъемлющего String конкретный EmailAddress, о чем Хоар написал еще в 1972 году (это его раздел в книге).
Там где есть возможность использовать Enumeration c subrange соответсвенно использовать его, например, для месяцев вместо Integer использовать Enum(Jan… Dec), вместо широчайшего Integer для минут Enum(0… 59), и т. д.
В совокупности же с Design by Contracts проверки уже можно делать сегодня даже на стадии компиляции.
Например, можно настроить программу в Java и .NET так, что вызов упомянутого в статье метода со следующими аргументами
SendEmail(new EmailAddress("wrongEmail@com", "");
покажет ошибку на стадии компиляции.Делайте код как можно более читаемым для других. Объявляя тип String для Email вы обманываете пользователей ваших методов, ведь «111», «222@ccc», «wrongEmail@com» являются типом String, но не валидным Email'ом. Также когда вы пишите софт для банкомата и декларируете аргумент в виде Interger/Decimal/Money для внесенных денег, явно показывая что вы принимаете -100 долларов, и -2 млрд. долларов, а неявно, в лучшем случае если не забыли, прокидываете где-то после ошибки. Другие возможные применения читайте в статье.
Очевидным бонусом использования конкретных типов является их переиспользование. Делая EmailAddress валидным всегда (например, через проверку и выброс ошибки в конструкторе), вы можете быть уверены, что это всегда правильный Email и не надо дополнительных проверок аля IsEmail() в других методах принимающих обширный String.
Это старые истины ООП, но мало кто это сегодня использует. Например, современный подход по поводу проверки Email в .NET через DataAnnotations выглядит так:
function void SendEmail([EmalAddress][Required] String email, [Required]String body){}
Если есть еще вопросы, с удовольствием отвечу, обращайтесь.
Суть статьи напомнить об одной из важных идей программирования, которая заключается в использовании конкретных типов для конкретных нужд.
А вы думаете, о ней забыли?
В совокупности же с Design by Contracts проверки уже можно делать сегодня даже на стадии компиляции.
Так, стоп. Давайте не будем путать design by contract, который использует контракты как набор утверждений, и type-based design, который использует статическую систему типов для гарантии корректности.
Например, можно настроить программу в Java и .NET так, что вызов упомянутого в статье метода со следующими аргументами SendEmail(new EmailAddress("wrongEmail@com", "");
покажет ошибку на стадии компиляции.
В .net — нельзя. Code Contracts работает иначе. Ну и да, мы оставим за отдельными скобками тот вопрос, правильно ли это.
Например, современный подход по поводу проверки Email в .NET через DataAnnotations выглядит так: function void SendEmail([EmalAddress][Required] String email, [Required]String body){}
Ну во-первых, это ровно тот самый design by contract, к которому вы только что призывали. Во-вторых, нет, это не "современный подход". Data Annotations применяются на другом уровне и для других целей. В частности, они применяются в UI, где применение строгой системы типов просто противопоказано.
Но, повторюсь, вы почему-то думаете, что про type-based design забыли — хотя и на Хабре по этому поводу есть статьи, и у Симана есть прекрасные примеры заведомо бизнес-корректных систем. Проблема type-based в том, что для них нужна существенно более мощная, нежели в том же C#, система типов, иначе это очень дорого. И начинать надо с — внезапно — устранения повсеместных null
(о которых, кстати, в вашем тексте ни слова).
А еще проблема type-based в том, что обещанное вами переиспользование не работает. Нет общей библиотеки типов, где есть EmailAddress
, поэтому две соседних библиотеки будут иметь две разных реализации, после чего нам понадобится писать конвертеры в две стороны.
Отвечу в таком же стиле.
Так, стоп. Давайте не будем путать design by contract, который использует контракты как набор утверждений, и type-based design, который использует статическую систему типов для гарантии корректности.
Здесь нет путаницы, просто сделайте один шажок дальше.
В .net — нельзя. Code Contracts работает иначе. Ну и да, мы оставим за отдельными скобками тот вопрос, правильно ли это.
Я вас приятно удивлю прикрепленным скриншотом. Data Contracts используют статическую проверку, при отключении кнопки Compile in backrground в CodeContracts вы получите Failed Compilation на стадии компиляции проекта. Программисты Microsoft неоднократно заявляли о сложности такой проверки путем parsing'a. Для дополнительной оптимизации используется компиляция кода с пограничными условиями, подробнее в блогах MS команды CodeContracts.
Ну во-первых, это ровно тот самый design by contract, к которому вы только что призывали. Во-вторых, нет, это не «современный подход». Data Annotations применяются на другом уровне и для других целей. В частности, они применяются в UI, где применение строгой системы типов просто противопоказано.
Для меня лично есть огромная и существенная разница между использованием набора атрибутов над классом String и типом EmailAddress.
Удивлю приятно второй раз, data annotations еще и для создания баз данных используют.
Но, повторюсь, вы почему-то думаете, что про type-based design забыли — хотя и на Хабре по этому поводу есть статьи, и у Симана есть прекрасные примеры заведомо бизнес-корректных систем. Проблема type-based в том, что для них нужна существенно более мощная, нежели в том же C#, система типов, иначе это очень дорого. И начинать надо с — внезапно — устранения повсеместных null (о которых, кстати, в вашем тексте ни слова).
Проблема Null reference активно обсуждается пользователями и архитекторами новых версий .NET, про это можно писать отдельную большую статью, которая выходит за рамки данной темы.
Если задуматься о чем статья и о чем пишет Хоар, то это не отдельно про subrange enum'ы, а про удобство использования более конкретных типов для конкретных нужд. И не важно, используете ли вы Enum или class с возможностью статического анализа проверок через контракты или что-то еще. Смотрите шире. Можно и без Design By Contracts обойтись, просто используя TryToConvert() и обрабатывая результат.
А еще проблема type-based в том, что обещанное вами переиспользование не работает. Нет общей библиотеки типов, где есть EmailAddress, поэтому две соседних библиотеки будут иметь две разных реализации, после чего нам понадобится писать конвертеры в две стороны.
Я специально для этого в статье упомянул про interoperability и конвертацию к простым типам (Integer, String).
Вы же не будете утверждать, что работая с типом String под видом Email'a в разных библиотеках и сторонних системах везде будут одни и те же проверки на IsEmail(). Тоже самое касается interoperability между разными платформами, например Java и .NET, конвертация к базовым типам безусловно необходима.
====
Я надеюсь, что многие читатели поняли идею и будут писать код с намного меньшими side-efect'ами.
И вам, и другим читателям приятного программирования!
Безусловно забывают про более конкретную типизацию для простых типов, достаточно просмотреть OpenSource проекты
А вы уверены, что это "забыли", а не "попробовали и оказалось слишком ресурсоемко"?
Здесь нет путаницы, просто сделайте один шажок дальше.
А зачем? В сильных (извините) системах типов Code Contracts просто не нужны, достаточно самой системы типов.
Я вас приятно удивлю прикрепленным скриншотом.
Не удивите. Я знаю, что в CC используется (может использоваться) статическая проверка, вот только она происходит не во время компиляции, а позже.
Для меня лично есть огромная и существенная разница между использованием набора атрибутов над классом String и типом EmailAddress.
Тем не менее, концептуально это ровно одно и то же. И писать проверочные атрибуты для каждого сабкласса немножко, знаете ли, сложно.
Если задуматься о чем статья и о чем пишет Хоар, то это не отдельно про subrange enum'ы, а про удобство использования более конкретных типов для конкретных нужд.
Потенциальное удобство, вот в чем дело. Накладные расходы это удобство вполне могут убивать.
Вы же не будете утверждать, что работая с типом String под видом Email'a в разных библиотеках и сторонних системах везде будут одни и те же проверки на IsEmail().
Не буду. Но если конкретная библиотека принимает от меня string
, мне не надо заниматься двойной конверсией.
Затем сделал то же с адресом просто vadim (имя сервера по умолчанию).
Date: Sun, 4 Sep 2016 14:41:56 +0300
To: vadim
Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\))
X-Mailer: Apple Mail (2.3124)
Чтение этой статьи — это как открыть незнакомую книгу в середине. Возникают те же вопросы: "Что?", "Для чего?", а главное "Зачем?".
От академических теорий до практики весьма далековато.
Речь не идёт о том, чтобы включать такой тип в стандартную библиотеку какого-то ЯП. Речь о том, что моделируя предметную область многие программисты ограничиваются высокоуровневыми концепциями (типа класс User), а на более низкий уровень забывают говоря, что email address — это просто String. Ну не просто String, ага, щас мы на формочке валидацию добавим, потом ещё на бэкенде проверим, что это не пустая строка. Хотя стойте, наверное хотя бы наличие @ стоит проверить? Ну ничего, сейчас проверим. Размажем по всему коду, где-то может быть забудем. Хотя нет, наверное лучше запилить EmailUtils с методом isValidEmail(String s). Код реюз — наше всё! В принципе работать будет, вот только важно не забыть этот isValidEmail() дёргать во всех местах, где возникает строка с email… Всех этих метаний и терзаний можно было бы избежать, если бы программист изначально ввёл тип EmailAddress и писал весь код с использования этого типа. Тогда возможности где-то что-то забыть просто не было бы.
В качестве примера — во многих ЯП есть стандартный тип URL, хотя, казалось бы, вроде тоже просто строка. Можно возразить, что URL "лучше" стандартизировано, чем адреса email, но нет. На практике как минимум максимальная поддерживаемая длина URL отличается для разных веб серверов: для Apache это 8192, для IIS — 2048.
На самом деле, даже с банальными числовыми поддиапазонами существует ряд практических проблем. Например, предположим, мы объявили переменную поддиапазонного типа
var i: 1..10;
а потом хотим сделать по ней цикл:
for i := 1 to 10 do something;
При обычной реализации этого цикла, переменная i получит на его выходе значение 11, выходящее за пределы её подтипа.
В своё время было много полемики по этому поводу в рамках академического флейма C vs Pascal.
Что касается конкретно практики isValidEmail, то я бы относился к ней с осторожностью, и использовал, скорее, как предупреждение, чем как безусловный императив, однозначно подразумеваемый статической системой типизации.
А то так и следующее перечисление с for(;;) не пройдет
enum Enum{
A = 1,
C = 3,
F = 20
}
На самом деле, даже цикл for… to MAXINT в большинстве реализаций никогда не завершится, даже без всяких поддиапазонов (в Си это тривиально следует из записи условия цикла, а во многих других языках является неприятным открытием для многих программистов). Это просто иллюстрирует фундаментальное противоречие, заключающееся в том, что алгоритмически целесообразный диапазон значений переменной может отличаться от домена прикладных значений, для обработки которых предназначена эта переменная.
Класс URL не для того произведён от String, чтобы обеспечить статическую валидацию, а просто затем, чтобы включить в него удобные методы.
Любой тип предназначен в первую очередь для того, чтобы отделить мух от котлет. Если даже тип является простой обёрткой с методами get/set вокруг String, он уже имеет ненулевой смысл, потому что делает часть вашей модели более явной давая ей имя.
Конкретно класс URL в Java начинается с валидации: new URL("привет")
— кинет MalformedURLException
.
При обычной реализации этого цикла, переменная i получит на его выходе значение 11, выходящее за пределы её подтипа.
Как верно заметил a-artur выше, такая задача на практике решается путём "перебора значений". Причём этот подход позволяет ещё и разрывы в диапазонах допустимых значений делать (типа "числа от 1 до 100 кроме числа 13").
Что касается конкретно практики isValidEmail, то я бы относился к ней с осторожностью, и использовал, скорее, как предупреждение, чем как безусловный императив, однозначно подразумеваемый статической системой типизации.
Основной профит от использования ЯП со статической типизацией — исключить некоторые категории ошибок программирования на этапе компиляции. В нашем случае речь про валидацию простых контрактов: можно забесплатно убедиться, что там, где вы ожидаете email, будет именно email, а не строка "привет". В языках с динамической типизацией на это нужно будет писать тест. Причём не просто "1 тест", а "+1 тест на каждый метод, в котором идёт работа с email адресом".
Единственный корректный способ отличить правильный e-mail от неправильного — это соединиться со своим smtp сервером и предложить ему соответствующее RCPT TO. Понятно, что это возможно только динамически. А все статические проверки e-mail адреса — это зло. Большинство авторов почтовых программ, к счастью, это хорошо понимают.
По поводу url — слово «привет», разумеется, не является url-ем, но, тем не менее, может иногда использоваться в качестве такового, в чём легко убедиться, набрав его в браузере.
Заблуждение здесь состоит в том, что компилятор может отличить корректное прикладное значение от некорректного.
У кого такое заблуждение?
Единственный корректный способ отличить правильный e-mail от неправильного — это соединиться со своим smtp сервером и предложить ему соответствующее RCPT TO.
Вы продолжаете решать не ту задачу. Мы говорим о корректности кода и о ловле некоторых ошибок программирования на этапе компиляции. Вот "практический" подход:
String registerUser(String username, String password, String email)
Ну да, вроде всё понятно. Маленький вопрос — что этот метод возвращает, ну это ладно. Нет никаких гарантий по поводу того, какие значения попадут в этот метод при вызове. Программист даже может немного лажануться и перепутать порядок username/password.
Теоретический подход:
class Username {
static Username fromString(String rawUsername) { ... }
String getRawUsername() { ... }
}
// аналогично реализуются
class Password { ... }
class EmailAddress { ... }
//
interface UserRegistrationResult {....}
class SuccessfulUserRegistrationResult : UserRegistrationResult {}
class UnsuccessfulUserRegistrationResult : UserRegistrationResult {}
UserRegistrationResult registerUser(Username username, Password password, EmailAddress email)
Попробуйте перепутать username и password — не получится. Попробуйте передать EmailAddress, содержащий пустое значение — не получится. У вас не то что передать это не получится, у вас в принципе не получится EmailAddress сконструировать (если в конструкторе EmailAddress есть такая проверка — а она там есть).
Возвращаясь к вашей практической части — если вы действительно хотите делать валидацию адресов через SMTP, можно оформить вот так:
interface EmailAddress { ... } // можно сохранять в базе
class UnconfirmedEmailAddress : EmailAddress { ... } // можно сохранять в базе, нельзя слать уведомления
class ConfirmedEmailAddress : EmailAddress { ... } // можно сохранять в базе, можно слать уведомления
// имея неподтверждённый email, можно его подтвердить
class EmailConfirmationService {
ConfirmedEmailAddress confirm(UnconfirmedEmailAddress email) { ... }
}
// "для моей программы" нет смысла подтверждать ранее подтверждённый email, поэтому даже код оформлен так, что это невозможно
// имея подтверждённый email, можно отправить юзеру уведомление
class NotificationService {
void notify(ConfirmedEmailAddress email) { ... }
}
// "для моей программы" есть смысл отправлять уведомления только на подтверждённые адреса
// обратите внимание - я обязан в явном виде привести email к типу Confirmed....
// я просто при всём желании не смогу в коде написать отправку уведомления,
// пока не получу этот самый ConfirmedEmailAddress
EmailAddress email = user.getEmailAddress()
if(email is ConfirmedEmailAddress) {
notificationService.notify((ConfirmedEmailAddress)email);
}
По поводу url — слово «привет», разумеется, не является url-ем, но, тем не менее, может иногда использоваться в качестве такового, в чём легко убедиться, набрав его в браузере.
Вы путаете URL и адресную строку броузера.
Вы путаете URL и адресную строку броузера.
Я ничего не путаю, я пишу о типичном случае применения.
Я писал только о том, что переменные ограниченных и перечислимых типов (не сами эти типы, а имеющие их переменные!) находят гораздо меньше применений на практике, чем этого хотелось бы на первый взгляд.
Эта ветка выросла из вашего заявления про тип EmailAddress:
Тип EmailAddress, предлагаемый в статье, не может существовать, так как адрес валидируется каждым сервером получателя по-своему.
Я всё пытаюсь понять вашу точку зрения по поводу применимости статической типизации — конкретно что не так с типом EmailAddress.
Я ничего не путаю, я пишу о типичном случае применения.
Опишите этот "типичный случай" для начала. Пока вы просто сделали односложное малопонятное утверждение, которое я читаю как "понятие URL само по себе не нужно, нужно понятие Фигня-Которую-Юзер-Написал-В-Адресной-Строке".
Я всё пытаюсь понять вашу точку зрения по поводу применимости статической типизации — конкретно что не так с типом EmailAddress.
Конкретно с типом EmailAddress не так то, что это по своему содержимому и есть String. И ничего более ограниченного, чем String, в нём нет.
А когда какая-нибудь программа начинает выдавать ошибку, потому что не находит в адресе собаки или точки, то это индусское программирование в чистом виде.
Опишите этот «типичный случай» для начала. Пока вы просто сделали односложное малопонятное утверждение, которое я читаю как «понятие URL само по себе не нужно, нужно понятие Фигня-Которую-Юзер-Написал-В-Адресной-Строке».
Понятие URL нужно. Не нужны (в большинстве случаев) переменные типа URL. Так как нет ни одного внешнего источника данных, за который мы могли бы поручиться, что оттуда всегда поступают именно URL.
Теоретический подход:
Обратите внимание, насколько это многословно.
interface UserRegistrationResult {....}
class SuccessfulUserRegistrationResult : UserRegistrationResult {}
class UnsuccessfulUserRegistrationResult : UserRegistrationResult {}
Вот здесь и становится видна вся "красота" прямолинейного подхода к type-based. Зачем эти два типа, какая с них польза?
Заметим в скобках, что если мы хотим выразить в типах потенциально неуспешное выполнение, то монада Try существенно полезнее.
fun registerUser userName password email: Try[userRegistration] = ...
fun sendConfirmation email confirmationLink: Try[unit] = ...
...
registerUser userName password email
|> Try.map (fun registration -> sendConfirmation email registration.confirmationLink)
Строго типизовано, пропустить ошибку нельзя (можно только явно поигнорить), ну и так далее. К сожалению, в C# это все разве что "будет когда-нибудь".
// обратите внимание - я обязан в явном виде привести email к типу Confirmed....
// я просто при всём желании не смогу в коде написать отправку уведомления,
// пока не получу этот самый ConfirmedEmailAddress
EmailAddress email = user.getEmailAddress()
if(email is ConfirmedEmailAddress) {
notificationService.notify((ConfirmedEmailAddress)email);
}
Угу. notificationService.notify((ConfirmedEmailAddress)email);
без предварительного is
, и все, ошибка в рантайме. Вообще, как только ваш дизайн использует приведение типов — можно считать, что type-based вы похоронили.
Собственно, чем это отличается от следующего?
if (email.IsConfirmed)
notificationService.notify(email);
Обратите внимание, насколько это многословно.
Полностью согласен и ни в коем случае не предлагаю писать такой код по умолчанию.
Собственно, чем это отличается от следующего?
Тем, что у вас ошибки не будет ни при компиляции, ни в рантайме. Не очень понимаю почему явное приведение хоронит статическую типизацию. Идея не в том, чтобы все конкретные типы выводить на этапе компиляции, а в том, чтобы программа сопротивлялась "неправильному развитию". Можно тот же самый код написать через визитор — будет такое же поведения без явных приведений типов. Влияет на что? По-моему нет. В других языках есть свои решения, чтобы избавиться от этих костылей.
Тем, что у вас ошибки не будет ни при компиляции, ни в рантайме
Ну так если не будет ошибки — и хорошо же?
Не очень понимаю почему явное приведение хоронит статическую типизацию
Не статическую типизацию, а type-driven design, это разные вещи.
Идея не в том, чтобы все конкретные типы выводить на этапе компиляции, а в том, чтобы программа сопротивлялась "неправильному развитию".
Когда это "сопротивление" преодолевается одним кастом — люди будут писать каст.
Ну так если не будет ошибки — и хорошо же?
Что же хорошего? Есть требование — отправлять уведомления только на подтверждённые email адреса. В вашем случае это требование выполняется только проверкой email.isConfirmed
. Если эту проверку забыть, требование будет нарушено, но код продолжит во-первых компилироваться, а во-вторых работать в рантайме. А забыть эту проверку очень легко, потому что она вообще никак не энфорсится. В моём же варианте нужно специально постараться, чтобы обойтись без проверки. Но даже в этом случае вы получите явную ошибку в рантайме.
Не статическую типизацию, а type-driven design, это разные вещи.
Можно пожалуйста вместо красивого "похоронили" написать в чём конкретно проблема?
Когда это "сопротивление" преодолевается одним кастом — люди будут писать каст.
В предыдущем сообщении обратил ваше внимание на то, что даже если обойти компайлтайм "защиту", остаётся ещё рантайм защита — каст заставит код скомпилироваться, но в рантайме оно иногда будет падать. Падать — значительно лучше, чем работать некорректно.
Можно пожалуйста вместо красивого "похоронили" написать в чём конкретно проблема?
В том, что идея type-driven в том, что вы просто не можете выразить на языке типов некорректное утверждение. Когда вы делаете каст, вы, по факту, создаете это самое некорректное утверждение насильно.
Падать — значительно лучше, чем работать некорректно.
Ну, мы же не знаем, что внутри notificationService, и будет ли оно в реальности работать некорректно или нет.
не можете выразить на языке типов некорректное утверждение
Я и не делал некорректное утверждение — там стоит явная проверка типа. Это вы предложили рассмотреть сценарий, когда в проект приходит полный отморозок и пишет хрень. Язык это позволяет — с этим я ничего поделать не могу. Но работать в итоге оно не будет — и это здорово. И к чему в итоге мы приходим?
Ну, мы же не знаем, что внутри notificationService, и будет ли оно в реальности работать некорректно или нет.
Вы понимаете, что notificationService.notify((ConfirmedEmailAddress)email)
падает именно на выражении (ConfirmedEmailAddress)email
— т.е. до вызова notify
?
Это вы предложили рассмотреть сценарий, когда в проект приходит полный отморозок и пишет хрень.
Да не "полный отморозок", а просто ленивый человек.
Язык это позволяет — с этим я ничего поделать не могу.
Вот об этом я и говорю: для того, чтобы адекватно работать в type-based design, нужна более мощная система типов, чем в C# (или хотя бы банальные запреты на касты). Справедливости ради, я замечу, что в ML-производных языках я нужного поведения тоже добиться не смог.
Вы понимаете, что notificationService.notify((ConfirmedEmailAddress)email) падает именно на выражении (ConfirmedEmailAddress)email — т.е. до вызова notify?
Понимаю. Я говорю о том, что мы, на самом деле, не знаем, как именно себя ведет notificationService, если ему передать неподтвержденный email, так что нарушение требований тут — пока — не очевидно.
Что интереснее, так это то, как этот подход двигать дальше. Предположим, вы реализовали требование "уведомления отправляются только на подтвержденные емейлы" с помощью типов. Таким образом, вы выразили какую-то часть состояния емейла в его типа. Прекрасно. Теперь у нас есть требование "уведомления отправляются только на подтвержденные емейлы только пользователей, живущих в Москве". Как это построить с помощью типа? (мы все понимаем, что Москва в данном случае — динамическое значение, на этапе компиляции программы неизвестное)
Это вы предложили рассмотреть сценарий, когда в проект приходит полный отморозок и пишет хрень.
Почитайте блог PVS-Studio — там есть много примеров того, какую хрень может написать специалист (в том числе вы сами) просто из-за усталости или из-за того что его отвлекли.
Падать — значительно лучше, чем работать некорректно.
Зависит от назначения программы. Если у вас прошивка маршрутизатора будет падать от некорректного, по её мнению, символа в контактном e-mail администратора в настройках, я думаю, вам вряд ли это понравится.
Как раз вопрос в проверке бизнес логики вашей системы.
Если Microsoft решил ограничить длину Email в 256 символов, то это их право, это их система.
Помимо указанного на хабре поста, есть множество информации о неправильных предположениях о номерах телефона, индекса, дома, и т. д. Но если вы работаете только с почтой РФ, наверное стоит подумать об неком ограничении.
Не бывает программ, которые только "хранят и отдают".
см. также антипаттерн PrimitiveObsession
А разве нельзя спроектировать язык (или компилятор) так, чтобы примитивные типы можно было использовать только для конструирования собственных (ну и задействовать их в скрытой автоматической проверке производных типов наряду заданными вручную, при подготовке нового типа, проверками) типов и нельзя было просто применить в программе — любой стандартный тип вне спецконструкции "Новый тип" — приводит к ошибке компиляции/интерпретации? А при подключении библиотек компилятору можно "сказать" — "Вот это — считай адресом электронной почты".
Можно. Кто будет этим пользоваться?
Примерный список тех кто будет этим пользоваться:
- Автор этой статьи (возможно)
- Читатели, одобрившие идеи из статьи (возможно)
- Академические исследователи (возможно)
- Специалисты, реализующие проекты повышенной ответственности (они НЕ захотят сесть на 50-70 лет в тюрьму из за проблем вызванных невнимательностью при работе с типами — если их сразу не пристрелят за такой косяк, конечно)
- Ole-Johan Dahl, Edsger W. Dijkstra, C.A.R. Hoare — эти тоже бы наверное пользовались, если бы не скончались.
Автор этой статьи (возможно)
Читатели, одобрившие идеи из статьи (возможно)
Академические исследователи (возможно)
Ключевое слово — "возможно". Вы задумывались о том, какие накладные расходы это за собой влечет?
Специалисты, реализующие проекты повышенной ответственности (они НЕ захотят сесть на 50-70 лет в тюрьму из за проблем вызванных невнимательностью при работе с типами)
А что, за невнимательность при работе с типами уже сажают? Я что-то думаю, что сажают за ошибки в ПО (да и то я не слышал ни разу, но это может быть просто избирательный слух), а для исправления ошибок в ПО нужна формальная верификация — ну вон там внизу F*, например, упомянули.
Понимаете, ничто не мешает создать "собственный тип" mystring = string
и использовать его везде. Ну как вы это запретите автоматически?
Никак не запретишь этот тип автоматически.
Сажают и стреляют не за ошибки а за последствия их и причинённый ими урон, убыток.
Возможно — это всего лишь "возможно" — мы же не о непреложных законах природы говорим.
Ну и по поводу накладных расходов — человек в ситуации "накладные расходы на типы" и "расходы на собственные похороны", обычно предпочитает расходы на типы.
Никак не запретишь этот тип автоматически.
Значит, запрет использования string
бессмысленен — он обходится в одну строчку кода. Надо не базовые типы запрещать, а делать очень легким и очень выгодным использование своих типов.
Ну и по поводу накладных расходов — человек в ситуации "накладные расходы на типы" и "расходы на собственные похороны", обычно предпочитает расходы на типы.
Это если вы ему такой выбор навяжете. Однако я что-то никогда в своей жизни такого выбора не видел (и не знаю никого, кто его видел бы). Зато вот выбор "нестрогое приложение, но сейчас" и "верифицируемое приложение, но через год" — видел. И, что занятно, люди выбирали первое.
"нестрогое приложение, но сейчас"
Только не тогда, когда это прошивка ядерной ракеты:)…
Вообще можно и неответственные приложения так писать как пишут ракетное ПО и выигрывать потом на сниженных расходах на поддержке и применении отлаженных библиотек, но это требует или нечеловеческого терпения или гигантского опыта и стойких привычек работать "правильно" — т. е. квалификации. Хорошо что программисты — не сапёры и не хирурги, где по другому работать невозможно — только "правильно".
Только не тогда, когда это прошивка ядерной ракеты
Это все, конечно, круто, но сколько людей пишет прошивки ядерных ракет?
Вообще можно и неответственные приложения так писать как пишут ракетное ПО и выигрывать потом на сниженных расходах на поддержке и применении отлаженных библиотек
А выигрывать ли? Есть конкретный расчет "мы потратим настолько больше ресурсов на разработке, зато потом выиграем вот столько на поддержке"?
Есть конкретный расчет "мы потратим настолько больше ресурсов на разработке, зато потом выиграем вот столько на поддержке"?
А нельзя ли вывести нужные показатели обработав статистически, скажем, базу бухгалтерии?
Скажем так, очень сложно, и я не знаю ни одного такого исследования.
Порой статистика крайне интересные и "злободневные" факты может "достать" из самых обыденных данных — посмотрите стр. 40 http://www.inp.nsk.su/%7Ebaldin/DataAnalysis/R/R-11-dep.pdf
Может и с бухгалтерскими данными удасться что нибудь придумать...
Вообще можно и неответственные приложения так писать как пишут ракетное ПОДа заказчик первый взвоет от принципов «Ракетного ПО».
Что, здесь надо кнопку добавить, а здесь подвинуть? Хорошо, меняем требования, начинаем весь цикл проектирования и согласования заново, а с потраченным за 3 года бюджетом, увы, придётся прощаться.
переиспользование типов
Мне кажется, это называют «повторным использованием».
В дополнение к этому существует огромное недопонимание между декларацией типа и его реальным значением. Например, в .NET Array принимает Integer как индекс в диапазоне [-2^31, 2^31], поэтому мы декларируем поддержку для чисел -1,-2,… как значений индекса. В это же время допустимы только неотрицательные числа, т.е. [0, 2^31]. Для честного и ясного кода должно быть обозначение Array[NonnegativeInteger].
.NET поддерживает отрицательные индексы в массивах.
В этом случае компилятор проверяет, что речь идет о размере и он не может быть отрицательным:
var array = new int[-1];
Но отрицательные индексы допустимы:
var array = Array.CreateInstance(typeof(int), new[] { 10 }, new[] { -5 });
for (int i = -5; i < 5; i++)
{
array.SetValue(i, i);
}
С точки зрения практики заметил, что даже зная про этот антипаттерн большинство программистов догадываются строить структуры или делать конкретную типизацию только для уж совсем наглядных сущностей, таких как Vector3D {int x, int y, int z}.
Ну вот кстати, интересный вопрос.
Есть у нас простенькая сущность: Point3D
, описывающая точку в трехмерном пространстве. У нее есть атрибуты x
, y
и z
. Каких они должны быть типов?
Я думаю тех типов, которые в контексте вашего приложения отвечают за измерения
Свой тип на каждый атрибут? Один тип на три атрибута?
Я думаю в типичном приложении это будет один и тот же тип, хотя моей фантазии хватит чтобы представить себе ситуацию, когда это разные типы.
Чем это отличается от сущности "пользователь", имеющей атрибуты "имя" и "фамилия"?
Что именно "это"?
Переформулирую.
Почему для атрибутов x
, y
и z
в сущности Point3D
можно использовать один и то же тип (причем ниже предлагают использовать просто базовый числовой тип нужной точности), а для атрибутов имя
и фамилия
сущности пользователь
(в посте) предлагается использовать отдельный — и, возможно, различный — тип?
Вероятно потому, что операция "сделай у этого вектора высоту как у того ширина" более осмысленна чем "сделай у этого пользователя такую же фамилию как у этого — имя". Но я сам так не делаю
Вероятно потому, что операция "сделай у этого вектора высоту как у того ширина" более осмысленна чем "сделай у этого пользователя такую же фамилию как у этого — имя".
Да легко. Пользователь при вводе перепутал поля для фамилии и имени, надо местами поменять.
Я до сих пор не встречал нигде такого функционала, где это бы автоматизировалось
У нас регулярно были такие запросы на массовое исправление.
(еще я видел системы, где это можно было поскриптовать внутри самой системы)
просто базовый числовой тип нужной точности
Вот это, кстати, не всегда. Истинные пуристы рекомендуют сделать какой-нибудь Length чтобы там были свойства типа AsMeters asInches и т.п.
… и вот тут тоже начинается отдельный вопрос. Координата — это длина? Нет, длина — это разность координат. Значит, у нас начинаются преобразования формата "при вычитании координат мы получаем отрезок, при сложении отрезка с координатами — новую координату" и так далее. И в принципе, это все очень круто, и уменьшает количество ошибок "я случайно запихнул длину вектора вместо его координаты". Но количество решений, которое надо принять и реализовать...
Приходит заказчик и говорит, что во всем приложении вводится единое правило для имени, будь то фамилия или имя. Не менее одного латинского символа и не более 256 латинских символов. В системе не должно быть никаких других символов, будь то кириллица, китайские иероглифы или иранские символы.
И такое часто в требованиях встречается.
Вместо глобального String вы создаете класс Name с требуемыми проверками.
И вместо размазни с атрибутами или проверками String на IsNameValid по всему приложению вы просто навсего используете этот класс. В частности для типа Person:
class Person{
Name Firstname {get;}
Name Lastnname {get;}
}
В другом месте на форме вы опять принимаете тип Name c теми же условиями, но только для Firstname.
И то же самое с EmailAddress, и другими маленьким типами и структурками.
Одно место для проверок, уверенность в валидности объекта в любое время и возможность повторного использования во всем приложении. Бинго!
Приходит заказчик и говорит, что во всем приложении вводится единое правило для имени, будь то фамилия или имя.
Для начала, заказчик говорит "на моей форме должно быть 50 символов". Не, я серьезно — уговорить заказчика на униформность по всей системе еще надо постараться. Так что с этого момента у вас в системе есть Finances.CustomerName
и CRM.Name
, и они отличаются.
Вместо глобального String вы создаете класс Name с требуемыми проверками. И вместо размазни с атрибутами или проверками String на IsNameValid по всему приложению вы просто навсего используете этот класс.
… и учите все инструменты с ним работать. Автоматический редактор поля в интерфейсе? Биндинги? Трансляция в БД?
А потом приходит заказчик и говорит: да, чуваки, все круто, но когда наша система принимает данные по обмену от другой системы, то их надо положить в БД как есть, и отобразить как есть, но при первом сохранении заставить оператора их отредактировать для соответствия. А вы не можете их ни положить, ни отобразить, потому что ваша сквозная система типов падает с рантайм-ошибками при попытке что получить эти данные в обмене, что донести их от БД до интерфейса.
Так что я еще могу понять использование именованных типов (на самом деле — банальных типов-синонимов) для того, чтобы разделить вещи, которые нельзя путать (вот, скажем, позицию и длину), но впихивать валидацию — которая рано или поздно окажется контекстно-зависимой — в систему типов мне кажется очень опасным решением. Именно из-за стоимости внесения изменений.
А потом приходит заказчик и говорит: да, чуваки, все круто, но когда наша система принимает данные по обмену от другой системы, то их надо положить в БД как есть, и отобразить как есть, но при первом сохранении заставить оператора их отредактировать для соответствия.
Мне кажется, это частный случай любого несоответствия типов. Например у вас была табличка с фамилией именем и отчеством а в другой системе "полковник Поликарпов Борис оглы Моисеевич старший".
Да, это он. Равно как и "мы храним этажность в виде текстового поля, чтобы можно было чердак написать". Но это реальные бизнес-сценарии, которые я наблюдал пять с чем-то лет, и которые успешно хоронили добрую часть попыток сделать не то что типизацию или code contracts, а хотя бы развернутые валидационные атрибуты.
Но ведь вы же не храните данные в одном большом неструктурированном блобе?
"Вы не поверите", для одного сценария мы почти так и сделали.
Но вообще приходится искать компромис между подвижностью под требования заказчика и верифицируемостью, и пока он ближе к заказчику, чем к верификации. Очевидно, что есть другие отрасли, в которых есть другие балансы, но я их не встречал.
В каких из пяти миров вы работали?
Нет ли внутри вашего мира областей, где можно ввести большую строгость?
Для начала, заказчик говорит «на моей форме должно быть 50 символов». Не, я серьезно — уговорить заказчика на униформность по всей системе еще надо постараться. Так что с этого момента у вас в системе есть Finances.CustomerName и CRM.Name, и они отличаются.
Вполне возможное и естественное требование заказчика, если это разные сущности и предназначены для разных систем. Да, разносите типы по разным интерфейсам.
Но, уж простите за утрированный пример, если заказчик говорит вам, что в одном месте максимум 50 символов для имени, а в другом 51, а третьем 52, и все это должно использоваться в итоге в одном месте, то это уже проблема общения с заказчиком.
… и учите все инструменты с ним работать. Автоматический редактор поля в интерфейсе? Биндинги? Трансляция в БД?
А потом приходит заказчик и говорит: да, чуваки, все круто, но когда наша система принимает данные по обмену от другой системы, то их надо положить в БД как есть, и отобразить как есть, но при первом сохранении заставить оператора их отредактировать для соответствия. А вы не можете их ни положить, ни отобразить, потому что ваша сквозная система типов падает с рантайм-ошибками при попытке что получить эти данные в обмене, что донести их от БД до интерфейса.
Давайте разделять две абсолютно разных операции: отображение данных и запись данных. Надо быть крайне «умным» человеком, чтобы выдергивать данные для отображения в виде сущностей с валидацией. Данные для чтения всегда просто возвращаются по их общему типу, в данном случае String.
Далее у вас новое требование от сторонней системы использовать данные как они есть, до исправления и пересохранения. Вот здесь вы нарушаете один из главных концептов стабильности системы. У вас в систему попадают потенциально некорректные данные. Вы разрешаете их где-то и когда-то исправить (возможно!) операторам.
Что если кто-нибудь прочитает эти некорректные данные до исправления и отправит в вашу систему неправильные данные, ну или в другие системы? Фонтан багов и некорректных данных в системе обеспечен. Иначе получается ужас какой-то, вы перед использованием данных каждый раз проверяете а не поправил ли их оператор, и так потенциально со всеми данными?
В общем случае картина часто выглядит следующим образом (на примере .NET, но актуально и для других):
/------------------------------------------
/ Внешние интерфейсы:
/ от пользователей на веб-морде до партнерских сервисов
/------------------------------------------
/ WebAPI
/ WCF
/ WS
/ WebHandler
/ External Library
/======================
||
||
||
/----------------------------------------
/ Core Logic
/----------------------------------------
/ Здесь лежат правила приложения
/ и требования заказчика.
/----------------------------------------
Из внешних систем приходят общие типы: String, Integer, etc.
В логике вашей системы или подсистемы вы работаете уже с конкретными типами.
Пока по вашим словам получается, что в вашей системы вы везде работаете с общим типом String, но где-то и как-то делаете валидацию IsName, CMS.IsLastname, IsEmailAddress, и отлично если вы не забыли эту проверку поставить в одной из многочисленных функций которые принимают множество общих типов String, ну или не забыли Unit test'ы накопипастить. Происходит посев потенциальных багов, которые на корню можно ликвидировать.
Расскажу про похожие случаи в двух крупных проектах в Австралии (Shell и Sanofi Pharmaceuticals).
Есть разные внешние системы, которые отправляют данные в систему компании. Поток сохраняют в очередь. Очередь обрабатывает автомат и конвертирует данные в систему компании. Если попадаются невалидные данные, то они отправляются на усмотрение оператора, который может их скорректировать, или вообще не впустить в систему (вы же не можете ручаться за все данные из внешних систем?).
Теперь об очень важном пункте в работе с заказчиком, который заказчики очень уважает. Как вы пришли к массовому невалидному потоку данных?
Вы отрабатывали импорт данных с заказчиком перед выпуском в продакшн и не учли какие-то факторы? 1) Если это ваша ошибка, то фиксите баг в ближайшую итерацию.
2) Если это ошибка заказчика
а) Заказчик с большими глазами прибежал и ему внезапно надо это в системе вчера — идет лесом, есть контракт работы. Ваша компания отвечает за программу, стабильность и потерю данных на данном этапе.
б) Заказчик понимает, что облажался, но фитча важная — делаете ее в высоком приоритете, также как и в 1 пункте.
Невалидные данные не теряются, они ждут в очереди вашего фикса или временной массовой обработки заказчика.
По поводу конвертации и валидации: не вижу в чем сложность написать один раз конвертер из String в EmailAddress, обратно даже ничего конвертировать не надо так как внутри String можно хранить. В случае веба сложно написать общий интерфейс для валидируемых объектов и один раз обработчик, который выкладывает ошибки в ModelState? Единственная проблема, это валидация на UI в JS, но это более общая проблема другого характера, которая тоже решаема.
Так что я еще могу понять использование именованных типов (на самом деле — банальных типов-синонимов) для того, чтобы разделить вещи, которые нельзя путать (вот, скажем, позицию и длину), но впихивать валидацию — которая рано или поздно окажется контекстно-зависимой — в систему типов мне кажется очень опасным решением. Именно из-за стоимости внесения изменений.
Иногда это все звучит так, что по не понятным мне причинам, в вашей системе все настолько универсальное, что даже Enum применить страшно (а вдруг программист большой Integer использует); валидации никакой нет и не будет (и с китайцами, и с японцами, и с арабами работаете), это просто хранилище данных в Unicode. Но тогда это к статье никакого отношения не имеет, да и вообще можно на ассемблере ваять.
это уже проблема общения с заказчиком.
К сожалению, проблемы общения с заказчиком — это жизнь, и она реальна.
Данные для чтения всегда просто возвращаются по их общему типу, в данном случае String.
То есть на каком-то этапе мы имеем дело как раз с невалидированными данными, не так ли?
В общем случае картина часто выглядит следующим образом [...] Из внешних систем приходят общие типы: String, Integer, etc. В логике вашей системы или подсистемы вы работаете уже с конкретными типами.
Вот смотрите, что получается. Из БД мы читаем "сырые" данные. Из всех внешних интерфейсов нам тоже приходят сырые данные. В UI-слое у нас тоже сырые данные, но мы уже должны их валидировать — то есть часть логики валидации применяется к сырым данным. Получается, что бизнес-типы живут на весьма ограниченном участке приложения (и, неизбежно, частично продублированы в UI и БД). А для всех ли приложений это оправдано вообще?
Далее у вас новое требование от сторонней системы использовать данные как они есть, до исправления и пересохранения. Вот здесь вы нарушаете один из главных концептов стабильности системы. У вас в систему попадают потенциально некорректные данные.
Нет, не нарушаю. Это требование заказчика, и он определяет приоритеты. Если потенциально некорректные данные ему ценнее отсутствующих — это его право.
Иначе получается ужас какой-то, вы перед использованием данных каждый раз проверяете а не поправил ли их оператор, и так потенциально со всеми данными?
Не "поправил ли их оператор", а "прошли ли данные верификацию". Для учетных систем нормальная процедура.
Теперь об очень важном пункте в работе с заказчиком, который заказчики очень уважает. Как вы пришли к массовому невалидному потоку данных?
Их поставляют контрагенты.
По поводу конвертации и валидации: не вижу в чем сложность написать один раз конвертер из String в EmailAddress, обратно даже ничего конвертировать не надо так как внутри String можно хранить. В случае веба сложно написать общий интерфейс для валидируемых объектов и один раз обработчик, который выкладывает ошибки в ModelState?
Ничего не "сложно" написать, но все эти "написать" — это ресурсы, которых ограниченное количество.
Может быть SmallInt, Integer, Float, Decimal, или даже Fraction как в smalltalk.
Опережая другой вопрос, мне известны возможные проблемы из-за разницы в хранении и представлении чисел.
Ну то есть все атрибуты одного типа?
Чтобы в двух местах одно и то же не обсуждать: https://habrahabr.ru/post/309142/#comment_9789080
для уж совсем наглядных сущностей, таких как Vector3D {int x, int y, int z}.
Кстати, с точки зрения производительности чаще всего выгоднее не делать таких вот наглядных сущностей. Хранить отдельные массивы для компонент векторов с точки зрения эффективности использования памяти может быть гораздо выгоднее, чем хранить массив векторов.
Вот видео, на котором разрабочкик компилятора из MS рассказывает, как ускорил N-body simulation в два раза, просто копируя данные в отдельные массивы на каждой итерации:
https://channel9.msdn.com/Events/Build/2013/4-329
Ну и в гейм-деве паттерн такой есть:
http://gameprogrammingpatterns.com/data-locality.html
Частные неструктурированные типы и повторное использование типов