Должны ли мы определять типы для всего?

Недавно у меня возникла проблема с читабельностью моего кода.
У меня была функция, которая выполняла операцию, и вернула строку, представляющую идентификатор этой операции для будущей ссылки (немного похожая на OpenFile в Windows, возвращающую дескриптор). Пользователь будет использовать этот идентификатор позже, чтобы начать операцию и следить за ее завершением.

Идентификатор должен был быть случайной строкой из-за проблем с совместимостью. Это создало метод, который имел очень нечеткую сигнатуру следующим образом:

 public string CreateNewThing()

Это делает цель типа возврата неясной. Я думал, что обернуть эту строку другим типом, который делает его смысл более понятным, например:

 public OperationIdentifier CreateNewThing()

Тип будет содержать только строку и будет использоваться всякий раз, когда используется эта строка.

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

Считаете ли вы, что это хорошая практика для переноса простых типов в классе по соображениям безопасности?

137 голосов | спросил Ziv 3 Maypm15 2015, 19:30:09

10 ответов


151

Примитивы, такие как string или int, не имеют смысла в бизнес-домене. Прямым следствием этого является то, что вы можете ошибочно использовать URL-адрес при ожидании идентификатора продукта или использовать количество при ожидании цены .

Это также почему вызов Object Calisthenics включает примитивы в качестве одного из своих правил:

  

Правило 3: Оберните все примитивы и строки

     

В языке Java int является примитивным, а не реальным объектом, поэтому он подчиняется различным правилам, чем объекты. Он используется с синтаксисом, который не является объектно-ориентированным. Что еще более важно, int сам по себе является просто скаляром, поэтому он не имеет никакого значения. Когда метод принимает int как параметр, имя метода должно выполнять всю работу по выражению намерения. Если один и тот же метод принимает Час в качестве параметра, гораздо проще увидеть, что происходит.

В том же документе объясняется, что есть дополнительное преимущество:

  

Маленькие объекты, такие как «Час» или «Деньги», также дают нам очевидное место, чтобы повести поведение, которое иначе было бы усеяно вокруг других классов .

В самом деле, когда используются примитивы, обычно очень сложно отслеживать точное местоположение кода, связанного с этими типами, что часто приводит к серьезному дублированию кода . Если есть класс Price: Money, естественно, что внутри него будет проверяться диапазон. Если вместо этого используется int (хуже, double)), чтобы сохранить цены на товар, кто должен проверить диапазон? Продукт? Скидка? Тележка?

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

Недостаток - и тот же аргумент применим к каждому правилу упражнения Object Calisthenics - это то, что если он быстро становится слишком подавляющим, чтобы создать класс для всего . Если Product содержит ProductPrice, который наследует от PositivePrice, который наследует от Price, который, в свою очередь, наследует от Money, это не чистая архитектура, а полная беспорядок, где для того, чтобы найти одну вещь, сопровождающий должен открывать несколько десятков файлов каждый раз.

Еще один момент для рассмотрения - стоимость (в терминах строк кода) создания дополнительных классов. Если обертки неизменны (как и должно быть, обычно), это означает, что если мы возьмем C #, вы должны иметь, по крайней мере, в обертке:

  • Свойство getter,
  • Это поле поддержки,
  • Конструктор, который присваивает значение поля поддержки,
  • Пользовательский ToString(),
  • Комментарии к документации XML (что делает много строк),
  • A Equals и GetHashCode переопределяет (также много LOC).

и, в конце концов, при необходимости:

  • A атрибут отладчика ,
  • Переопределение операторов == и !=,
  • В конечном итоге перегрузка оператора неявного преобразования для беспрепятственного преобразования в и из инкапсулированного типа,
  • Кодовые контракты (включая инвариант, который является довольно длинным методом, с тремя его атрибутами),
  • Несколько конвертеров, которые будут использоваться во время сериализации XML, сериализации JSON или хранения /загрузки значения в /из базы данных.

Сто LOC для простой обертки делает ее довольно запретительной, и именно поэтому вы можете быть полностью уверены в долгосрочной рентабельности такой обертки. Понятиеscope , объясненный Thomas Junk , здесь особенно уместен. Написание сто LOC для представления ProductId, используемого по всей вашей кодовой базе, выглядит весьма полезным. Написание класса такого размера для фрагмента кода, который делает три строки внутри одного метода, гораздо более сомнительным.

Вывод:

  • Удерживайте примитивы в классах, которые имеют значение в бизнес-домене приложения, когда (1) он помогает уменьшить ошибки, (2) уменьшает риск дублирования кода или (3) помогает изменить базовый тип позже .

  • Не обматывайте автоматически каждый примитив, который вы найдете в своем коде: есть много случаев, когда использование string или int отлично.

