Управление повторяющимся кодом и документацией на Java
Повторяющийся код, как правило, плохая вещь, и существуют шаблоны проектирования, которые могут помочь минимизировать это. Однако иногда это просто неизбежно из-за ограничений самого языка. Возьмите следующий пример из java.util.Arrays
:
/**
* Assigns the specified long value to each element of the specified
* range of the specified array of longs. The range to be filled
* extends from index <tt>fromIndex</tt>, inclusive, to index
* <tt>toIndex</tt>, exclusive. (If <tt>fromIndex==toIndex</tt>, the
* range to be filled is empty.)
*
* @param a the array to be filled
* @param fromIndex the index of the first element (inclusive) to be
* filled with the specified value
* @param toIndex the index of the last element (exclusive) to be
* filled with the specified value
* @param val the value to be stored in all elements of the array
* @throws IllegalArgumentException if <tt>fromIndex > toIndex</tt>
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
* <tt>toIndex > a.length</tt>
*/
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
rangeCheck(a.length, fromIndex, toIndex);
for (int i=fromIndex; i<toIndex; i++)
a[i] = val;
}
Приведенный выше фрагмент кода встречается в исходном коде 8 раз, с очень небольшими изменениями в сигнатуре документации / метода, но в точности совпадает с телом метода, по одному для каждого типа корневого массива. int[]
, short[]
, char[]
, byte[]
, boolean[]
, double[]
, float[]
, а также Object[]
,
Я считаю, что если кто-то не прибегает к рефлексии (что само по себе является совершенно другим предметом), такое повторение неизбежно. Я понимаю, что, как служебный класс, такая высокая концентрация повторяющегося кода Java очень нетипична, но даже при наилучшей практике повторение происходит! Рефакторинг не всегда работает, потому что это не всегда возможно (очевидный случай, когда повторение в документации).
Очевидно, что поддержание этого исходного кода - кошмар. Небольшая опечатка в документации или небольшая ошибка в реализации умножается на то, сколько раз было сделано повторений. На самом деле, лучшим примером является именно этот класс:
Ошибка удивительно тонкая, возникающая в том, что многие считают простым и понятным алгоритмом.
// int mid =(low + high) / 2; // the bug
int mid = (low + high) >>> 1; // the fix
Приведенная выше строка появляется в исходном коде 11 раз!
Итак, мои вопросы:
- Как на практике обрабатываются эти виды повторяющегося кода / документации Java? Как они разработаны, поддерживаются и проверяются?
- Вы начинаете с "оригинала" и делаете его как можно более зрелым, а затем копируете и вставляете по мере необходимости и надеетесь, что не ошиблись?
- И если вы допустили ошибку в оригинале, то просто исправьте ее повсюду, если вы не можете удалить копии и повторить весь процесс репликации?
- И вы применяете этот же процесс для тестирования кода?
- Получит ли Java выгоду от некоторой предварительной обработки исходного кода для такого рода вещей?
- Возможно, у Sun есть собственный препроцессор, помогающий писать, поддерживать, документировать и тестировать этот тип повторяющегося библиотечного кода?
Комментарий запросил другой пример, поэтому я вытащил его из Google Collections: com.google.common.base.Predicates строки 276-310 (AndPredicate
) против строк 312-346 (OrPredicate
).
Источник для этих двух классов идентичен, за исключением:
AndPredicate
противOrPredicate
(каждый появляется 5 раз в своем классе)"And("
противOr("
(в соответствующемtoString()
методы)#and
против#or
(в@see
Ага комментарии)true
противfalse
(вapply
;!
можно переписать из выражения)-1 /* all bits on */
против0 /* all bits off */
вhashCode()
&=
против|=
вhashCode()
9 ответов
Для людей, которые абсолютно нуждаются в производительности, бокс и распаковка, а также обобщенные коллекции и тому подобное, являются большими запретами.
Та же проблема возникает в высокопроизводительных вычислениях, когда вам нужен один и тот же комплекс для работы как для чисел с плавающей точкой, так и для двойных (например, некоторые из методов, показанных в документе Голдберда " Что должен знать каждый компьютерщик о числах с плавающей запятой ").
Есть причина, по которой Троув TIntIntHashMap
бегает кругами вокруг Явы HashMap<Integer,Integer>
при работе с аналогичным количеством данных.
Теперь, как пишется исходный код коллекции Trove?
Конечно, используя инструментарий исходного кода:)
Существует несколько библиотек Java для более высокой производительности (намного выше, чем библиотеки Java по умолчанию), которые используют генераторы кода для создания повторяющегося исходного кода.
Мы все знаем, что "инструментарий исходного кода" - это зло, а генерация кода - дерьмо, но тем не менее именно так и поступают люди, которые действительно знают, что они делают (например, люди, которые пишут такие вещи, как Trove):)
Для чего это стоит, мы генерируем исходный код, который содержит большие предупреждения, такие как:
/*
* This .java source file has been auto-generated from the template xxxxx
*
* DO NOT MODIFY THIS FILE FOR IT SHALL GET OVERWRITTEN
*
*/
Если вам абсолютно необходимо дублировать код, следуйте приведенным великолепным примерам и группируйте весь этот код в одном месте, где его легко найти и исправить, когда вам нужно внести изменения. Документируйте дублирование и, что более важно, причину дублирования, чтобы каждый, кто придет за вами, знал об обоих.
Из Википедии Не повторяйся (СУХОЙ), или Дублирование - зло (УМИРАЕТ)
В некоторых контекстах усилия, требуемые для реализации философии DRY, могут быть больше, чем усилия по сохранению отдельных копий данных. В некоторых других контекстах дублирующаяся информация является неизменной или находится под достаточно жестким контролем, чтобы сделать DRY ненужным.
Вероятно, нет ответа или техники, чтобы предотвратить подобные проблемы.
Даже такие необычные языки, как Haskell, имеют повторяющийся код ( см. Мой пост о haskell и сериализации)
Кажется, есть три варианта решения этой проблемы:
- Используй отражение и теряй производительность
- Используйте предварительную обработку, такую как шаблон Haskell или эквивалент Caml4p для вашего языка, и живите с мерзостью
- Или мои личные любимые макросы, если они поддерживаются вашим языком (схема и шрифт)
Я считаю, что макросы отличаются от предварительной обработки, потому что макросы обычно находятся на том же языке, на котором находится цель, а предварительная обработка - на другом языке.
Я думаю, что макросы Lisp/Scheme решат многие из этих проблем.
Вы можете использовать генератор кода для построения вариантов кода с использованием шаблона. В этом случае исходный код Java является продуктом генератора, а реальный код является шаблоном.
Учитывая два фрагмента кода, которые, как утверждают, являются подобными, большинство языков имеют ограниченные возможности для создания абстракций, которые объединяют фрагменты кода в монолит. Чтобы абстрагироваться, когда ваш язык не может этого сделать, вы должны выйти за пределы языка:-{
Наиболее общий механизм "абстракции" - это полноценный макропроцессор, который может применять произвольные вычисления к "телу макроса" при его создании (например, Post или система перезаписи строк, которая поддерживает Turing). M4 и GPM являются типичными примерами. Препроцессор C не является одним из них.
Если у вас есть такой макропроцессор, вы можете создать "абстракцию" в виде макроса и запустить макропроцессор на "абстрагированном" исходном тексте, чтобы получить фактический исходный код, который вы компилируете и запускаете.
Вы также можете использовать более ограниченные версии идей, часто называемые "генераторами кода". Они обычно не способны к Тьюрингу, но во многих случаях они работают достаточно хорошо. Это зависит от того, насколько сложной должна быть ваша "реализация макроса". (Причина, по которой люди в восторге от механизма шаблонов C++, заключается в том, что, несмотря на его уродство, он способен на Тьюринга, и поэтому люди могут делать с ним действительно уродливые, но удивительные задачи генерации кода). В другом ответе здесь упоминается Trove, который явно относится к более ограниченной, но все же очень полезной категории.
Действительно общие макропроцессоры (такие как M4) манипулируют только текстом; это делает их мощными, но они плохо справляются со структурой языка программирования, и очень неудобно писать генераторы в таком процессоре mcaro, который может не только генерировать код, но и оптимизировать сгенерированный результат. Большинство генераторов кода, с которыми я сталкиваюсь, "вставляют эту строку в этот шаблон строки" и поэтому не могут оптимизировать сгенерированный результат. Если вы хотите, чтобы генерация произвольного кода и высокая производительность загружались, вам нужно что-то, способное Turing, но понимающее структуру сгенерированного кода, чтобы оно могло легко манипулировать (например, оптимизировать) им).
Такой инструмент называется системой трансформации программ. Такой инструмент анализирует исходный текст так же, как это делает компилятор, а затем выполняет анализ / преобразование для достижения желаемого эффекта. Если вы можете поместить маркеры в исходный текст вашей программы (например, структурированные комментарии или аннотации в языках, на которых они есть), указывающие, что делать программному инструменту преобразования, то вы можете использовать его для выполнения такой реализации абстракции, генерации кода и / или оптимизация кода. (Предложение одного автора о подключении к компилятору Java является вариацией этой идеи). Использование общей системы преобразования пупроз (например, DMS Software Reengineering Tookit) означает, что вы можете сделать это практически для любого языка.
Примитивные типы Java портят вас, особенно когда речь идет о массивах. Если вы конкретно спрашиваете о коде, включающем примитивные типы, то я бы сказал, просто попробуйте их избежать. Метод Object[] достаточно, если вы используете упакованные типы.
В общем, вам нужно много модульных тестов, и на самом деле больше ничего не нужно делать, кроме как прибегнуть к рефлексии. Как вы сказали, это совсем другая тема, но не стоит слишком бояться размышлений. Напишите DRYest-код, который вы можете сначала, затем профилируйте его и определите, действительно ли снижение производительности отражения достаточно плохо, чтобы гарантировать написание и поддержку дополнительного кода.
Я понял, что Sun должна документировать подобное для библиотечного кода Java SE, и, возможно, другие сторонние авторы библиотек делают то же самое.
Тем не менее, я думаю, что копировать и вставлять документацию по всему файлу, подобному этому, в код, который используется только дома, - абсолютная трата. Я знаю, что многие люди не согласятся, потому что это сделает их собственные JavaDocs менее чистыми. Тем не менее, компромисс в том, что это делает их код более чистым, что, на мой взгляд, является более важным.
Много такого рода повторений теперь можно избежать благодаря генерикам. Они находка, когда пишут тот же код, где меняются только типы.
К сожалению, я думаю, что универсальные массивы все еще не очень хорошо поддерживаются. Пока, по крайней мере, используйте контейнеры, которые позволяют вам использовать дженерики. Полиморфизм также является полезным инструментом для уменьшения дублирования кода.
Чтобы ответить на ваш вопрос о том, как обрабатывать код, который обязательно должен быть продублирован... Пометьте каждый экземпляр легко доступными для поиска комментариями. Есть некоторые препроцессоры Java, которые добавляют макросы в стиле C. Я думаю, что я помню netbeans, имеющий один.