Недавно у меня возникла проблема с читабельностью моего кода.
У меня была функция, которая выполняла операцию и возвращала строку, представляющую идентификатор этой операции, для дальнейшего использования (немного похоже на OpenFile в Windows, возвращающий дескриптор). Пользователь будет использовать этот идентификатор позже, чтобы начать операцию и контролировать ее завершение.
Идентификатор должен был быть случайной строкой из-за проблем совместимости. Это создало метод с очень непонятной сигнатурой, например:
public string CreateNewThing()
Это делает цель возвращаемого типа неясной. Я думал обернуть эту строку в другой тип, который делает ее значение более понятным, например, так:
public OperationIdentifier CreateNewThing()
Тип будет содержать только строку и использоваться всякий раз, когда эта строка используется.
Очевидно, что преимущество этого способа заключается в большей безопасности типов и более четких намерениях, но он также создает намного больше кода и кода, который не очень идиоматичен. С одной стороны, мне нравится дополнительная безопасность, но она также создает много беспорядка.
Считаете ли вы хорошей практикой упаковывать простые типы в классе по соображениям безопасности?
источник
Ответы:
Примитивы, такие как
string
илиint
, не имеют значения в бизнес-сфере. Прямым следствием этого является то, что вы можете по ошибке использовать URL, когда ожидается идентификатор продукта, или использовать количество, когда ожидаете цену .Вот почему вызов Object Calisthenics включает в себя обертывание примитивов как одно из его правил:
В том же документе объясняется, что есть дополнительное преимущество:
Действительно, когда используются примитивы, обычно чрезвычайно трудно отследить точное местоположение кода, связанного с этими типами, что часто приводит к серьезному дублированию кода . Если есть
Price: Money
класс, естественно найти диапазон проверки внутри. Если вместо этого для хранения цен на товары используетсяint
(хуже, аdouble
), кто должен проверять диапазон? Продукт? Скидка? Корзина?Наконец, третье преимущество, не упомянутое в документе, - это возможность относительно легко изменить базовый тип. Если сегодня my
ProductId
имеет вshort
качестве базового типа, а позже мне нужно использоватьint
вместо этого, скорее всего, код для изменения не будет охватывать всю базу кода.Недостаток - и тот же аргумент применяется к каждому правилу упражнения Object Calisthenics - состоит в том, что если быстро становится слишком подавляющим, чтобы создать класс для всего . Если
Product
содержит то,ProductPrice
что наследует, отPositivePrice
которого наследует, отPrice
которого, в свою очередь, наследуетсяMoney
, это не чистая архитектура, а скорее полный беспорядок, где для того, чтобы найти одну вещь, сопровождающий должен каждый раз открывать несколько десятков файлов.Другим важным моментом является стоимость (с точки зрения строк кода) создания дополнительных классов. Если обертки являются неизменяемыми (как обычно и должно быть), это означает, что, если мы берем C #, вы должны иметь в обёртке как минимум:
ToString()
,Equals
иGetHashCode
переопределения (также много LOC).и в конце концов, когда это уместно:
==
и!=
операторы,Сто LOC для простой обертки делает его довольно запретным, и поэтому вы можете быть полностью уверены в долгосрочной прибыльности такой обертки. Понятие объема, объясненное Томасом Джанком , особенно актуально здесь. Написание сотен LOC для представления
ProductId
всей используемой базы кода выглядит весьма полезным. Написание класса такого размера для фрагмента кода, который составляет три строки в одном методе, является гораздо более сомнительным.Заключение:
Оборачивайте примитивы в классы, которые имеют значение в бизнес-области приложения, когда (1) это помогает уменьшить количество ошибок, (2) снижает риск дублирования кода или (3) помогает изменить базовый тип позже.
Не оборачивайте автоматически все примитивы, которые вы найдете в своем коде: во многих случаях используется
string
илиint
отлично работает.На практике
public string CreateNewThing()
возвращение экземпляраThingId
класса вместо вместоstring
может помочь, но вы также можете:Вернуть экземпляр
Id<string>
класса, который является объектом универсального типа, указывая, что базовый тип является строкой. Вы получаете удобство чтения, без недостатка необходимости поддерживать много типов.Вернуть экземпляр
Thing
класса. Если пользователю нужен только идентификатор, это легко сделать с помощью:источник
Я бы использовал область действия в качестве практического правила:
values
чем меньше область создания и потребления , тем меньше вероятность создания объекта, представляющего это значение.Скажем, у вас есть следующий псевдокод
тогда область действия очень ограничена, и я не вижу смысла делать ее типом. Но, скажем, вы генерируете это значение в одном слое и передаете его другому слою (или даже другому объекту), тогда имеет смысл создать тип для этого.
источник
doFancyOtherstuff()
в подпрограмму, вы можете подумать, чтоjob.do(id)
ссылка недостаточно локальна, чтобы сохранить ее в виде простой строки.Системы статического типа предназначены для предотвращения неправильного использования данных.
Есть очевидные примеры типов, делающих это:
Есть более тонкие примеры
У нас может возникнуть соблазн использовать
double
как для цены, так и для длины, или использовать какstring
для имени, так и для URL. Но это подрывает нашу удивительную систему типов и позволяет этим злоупотреблениям проходить статические проверки языка.Смешение фунт-секунд с ньютон-секундами может привести к плохим результатам во время выполнения .
Это особенно проблема со строками. Они часто становятся «универсальным типом данных».
Мы привыкли в первую очередь текстовые интерфейсы с компьютерами, и мы часто расширяем эти человеческие интерфейсы (UI) для интерфейсов программирования (API). Мы думаем о 34,25 как символы 34,25 . Мы думаем о дате как символы 05-03-2015 . Мы рассматриваем UUID как символы 75e945ee-f1e9-11e4-b9b2-1697f925ec7b .
Но эта ментальная модель вредит абстракции API.
Точно так же текстовые представления не должны играть никакой роли в разработке типов и API. Осторожно
string
! (и другие чрезмерно общие "примитивные" типы)Типы сообщают «какие операции имеют смысл».
Например, однажды я работал на клиенте с HTTP REST API. REST, правильно выполненный, использует гипермедиа сущности, которые имеют гиперссылки, указывающие на связанные сущности. В этом клиенте были введены не только сущности (например, Пользователь, Учетная запись, Подписка), но и ссылки на эти сущности (UserLink, AccountLink, SubscriptionLink). Ссылки были всего лишь обертками
Uri
, но отдельные типы делали невозможным использование AccountLink для получения пользователя. Если бы все было простоUri
- или даже хуже,string
- эти ошибки были бы обнаружены только во время выполнения.Кроме того, в вашей ситуации у вас есть данные, которые используются только для одной цели: для идентификации
Operation
. Это не должно использоваться ни для чего другого, и мы не должны пытаться идентифицироватьOperation
s со случайными строками, которые мы составили. Создание отдельного класса повышает удобочитаемость и безопасность вашего кода.Конечно, все хорошее можно использовать в избытке. Рассмотреть возможность
насколько ясность это добавляет к вашему коду
как часто это используется
Если «тип» данных (в абстрактном смысле) часто используется для различных целей и между интерфейсами кода, это очень хороший кандидат на то, чтобы быть отдельным классом за счет многословия.
источник
05-03-2015
означает, когда интерпретируется как дата ?Иногда.
Это один из тех случаев, когда вам нужно взвесить проблемы, которые могут возникнуть при использовании
string
более конкретногоOperationIdentifier
. Какова их серьезность? Какова их вероятность?Тогда вам нужно учитывать стоимость использования другого типа. Насколько больно использовать? Сколько работы это сделать?
В некоторых случаях вы сэкономите время и силы, имея хороший конкретный тип для работы. В других это не будет стоить проблем.
В общем, я думаю, что такого рода вещи нужно делать больше, чем сегодня. Если у вас есть объект, который что-то значит в вашем домене, хорошо иметь его в качестве своего собственного типа, так как этот объект с большей вероятностью изменится / будет расти вместе с бизнесом.
источник
Я, как правило, согласен с тем, что много раз вам следует создавать тип для примитивов и строк, но поскольку приведенные выше ответы рекомендуют создавать тип в большинстве случаев, я перечислю несколько причин, почему / когда нет:
источник
short
(например) имеет одинаковую стоимость, упакованную или развернутую. (?)Нет, вы не должны определять типы (классы) для «всего».
Но, как утверждают другие ответы, это часто бывает полезно. Вы должны развивать - сознательно, если возможно - чувство или чувство слишком большого трения из-за отсутствия подходящего типа или класса, когда вы пишете, тестируете и поддерживаете свой код. Для меня начало слишком большого трения - это когда я хочу объединить несколько значений примитивов в одно значение или когда мне нужно проверить значения (т. Е. Определить, какие из всех возможных значений типа примитива соответствуют действительным значениям «подразумеваемый тип»).
Я обнаружил, что такие соображения, как то, что вы задали в своем вопросе, были ответственны за слишком много дизайна в моем коде. Я выработал привычку сознательно избегать написания кода, больше необходимого. Надеемся, что вы пишете хорошие (автоматизированные) тесты для своего кода - если да, то вы можете легко реорганизовать свой код и добавить типы или классы, если это дает чистые преимущества для постоянной разработки и обслуживания вашего кода .
Ответ Telastyn в и ответ Томаса Нежелательный в оба делают очень хорошую точку о контексте и использовании соответствующего кода. Если вы используете значение в одном блоке кода (например, метод, цикл,
using
блок), тогда можно просто использовать примитивный тип. Можно даже использовать примитивный тип, если вы используете набор значений неоднократно и во многих других местах. Но чем чаще и шире вы используете набор значений, и чем менее близко этот набор значений соответствует значениям, представленным примитивным типом, тем больше вы должны рассмотреть инкапсуляцию значений в классе или типе.источник
На первый взгляд все, что вам нужно сделать, это определить операцию.
Дополнительно вы говорите, что должна делать операция:
Вы говорите это так, как будто это «просто как используется идентификация», но я бы сказал, что это свойства, описывающие операцию. Для меня это звучит как определение типа, есть даже шаблон, называемый «шаблон команды», который очень хорошо подходит. ,
Это говорит
Я думаю, что это очень похоже на то, что вы хотите делать со своими операциями. (Сравните фразы, которые я выделил жирным шрифтом в обеих кавычках) Вместо того, чтобы возвращать строку, верните идентификатор
Operation
в абстрактном смысле, например указатель на объект этого класса в oop.По поводу комментария
Нет, не будет. Помните, что шаблоны очень абстрактны , настолько абстрактны, что являются в некоторой степени мета. То есть они часто абстрагируют само программирование, а не какую-то концепцию реального мира. Шаблон команды - это абстракция вызова функции. (или метод) Как будто кто-то нажал кнопку паузы сразу после передачи значений параметров и непосредственно перед выполнением, которое будет возобновлено позже.
Следующее рассматривает oop, но мотивация должна быть верна для любой парадигмы. Мне нравится приводить некоторые причины, почему размещение логики в команде может считаться плохой вещью.
Напомним: шаблон команды позволяет вам обернуть функциональность в объект, который будет выполнен позже. Ради модульности (функциональность существует независимо от того, выполняется она с помощью команды или нет), тестируемости (функциональность должна проверяться без команды) и всех тех умных слов, которые по существу выражают требование писать хороший код, вы бы не ставили фактическую логику в команду.
Поскольку шаблоны являются абстрактными, может быть трудно найти хорошие метафоры реального мира. Вот попытка:
«Эй, бабушка, не могли бы вы нажать кнопку записи на телевизоре в 12 часов, чтобы я не пропустил симпсонов на канале 1?»
Моя бабушка не знает, что происходит технически, когда она нажимает кнопку записи. Логика в другом месте (в телевизоре). И это хорошо. Функциональность инкапсулирована, а информация скрыта от команды, это пользователь API, не обязательно часть логики ... о, боже, я снова болтаю модные слова, мне лучше закончить это редактирование сейчас.
источник
Идея обертывания примитивных типов,
Очевидно, что это будет очень сложно и непрактично делать везде, но важно обернуть типы там, где это необходимо,
Например, если у вас есть класс Order,
Важными свойствами для поиска ордера являются главным образом OrderId и InvoiceNumber. А Amount и CurrencyCode тесно связаны между собой, и если кто-то изменит CurrencyCode без изменения Amount, Заказ больше не будет считаться действительным.
Таким образом, в этом сценарии имеет смысл только перенос OrderId, InvoiceNumber и введение композита для валюты, и, вероятно, перенос описания не имеет смысла. Таким образом, предпочтительный результат может выглядеть так:
Таким образом, нет причин, чтобы обернуть все, но вещи, которые на самом деле имеет значение.
источник
Непопулярное мнение:
Обычно вы не должны определять новый тип!
Определение нового типа для переноса примитива или базового класса иногда называется определением псевдотипа. IBM описывает это как плохая практика здесь (они сосредоточены именно на неправильное использование дженериков в данном случае).
Псевдотипы делают обычные функции библиотеки бесполезными.
Математические функции Java могут работать со всеми числовыми примитивами. Но если вы определяете новый класс Percentage (оборачивая двойной, который может находиться в диапазоне 0 ~ 1), все эти функции бесполезны и должны быть обернуты классами, которые (что еще хуже) должны знать о внутреннем представлении класса Percentage ,
Псевдотипы становятся вирусными
При создании нескольких библиотек вы часто обнаружите, что эти псевдотипы становятся вирусными. Если вы используете вышеупомянутый класс Percentage в одной библиотеке, вам нужно либо преобразовать его на границе библиотеки (потеряв всю безопасность / смысл / логику / другую причину, по которой вы создавали этот тип), либо вам придется создавать эти классы доступны для другой библиотеки, а также. Заражение новой библиотеки вашими типами, где простого двойника могло бы быть достаточно.
Забрать сообщение
Поскольку тип, который вы переносите, не требует много бизнес-логики, я бы посоветовал не помещать его в псевдокласс. Вы должны только обернуть класс, если есть серьезные деловые ограничения. В других случаях правильное именование переменных должно иметь большое значение для передачи значения.
Пример:
A
uint
может прекрасно представлять UserId, мы можем продолжать использовать встроенные операторы Java дляuint
s (например, ==), и нам не нужна бизнес-логика для защиты «внутреннего состояния» идентификатора пользователя.источник
Как и все советы, умение знать, когда применять правила. Если вы создаете свои собственные типы на языке управления типами, вы получаете проверку типов. В общем, это будет хороший план.
NamingConvention является ключом к удобочитаемости. Два вместе могут ясно выразить намерение.
Но /// все еще полезно.
Так что да, я бы сказал, создайте много собственных типов, когда их время жизни выходит за границы класса. Также рассмотрите возможность использования обоих
Struct
иClass
не всегда класса.источник
///
значит?