На практике в public string CreateNewThing() может помочь экземпляр класса ThingId вместо string, но вы также можете

  • Возвращает экземпляр класса Id<string>, который является объектом общего типа, указывающим, что базовый тип является строкой. У вас есть преимущество чтения, без недостатка в необходимости поддерживать множество типов.

  • Возвращает экземпляр класса Thing. Если пользователю нужен только идентификатор, это легко сделать с помощью:

    var thing = this.CreateNewThing();
    var id = thing.Id;
    
ответил Arseni Mourzenko 3 Maypm15 2015, 20:09:07
60

Я бы использовал область scope как правило: чем меньше область генерации и потребления таких значений values, тем меньше вероятность создания объекта, представляющего это значение.

Скажем, у вас есть следующий псевдокод

id = generateProcessId();
doFancyOtherstuff();
job.do(id);

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

ответил Thomas Junk 3 Maypm15 2015, 20:15:39
34

Системы статического типа предназначены для предотвращения неправильного использования данных.

Есть очевидные примеры типов, которые делают это:

  • вы не можете получить месяц UUID
  • вы не можете умножить две строки.

Есть более тонкие примеры

  • вы не можете оплатить что-то, используя длину стола.
  • вы не можете сделать HTTP-запрос, используя чье-то имя в качестве URL-адреса.

У нас может возникнуть соблазн использовать double для цены и длины или использовать string для имени и URL-адреса. Но выполнение этого подрывает нашу удивительную систему типов и позволяет этим злоупотреблениям передавать статические проверки языка.

Получение фунта-секунд, спутанное с Newton-seconds, может плохие результаты во время выполнения .


Это особенно проблема со строками. Они часто становятся «универсальным типом данных».

Мы используем прежде всего текстовые интерфейсы с компьютерами, и мы часто расширяем эти интерфейсы человеческих интерфейсов (UI) до интерфейсов программирования (API). Мы считаем 34.25 символами 34.25 . Мы думаем о дате в качестве символов 05-03-2015 . Мы рассматриваем UUID как символы 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .

Но эта ментальная модель вредит абстракции API.

  

Слова или язык, поскольку они написаны или произнесены, не играют никакой роли в моем механизме мышления.

     

Альберт Эйнштейн

Аналогично, текстовые представления не должны играть никакой роли в разработке типов и API. Остерегайтесь строки string! (и другие чрезмерно общие «примитивные» типы)


Типы сообщают «какие операции имеют смысл».

Например, я однажды работал над клиентом API HTTP REST. REST, правильно сделанные, используют объекты гипермедиа, у которых есть гиперссылки, указывающие на связанные объекты. В этом клиенте не только были введены сущности (например, пользователь, учетная запись, подписка), также были введены ссылки на эти объекты (UserLink, AccountLink, SubscriptionLink). Ссылки были немного больше, чем обертки вокруг Uri, но отдельные типы не позволяли использовать AccountLink для извлечения пользователя. Если бы все было простым Uri s - или даже хуже, string - эти ошибки можно было найти только во время выполнения.

Аналогично, в вашей ситуации у вас есть данные, которые используются только для одной цели: для идентификации Operation. Его нельзя использовать ни для чего другого, и мы не должны пытаться идентифицировать Operation s со случайными строками, которые мы составили. Создание отдельного класса добавляет читаемость и безопасность в ваш код.


Конечно, все хорошие вещи можно использовать в избытке. Рассмотрим

  1. насколько ясность добавляет ваш код

  2. , как часто он используется

Если «тип» данных (в абстрактном смысле) используется часто, для разных целей и между интерфейсами кода, он является очень хорошим кандидатом для отдельного класса за счет многословия.

ответил Paul Draper 4 Mayam15 2015, 02:31:46
10

Я согласен с тем, что много раз вы должны создавать тип для примитивов и строк, но поскольку приведенные выше ответы рекомендуют создать тип в большинстве случаев, я приведу несколько причин, почему /когда не нужно:

  • Производительность. Здесь я должен ссылаться на реальные языки. В C #, если идентификатор клиента является коротким, и вы переносите его в класс - вы только что создали много накладных расходов: память, так как теперь она составляет 8 байтов на 64-битной системе и скорость с тех пор, пока она выделена в куче.
  • Когда вы делаете предположения относительно типа. Если идентификатор клиента является коротким, и у вас есть некоторая логика, которая его каким-то образом упаковывает - вы обычно уже делаете предположения относительно типа. Во всех этих местах вам нужно сломать абстракцию. Если это одно место, это неважно, если это повсюду, вы можете обнаружить, что в половине случаев вы используете примитив.
  • Потому что не каждый язык имеет typedef. А для языков, которые этого не делают, и кода, который уже написан, такое изменение может быть большой задачей, которая может даже привести к ошибкам (но у каждого есть набор тестов с хорошим охватом, верно?).
  • В некоторых случаях он уменьшает читаемость . Как это напечатать? Как это подтвердить? Должен ли я проверять значение null? Все вопросы, которые требуют, чтобы вы сверлили определение типа.
