Статьи

Плохой код: слишком много преобразований объектов между уровнями приложений и как их избежать

Работали ли вы когда-нибудь с приложением, в котором вам приходилось копировать данные из одного объекта в другой, другой и т. Д., Прежде чем вы действительно смогли что-то с ним сделать? Вы когда-нибудь писали код для преобразования данных из XML в DTO в бизнес-объект в оператор JDBC? Снова и снова для каждого из типов обрабатываемых данных? Затем вы столкнулись со слишком распространенным антипаттерном многих «корпоративных» (читай «переобработанных») приложений, которые мы могли бы назвать «Марш бесконечной картографии смерти». Давайте посмотрим на приложение, страдающее от этого антипаттерна, и на то, как переписать его в гораздо более приятной, простой и удобной форме.

Приложение World of Thrilling Fashion (или WTF для краткости) собирает и хранит информацию о недавно разработанных платьях и делает ее доступной через REST API. Каждое бедное платье должно пройти следующие преобразования, прежде чем обратиться к преданному поклоннику моды:

  1. Разбор из XML в XML-специфичный объект XDress
  2. Обработка и преобразование в специализированный объект Dress
  3. Преобразование в DBObject MongoDB, чтобы его можно было сохранить в БД (как JSON)
  4. Преобразование из объекта DBObject обратно в объект Dress
  5. Преобразование из платья в строку JSON

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

Исключение ручных преобразований

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

  1. Обобщите преобразования так, чтобы их нужно было записать только один раз (вероятно, используя существующие библиотеки преобразований)
  2. Устраните их, например, используйте один и тот же формат данных по всей цепочке обработки

Чтобы быть справедливым, я должен признать, что ручной подход также имеет некоторые преимущества: у вас есть полная (дурак? :-)) Власть над формой объектов и вы можете идеально подходить им для требуемой обработки, вам не нужно вводить огромные и глючные библиотеки, и вы зарабатываете больше денег, если оплачиваете LOC.

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

Остается один вопрос: как мы представляем данные? У нас есть две возможности:

  1. С общими структурами данных, т.е. картами. Это распространено в динамических функциональных языках, таких как Clojure, и это чрезвычайно просто и удобно.

    • Плюсы: меньше работы, очень гибкие, общие операции могут быть легко применены (карта, фильтр и т. Д.)
  2. С объектами, специфичными для каждого типа данных, т.е. POJO, такими как DressVariant, Shoes

    • Плюсы: тип безопасности, компилятор помогает убедиться, что ваш код правильный, это может быть проще для понимания
    • Минусы: Вы должны написать и поддерживать класс для каждого возможного элемента данных, обрабатываемого

Sidenote: бизнес-домен

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

WTF должен выполнить некоторую обработку найденных элементов одежды, главным образом потому, что несколько элементов могут представлять одно и то же платье только с небольшими изменениями, такими как цвет. Таким образом, WTF хранит такую ​​группу связанных элементов как список элементов DressVariant внутри родительского объекта Dress, генерирует уникальный идентификатор для Dress и сохраняет идентификаторы входных элементов в атрибуте с именем «externalIds». Поэтому N входных элементов становится M элементами Dress с 1+ DressVariants, M <= N.

WTF также должен выполнить некоторую другую обработку своего XML-ввода WTF, например, определить, какие изображения являются реальными, а какие — просто фальшивыми заполнителями, но мы не будем это обсуждать.

Реализация статической типичной обработки

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

Давайте сначала посмотрим, как я хотел бы построить конвейер обработки:

fetchFrom("http://wtf.example.com/atom/dresses.xml")
	   .parseNodesAt("/feed/dress")
	   .transform(DressVariant.class, new DressDeduplicatingTransformer()); // Transformer
	   .transform(new PojoToDboTransformer()); // Transformer
	   .store(new MongoDAO());
	// + we'll use DBObject.toMap() + PojoToJson mapper when serving the data via REST

Таким образом, мы извлекаем XML из URL, отправляем его в анализатор для извлечения некоторых узлов, которые автоматически преобразуются в объекты DressVariant, затем мы используем преобразователь, который объединяет несколько DressVariants в один унифицированный объект Dress, и, наконец, мы конвертируем полученный POJO в Mongo DBObject перед сохранением в БД. Что мы используем для конверсий?

  1. XML -> DressVariant: используйте JAXB для преобразования узлов в наш POJO с аннотацией @XmlRootElement. Обратите внимание, что вы можете настроить преобразование, которое JAXB выполняет очень много, если это необходимо. Таким образом, вам нужно всего лишь создать простой POJO и добавить одну аннотацию.
  2. DressVariant> Dress проблема для нас). Это преобразование зависит от типа, то есть для каждого типа данных мы должны кодировать свое собственное преобразование. Это хорошо, потому что, например, обувь не нуждается в такой дедупликации обработки / конвертации.
  3. Dress -> DBObject: Мы будем использовать Jackson Mongo Mapper и расширение первоклассной библиотеки отображения JSON, которая добавляет поддержку Mongo DB. Он также выполняет специальную очистку данных, требуемую Mongo, например замену ‘.’ в ключах карты с ‘-‘.
  4. DBObject -> MongoDB: у нас будет один универсальный метод storeDocument (String collectionName, DBObject doc), где имя коллекции получено из исходного объекта (например, DressVariant -> «dressVariants»). Идентификатор атрибута документа, как ожидается, будет его уникальным идентификатором (и, таким образом, мы будем либо обновлять, либо вставлять на основе его [отсутствующего] значения).
  5. MongoDB -> DBObject: снова универсальный метод list (String collectionName)
  6. DBObject -> Map: DBObject делает это сам
  7. Карта -> JSON: мы будем использовать функцию PojoMapping библиотеки REST Джерси для автоматического преобразования карты, созданной нашими методами, в JSON при отправке ее клиентам.
  8. JSON -> клиенты: у нас будет один GenericCollectionResource с методом списка, сопоставленным с URL / list / {collectionName} ». Он загрузит коллекцию из Mongo, как описано, и вернет список, автоматически преобразованный в JSON Джерси.

Результат: Помимо пользовательских преобразований, специфичных для типа данных, вместо 1 POJO, 4 преобразователей с ручным кодированием и 2 + 2 методов для каждого типа данных нам теперь требуется только 1 POJO на тип данных плюс один универсальный преобразователь, 4 универсальных метода и одна или две библиотеки. Меньше кода, меньше кода, меньше дефектов, больше производительности, больше веселья.

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

Образец кода

Пример кода, демонстрирующий автоматические общие отображения XML -> Java -> Mongo -> REST с JSON, доступен на GitHub — generic-pojo-mappers.

Резюме и заключение

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

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

В примере World of Thrilling Fashion мы значительно сократили количество ручного кодирования и методов, и в результате получился меньший, более чистый и более гибкий код (за счет добавления новых типов данных).

критика

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

Ваш выбор, если вам действительно это нужно, сделайте это, но помните, сколько вы за это платите.

Библиотеки злые!

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

Ты — идиот!

Да, многие так думают . Спасибо за чтение.

Вы говорите, что я идиот, если я написал такой код?

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

связанные с

Богатые шаблоны персистентных доменных объектов + тонкий шлюз от Адама Бена также позволяют использовать один и тот же объект (сущность JPA) во всем приложении (веб-интерфейс — БД) во имя повышения производительности.