Как сохранить низкий аргумент и сохранить независимость сторонних зависимостей?

13

Я использую стороннюю библиотеку. Они передают мне POJO, который для наших намерений и целей, вероятно, реализован так:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

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

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

И тогда это:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Этот класс адаптера связан с несколькими другими классами, которые ДОЛЖНЫ знать о стороннем API, ограничивая его распространение во всей моей системе. Однако ... это решение GROSS! В Чистом Коде, страница 40:

Более трех аргументов (полиадические) требуют особого обоснования, и их не следует использовать в любом случае.

Вещи, которые я рассмотрел:

  • Создание фабричного объекта, а не статического вспомогательного метода
    • Не решить проблему с баджиллионными аргументами
  • Создание подкласса DataTypeOne и DataTypeTwo с зависимым конструктором
    • Все еще имеет конструктор с полиадной защитой
  • Создайте совершенно отдельные реализации, которые соответствуют одному и тому же интерфейсу
  • Множество вышеперечисленных идей одновременно

Как справиться с этой ситуацией?


Обратите внимание, что это не ситуация с антикоррупционным уровнем . Нет ничего плохого в их API. Проблемы:

  • Я не хочу, чтобы мои структуры данных имели import com.third.party.library.SomeDataStructure;
  • Я не могу построить их структуры данных в моих тестовых примерах
  • Мое текущее решение приводит к очень очень большому количеству аргументов. Я хочу, чтобы количество аргументов было низким, БЕЗ передачи их структур данных.
  • Этот вопрос « что такое антикоррупционный слой?». Мой вопрос: « Как я могу использовать шаблон, любой шаблон, чтобы решить этот сценарий?»

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

durron597
источник
Если существует несколько таких POJO сторонних производителей, возможно, стоит написать пользовательский тестовый код, который использует карту с некоторыми соглашениями (например, называть ключи int_bar) в качестве входных данных для теста. Или используйте JSON или XML с некоторым пользовательским промежуточным кодом. По сути, это своего рода DSL для тестирования com.thirdparty.
user949300
Полная цитата из Чистого кода:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal
11
Слепое следование шаблону или руководству по программированию является его собственным анти-шаблоном .
Лилиенталь
2
«Инкапсуляция их API и облегчение модульного тестирования» Похоже, это может быть причиной чрезмерного тестирования и / или вызванного тестированием Повреждения проекта (или показательного, что вы могли бы спроектировать это по-другому с самого начала). Задайте себе вопрос: действительно ли это делает ваш код легче для понимания, изменения и повторного использования? Я бы положил свои деньги на «нет». Насколько реально, что вы когда-нибудь поменяетесь этой библиотекой? Наверное, не очень. Если вы меняете его местами, неужели это действительно облегчает установку совершенно другого на место? Опять же, я бы поставил на «нет».
jpmc26
1
@JamesAnderson Я только что воспроизвел полную цитату, потому что нашел ее интересной, но из фрагмента мне было непонятно, относится ли она к функциям вообще или конкретно к конструкторам. Я не хотел одобрить претензию, и, как сказал jpmc26, мой следующий комментарий должен дать вам некоторое представление о том, что я этого не делал. Я не уверен, почему вы чувствуете необходимость нападать на ученых, но использование полислогов не делает кого-то академическим элитаром, сидящим на своей башне из слоновой кости над облаками.
Лилиенталь

Ответы:

10

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

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Затем конструктор для DataTypeTwo принимает объект DataTypeTwoParameters, а DataTypeTwo создается с помощью:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Это дает много возможностей прояснить, что представляют собой все параметры, входящие в DataTypeTwo, и что они означают. Вы также можете указать разумные значения по умолчанию в конструкторе DataTypeTwoParameters, чтобы можно было задавать только те значения, которые необходимо установить, в любом порядке, который нравится потребителю API.

Erik
источник
Интересный подход. Где бы вы поставили соответствующий Integer.parseInt? В сеттере или вне класса параметров?
durron597
5
За пределами класса параметров. Класс параметров должен быть «тупым» объектом и не должен пытаться делать что-либо, кроме как выражать требуемые входные данные и их типы. Синтаксический должно быть сделано в другом месте, например: p.bar = Integer.parseInt("4").
Эрик
7
это звучит как шаблон объекта параметров
комнат
9
... или анти-паттерн.
Теластин
1
... или вы можете просто переименовать DataTypeTwoParametersв DataTypeTwo.
user253751 30.01.15
14

У вас действительно есть две отдельные проблемы: обертывание API и сохранение низкого количества аргументов.

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

Затем, когда у вас есть желаемый API, количество параметров может перестать быть проблемой. Если это так, существует ряд общих шаблонов, которые следует учитывать:

  • Объект параметра, как в ответе Эрика .
  • Шаблон строителя , где вы создаете отдельный объект строителя, а затем вызвать ряд сеттеров для установки параметров по отдельности, а затем создать конечный объект.
  • Модель прототипа , где вы клонировать подклассы нужного объекта с поля уже установлены внутри.
  • Завод, с которым вы уже знакомы.
  • Некоторая комбинация вышеперечисленного.

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

Обратите внимание, что, как правило, гораздо проще и проще обслуживать переход к базовому API, сохраняя ссылку на OurDataобъект и перенаправляя вызовы метода, а не пытаясь переопределить его внутренние компоненты. Например:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Карл Билефельдт
источник
Первая половина этого ответа: отлично, очень полезно, +1. Вторая половина этого ответа: «перейти к базовому API, сохранив ссылку на OurDataобъект» - это то, чего я пытаюсь избежать, по крайней мере, в базовом классе, чтобы гарантировать отсутствие зависимости.
durron597
1
Вот почему вы делаете это только в одной из своих реализаций DataInterface. Вы создаете другую реализацию для своих фиктивных объектов.
Карл Билефельдт
@ durron597: да, но вы уже знаете, как решить эту проблему, если она действительно беспокоит вас.
Док Браун
1

Я думаю, что вы, возможно, интерпретируете рекомендацию дяди Боба слишком строго. Для нормальных классов, с логикой, методами, конструкторами и т.п., полиадический конструктор действительно очень похож на запах кода. Но для чего-то, что является строго контейнером данных, который предоставляет поля и генерируется тем, что по сути уже является объектом Factory, я не думаю, что это слишком плохо.

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

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

Авнер Шахар-Каштан
источник
Проблема в том, что количество полей в моей структуре данных менялось несколько раз и, вероятно, снова изменится. Это означает, что мне нужно реорганизовать конструктор во всех моих тестовых случаях. Шаблон параметров с разумными значениями по умолчанию звучит как лучший путь; наличие изменяемой версии, которая сохраняется в неизменяемом виде, могло бы облегчить мою жизнь во многих отношениях.
durron597