ответил ytoledano 4 Maypm15 2015, 17:41:01
9
  

Считаете ли вы, что это хорошая практика для переноса простых типов в классе по соображениям безопасности?

Иногда.

Это один из тех случаев, когда вам нужно взвесить проблемы, которые могут возникнуть в результате использования string вместо более конкретного OperationIdentifier. В чем их тяжесть? Какова их вероятность?

Затем вам нужно учитывать стоимость использования другого типа. Насколько болезненно это использовать? Сколько работы это сделать?

В некоторых случаях вы сэкономите время и усилия, имея хороший конкретный тип для работы. В других это не будет проблемой.

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

ответил Telastyn 3 Maypm15 2015, 19:41:41
7

Нет, вы не должны определять типы (классы) для «всего».

Но, как утверждают другие ответы, это полезно часто . Вы должны развивать «сознательно, если это возможно» чувство или чувство слишком большого трения из-за отсутствия отсутствия подходящего типа или класса, когда вы пишете, проверяете и поддерживаете код. Для меня начало слишком большого трения - это когда я хочу консолидировать несколько примитивных значений как одно значение или когда мне нужно проверить значения (т. Е. Определить, какое из всех возможных значений примитивного типа соответствует допустимым значениям «подразумеваемый тип»).

Я нашел такие соображения, как то, что вы поставили в своем вопросе, чтобы отвечать за дизайн too much в моем коде. У меня сложилась привычка сознательно избегать написания кода больше, чем нужно. Надеюсь, вы пишете хорошие (автоматизированные) тесты для своего кода - если да, то вы можете легко реорганизовать свой код и добавить типы или классы , если это дает чистые преимущества для текущей разработки и обслуживания вашего кода .

ответ Telastyn и ответ Томаса Джанка оба дают очень хорошее представление о контексте и использовании соответствующего кода. Если вы используете значение внутри одного блока кода (например, метод, цикл, using), то просто использовать примитивный тип. Даже очень просто использовать примитивный тип, если вы используете множество значений повторно и во многих других местах. Но чем чаще и чаще вы используете набор значений, и чем меньше этот набор значений соответствует значениям, представленным примитивным типом, тем больше вы должны учитывать инкапсуляцию значений в классе или типе.

ответил Kenny Evitt 4 Mayam15 2015, 00:09:13
5

На первый взгляд все, что вам нужно сделать, это идентифицировать операцию.

Кроме того, вы говорите, что должна делать операция:

  

до запустите операцию [и], чтобы отслеживать ее завершение

Вы говорите это так, как будто это «как используется идентификация», но я бы сказал, что это свойства, описывающие операцию. Это звучит как определение типа для меня, есть даже шаблон, называемый «шаблон команды», который подходит очень хорошо. .

В нем говорится

  

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

Я думаю, что это очень похоже на то, что вы хотите делать с вашими операциями. (Сравните фразы, которые я сделал полужирным в обоих кавычках) Вместо того, чтобы возвращать строку, верните идентификатор для Operation в абстрактном смысле, указатель на объект этого класса в oop, например.

Относительно комментария

  

Было бы wtf-y называть этот класс командой, а затем не иметь в себе логики.

Нет, не будет. Помните, что шаблоны очень абстрактны , поэтому абстрактны на самом деле, что они несколько мета. То есть, они часто абстрагируют само программирование, а не какую-то концепцию реального мира. Шаблон команды представляет собой абстракцию вызова функции. (или метод) Как будто кто-то нажал кнопку паузы сразу после передачи значений параметров и прямо перед исполнением, чтобы возобновиться позже.

Далее рассматривается oop, но мотивация, стоящая за ней, должна быть верна для любой парадигмы. Мне нравится приводить некоторые причины, по которым размещение логики в команде можно считать плохой.

  1. Если бы у вас была вся эта логика в команде, она была бы раздутой и беспорядочной. Вы было бы «хорошо, вся эта функциональность должна быть в отдельные классы. «Вы бы реорганизовали логику из команда.
  2. Если бы у вас была вся эта логика в команде, было бы трудно проверить и встать на путь при тестировании. «Святое дерьмо, почему я должен использовать эта командная чепуха? Я хочу проверить только, если эта функция из 1 или нет, я не хочу называть его позже, я хочу проверить его сейчас! "
  3. Если бы у вас была вся эта логика в команде, это не сделало бы слишком много смысл сообщить, когда закончите. Если вы думаете о функции выполняться синхронно по умолчанию, нет смысла уведомляется о завершении выполнения. Возможно, вы начинаете нить, потому что ваша операция на самом деле представляет аватар для кино формат (может потребоваться дополнительная нить на малине pi), возможно, у вас есть для получения некоторых данных с сервера, ... что бы это ни было, если есть причина для сообщения о завершении, может быть, потому что есть некоторая асинхронность (это слово?) продолжается. Я думаю, что нить или контакт с сервером - это логика, которая не должна быть в вашем команда. Это несколько мета-аргумент и, возможно, спорный. Если вы считаете, что это не имеет смысла, сообщите мне в комментариях.

