С помощью различных постов и комментариев на этой странице было найдено решение, которое я считаю правильным для моего сценария.
Ниже показаны итеративные изменения в решении, соответствующие принципам SOLID.
Требование
Чтобы создать ответ для веб-службы, пары ключ + объект добавляются к объекту ответа. Необходимо добавить множество пар «ключ + объект», каждая из которых может иметь уникальную обработку, необходимую для преобразования данных из источника в формат, требуемый в ответе.
Из этого ясно, что, хотя разные пары ключ / значение могут иметь разные требования к обработке для преобразования исходных данных в целевой объект ответа, все они имеют общую цель добавления объекта в объект ответа.
Следовательно, в итерации 1 был создан следующий интерфейс:
Итерация решения 1
ResponseObjectProvider<T, S> {
void addObject(T targetObject, S sourceObject, String targetKey);
}
Любой разработчик, которому необходимо добавить объект в ответ, теперь может сделать это, используя существующую реализацию, соответствующую их требованию, или добавить новую реализацию с учетом нового сценария
Это замечательно, поскольку у нас есть общий интерфейс, который действует как контракт для этой обычной практики добавления объектов ответа
Однако один сценарий требует, чтобы целевой объект был взят из исходного объекта с заданным конкретным ключом, «идентификатором».
Здесь есть опции, во-первых, добавить реализацию существующего интерфейса следующим образом:
public class GetIdentifierResponseObjectProvider<T extends Map, S extends Map> implements ResponseObjectProvider<T, S> {
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get("identifier"));
}
}
Это работает, однако этот сценарий может потребоваться для других ключей исходного объекта («startDate», «endDate» и т. Д.), Поэтому эту реализацию следует сделать более общей, чтобы разрешить ее повторное использование в этом сценарии.
Кроме того, для других реализаций может потребоваться больше контекстной информации для выполнения операции addObject ... Таким образом, необходимо добавить новый универсальный тип для обслуживания этого
Итерация решения 2
ResponseObjectProvider<T, S, U> {
void addObject(T targetObject, S sourceObject, String targetKey);
void setParams(U params);
U getParams();
}
Этот интерфейс обслуживает оба сценария использования; реализации, которые требуют дополнительных параметров для выполнения операции addObject, и реализации, которые не
Однако, учитывая последний из сценариев использования, реализации, которые не требуют дополнительных параметров, нарушат принцип сегрегации интерфейса SOLID, так как эти реализации будут переопределять методы getParams и setParams, но не реализуют их. например:
public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S, U> {
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get(U));
}
public void setParams(U params) {
//unimplemented method
}
U getParams() {
//unimplemented method
}
}
Итерация решения 3
Чтобы исправить проблему разделения интерфейса, методы интерфейса getParams и setParams были перемещены в новый интерфейс:
public interface ParametersProvider<T> {
void setParams(T params);
T getParams();
}
Реализации, которым требуются параметры, теперь могут реализовывать интерфейс ParametersProvider:
public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S>, ParametersProvider<U>
private String params;
public void setParams(U params) {
this.params = params;
}
public U getParams() {
return this.params;
}
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get(params));
}
}
Это решает проблему разделения интерфейса, но вызывает еще две проблемы ... Если вызывающий клиент хочет запрограммировать интерфейс, то есть:
ResponseObjectProvider responseObjectProvider = new GetObjectBySourceKeyResponseObjectProvider<>();
Тогда метод addObject будет доступен для экземпляра, но НЕ для методов getParams и setParams интерфейса ParametersProvider ... Чтобы вызвать их, требуется приведение, и для обеспечения безопасности также следует выполнить проверку экземпляра:
if(responseObjectProvider instanceof ParametersProvider) {
((ParametersProvider)responseObjectProvider).setParams("identifier");
}
Это не только нежелательно, но и нарушает принцип подстановки Лискова - ", если S является подтипом T, тогда объекты типа T в программе могут быть заменены объектами типа S без изменения какого-либо из желаемых свойства этой программы"
т.е. если мы заменим реализацию ResponseObjectProvider, которая также реализует ParametersProvider, реализацией, которая не реализует ParametersProvider, то это может изменить некоторые из желательных свойств программы ... Кроме того, клиент должен знать, какая реализация используется вызывать правильные методы
Дополнительной проблемой является использование для вызова клиентов. Если вызывающий клиент хочет использовать экземпляр, который реализует оба интерфейса, для выполнения addObject несколько раз, метод setParams должен быть вызван перед addObject ... Это может привести к ошибкам, которых можно избежать, если при вызове не соблюдать осторожность.
Итерация решения 4 - Окончательное решение
Интерфейсы, созданные в Solution Iteration 3, решают все известные на данный момент требования к использованию с некоторой гибкостью, обеспечиваемой обобщениями для реализации с использованием различных типов. Однако это решение нарушает принцип подстановки Лискова и имеет неочевидное использование setParams для вызывающего клиента
Решение состоит в том, чтобы иметь два отдельных интерфейса: ParameterisedResponseObjectProvider и ResponseObjectProvider.
Это позволяет клиенту программировать для интерфейса и выбирает соответствующий интерфейс в зависимости от того, требуют ли дополнительные параметры добавляемые в ответ объекты или нет
Новый интерфейс был впервые реализован как расширение ResponseObjectProvider:
public interface ParameterisedResponseObjectProvider<T,S,U> extends ResponseObjectProvider<T, S> {
void setParams(U params);
U getParams();
}
Однако, это все еще имело проблему использования, когда вызывающему клиенту сначала нужно было бы вызвать setParams перед вызовом addObject, а также сделать код менее читаемым.
Таким образом, окончательное решение имеет два отдельных интерфейса, определенных следующим образом:
public interface ResponseObjectProvider<T, S> {
void addObject(T targetObject, S sourceObject, String targetKey);
}
public interface ParameterisedResponseObjectProvider<T,S,U> {
void addObject(T targetObject, S sourceObject, String targetKey, U params);
}
Это решение устраняет нарушения принципов сегрегации интерфейса и подстановки Лискова, а также улучшает использование для вызова клиентов и улучшает читаемость кода.
Это означает, что клиент должен знать о различных интерфейсах, но, поскольку контракты различаются, это кажется оправданным решением, особенно при рассмотрении всех проблем, которых решение избежало.