Напомним: шаблон команды позволяет вам обернуть функциональность  в объект, который будет выполнен позже. Для модульности (функциональность существует независимо от того, выполняется ли она командой или нет), тестируемость (функциональность должна быть проверена без команды) и все эти другие слова, которые существенно выражают требование написать хороший код, вы не ставили бы фактическую логику в команду.


Поскольку шаблоны абстрактны, может быть трудно найти хорошие метафоры реального мира. Вот попытка:

«Эй, бабушка, не могли бы вы нажать кнопку записи на телевизоре в 12, чтобы я не пропустил симпсонов на канале 1?»

Моя бабушка не знает, что происходит технически, когда она нажимает на эту кнопку записи.Логика в другом месте (в телевизоре). И это хорошо. Функциональность инкапсулирована, и информация скрыта от команды, она является пользователем API, не обязательно является частью логики ... oh jeez Я снова болтаю слова гудения, теперь я лучше закончу это редактирование.

ответил null 4 Maypm15 2015, 19:37:10
5

Идея для примитивных типов обертывания,

  • Чтобы установить язык, специфичный для домена
  • Предельные ошибки, совершаемые пользователями путем передачи неверных значений

Очевидно, что это было бы очень сложно и не практично делать это повсюду, но важно обернуть нужные типы,

Например, если у вас есть класс Order,

 Order
{
   long OrderId { get; set; }
   string InvoiceNumber { get; set; }
   string Description { get; set; }
   double Amount { get; set; }
   string CurrencyCode { get; set; }
}

Важными свойствами для поиска ордера являются в основном OrderId и InvoiceNumber. И сумма и CurrencyCode тесно связаны между собой, если если кто-то изменит CurrencyCode без изменения суммы, то Заказ больше не может считаться действительным.

Таким образом, в этом сценарии имеет смысл обернуть только OrderId, InvoiceNumber и ввести составной для валюты, и, вероятно, обертывание описания не имеет смысла. Таким образом, предпочтительный результат может выглядеть следующим образом:

     Order
    {
       OrderIdType OrderId { get; set; }
       InvoiceNumberType InvoiceNumber { get; set; }
       string Description { get; set; }
       Currency Amount { get; set; }
    }

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

ответил Low Flying Pelican 4 Mayam15 2015, 11:02:55
4

Непопулярное мнение:

Обычно вы не должны определять новый тип!

Определение нового типа для переноса примитива или базового класса иногда называется определением псевдотипа. IBM описывает это как плохую практику здесь (они сосредоточены конкретно на неправильном использовании с дженериками в этом случае).

Псевдо-типы не позволяют использовать обычную библиотечную функцию.

Математические функции Java могут работать со всеми примитивами числа. Но если вы определяете новый класс Percentage (обертывание двойника, который может находиться в диапазоне 0 ~ 1), все эти функции бесполезны и должны быть обернуты классами, которые (что еще хуже) должны знать о внутреннем представлении класса Percentage .

Псевдо-типы идут вирусными

При создании нескольких библиотек вы часто обнаружите, что эти псевдотипы становятся вирусными. Если вы используете вышеперечисленный класс Percentage в одной библиотеке, вам нужно будет либо преобразовать его на границе библиотеки (потеряв все безопасность /смысл /логику /другую причину, которую вы создали для создания этого типа), либо вам придется делать эти классы доступный и для другой библиотеки. Заражение новой библиотеки вашими типами, где может быть достаточно простого двойника.

Отнять сообщение

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

Пример:

A uint может отлично представлять UserId, мы можем использовать встроенные операторы Java для uint s (например, ==), и нам не нужна бизнес-логика для защитить «внутреннее состояние» идентификатора пользователя.

ответил Roy T. 6 Mayam15 2015, 11:29:53
3

Как и во всех советах, знание того, когда применять правила, - это умение. Если вы создаете свои собственные типы на языке, управляемом типом, вы получаете проверку типов. Поэтому, как правило, это хороший план.

NamingConvention - ключ к читабельности. Обе эти комбинации могут четко передать намерение.
Но ///все еще полезно.

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

ответил phil soady 3 Maypm15 2015, 19:44:17

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132