Рефакторинг устаревшего экземпляра корпуса коммутатора с помощью шаблонов проектирования
Унаследованный код моей компании страдает от распространенного использования экземпляра кожуха коммутатора в виде:
if(object instanceof TypeA) {
TypeA typeA = (TypeA) object;
...
...
}
else if(object instanceof TypeB) {
TypeB typeB = (TypeB) object;
...
...
}
...
...
Что еще хуже, некоторые из классов TypeX в вопросах на самом деле являются обертками классов, найденных в сторонних библиотеках.
Предлагаемый подход использования шаблона проектирования посетителя и специальных оболочек шаблона проектирования посетителя для сторонних классов, как показано здесь (instanceof -> Visitor DP) и здесь (Visitor DP с сторонними классами), кажется хорошим подходом.
Однако во время сеанса обзора кода, на котором был предложен этот подход, вопрос о дополнительных накладных расходах стандартного кода, требуемых при каждом рефакторинге установочного корпуса коммутатора, привел к отклонению этого механизма.
Я хотел бы исправить эту текущую проблему, и я рассматриваю общий подход к проблеме:
Служебный класс, который обернет шаблон дизайна посетителя с общей ссылкой на посещаемые объекты. Идея состоит в том, чтобы реализовать общее ядро класса утилит посетителя один раз и только один раз и предоставить конкретные реализации для поведения объекта TypeX, где это необходимо - возможно, даже многократно используя некоторые реализации через расширение OO классов, реализующих функциональность.
Мой вопрос - кто-нибудь здесь делал что-то похожее? Если нет - можете ли вы указать какие-либо плюсы / минусы, которые могут иметь отношение?
РЕДАКТИРОВАТЬ: слишком много стандартного кода = реализация шаблона дизайна посетителя специально для каждого экземпляра экземпляра switch-case. Это явно избыточно и вызовет много дублирования кода, если посетитель DP не будет реализован с использованием обобщений.
Что касается универсальной утилиты DP посетителя, которую я имел в виду:
Прежде всего, использование отражения с посетителем DP, как показано здесь.
Во-вторых, следующее использование дженериков (на основе рефлексивного посетителя):
public interface ReflectiveVisitor<GenericReturn,GenericMetaData>
{
public GenericReturn visit(Object o, GenericMetaData meta);
}
public interface ReflectiveVisitable<A,B>
{
public GenericReturn accept(Visitor visitor, GenericMetaData meta);
}
GenericReturn и GenericMetaData - это интерфейсы, предназначенные для предоставления любых дополнительных необходимых метаданных для реализации определенных логик, а также для обеспечения универсальности типов возврата, возвращаемых посетителем DP.
Заранее спасибо!
РЕДАКТИРОВАТЬ: Кодирование пластины котла при рефакторинге от instanceof к посетителю:
Частым случаем использования, который мне нужно обработать, является casefasecase для выполнения отдельных вызовов API конкретных реализаций:
public class BoilerPlateExample
...
if(object instanceof TypeA) {
((TypeA) object).specificMethodTypeA(...)......;
}
else if(object instanceof TypeB) {
((TypeB) object).completeyDifferentTypeBMethod(...)......;
}
...
...
Что касается дизайна посетителей, обращающегося с этим?
public interface Visitor
{
// notice how I just binded my interface to a specific set of methods?
// this interface will have to be generic in order to avoid an influx of
// of dedicated interfaces
public void visit(TypeA typeA);
public void visit(TypeB typeB);
}
public interface Visitable
{
public void accept(Visitor visitor);
}
public class BoilerPlateExampleVisitable<T> implements Visitable
{
// This is basically a wrapper on the Types
private T typeX;
public BoilerPlateExampleVisitable (T typeX) {
this.typeX = typeX;
}
public void accept(Visitor visitor) {
visitor.visit(typeX);
}
}
public class BoilerPlateExampleVisitor implements Visitor
{
public void visit(TypeA typeA) {
typeA.specificMethodTypeA(...)......;
}
public void visit(TypeB typeB) {
typeB.completeyDifferentTypeBMethod(...)......;
}
}
public static final BoilerPlateExampleVisitor BOILER_PLATE_EXAMPLE_VISITOR = new BoilerPlateExampleVisitor();
public static void main(....) {
TypeA object = .....; // created by factory
BoilerPlateExampleVisitable boilerPlateVisitable = VisitableFactory.create(object); // created by dedicated factory, warning due to implicit generics
boilerPlateVisitable.accept(BOILER_PLATE_EXAMPLE_VISITOR);
}
2 ответа
TL; DR: допустим, у вас есть N классов с M операциями в каждом. Вам нужен шаблон посетителей, только если M может расти, а N уже велико. Остальное используют полиморфизм.
Может быть, я буду открывать дверь, потому что вы уже думали об этом, но вот несколько мыслей.
Шаблон посетителя
В общем случае вы будете использовать шаблон посетителя, только если вы хотите добавить новые операции без рефакторинга всех классов. Тогда M может расти, а N уже велико.
Для каждой новой операции вы создаете нового посетителя. Этот посетитель принимает N классов и обрабатывает операцию для каждого из них:
public class NewOperationVisitor implements Visitor
{
public void visit(TypeA typeA) {
// apply my new operation to typeA
}
public void visit(TypeB typeB) {
// apply my new operation to typeB
}
...
}
Таким образом, вам не нужно добавлять новую операцию ко всем N классам, но вы должны реорганизовать каждого посетителя, если добавите класс.
Полиморфизм
Теперь, если M стабильна, пожалуйста, избегайте шаблона посетителей: используйте полиморфизм. Каждый класс имеет четко определенный набор методов (примерно по одному на операцию). Если вы добавляете класс, просто определите известные операции для этого класса:
public class TypeX implements Operator
{
public void operation1() {
// pretty simple
}
public void operation2() {
// pretty simple
}
}
Теперь вам нужно провести рефакторинг каждого класса, если вы добавляете операцию, но добавить класс очень легко.
Этот компромисс объясняется в "Чистом коде" Р. К. Мартином (6. Объекты и структуры данных, Антисимметрия данных / объектов):
Процедурный код [здесь: посетитель] затрудняет добавление новых структур данных, потому что все функции должны измениться. ОО-код затрудняет добавление новых функций, потому что все классы должны измениться.
Что ты должен делать
Как указано в комментарии @radiodef, избегайте рефлексии и других хитростей. Это будет хуже, чем проблема.
Четко разделите, где вам действительно нужен шаблон посетителей, а где нет. Подсчет классов и операций. Могу поспорить, что в большинстве случаев вам не нужен шаблон посетителей. (Ваши менеджеры, вероятно, правы!). Если вам нужен шаблон посетителей в 10 % случаев, возможно, "дополнительные издержки стандартного кода" будут приемлемыми.
Поскольку несколько из ваших
TypeX
классы уже являются обертками, возможно, вам нужно обернуть лучше. Иногда один оборачивается снизу вверх: "У моего стороннего класса есть эти методы: я оберну нужные мне методы и забуду другие. И буду сохранять те же имена, чтобы сделать его простым". Вместо этого вы должны тщательно определитьTypeX
класс должен предоставить. (Подсказка: посмотрите в своемif ... instanceof ...
тела). А затем снова заверните сторонние библиотеки для предоставления этих услуг.На самом деле: избегать избегать отражения и других уловок.
Что бы я сделал?
Вы попросили в комментарии псевдокод, но я не могу дать его вам, так как я имею в виду метод, а не процедуру или алгоритм.
Вот минимальный шаг за шагом, что я бы сделал в такой ситуации.
Изолировать каждый "большой instanceof
переключатель "в методе
Это почти медицинский совет! До:
public void someMethod() {
...
...
if(object instanceof TypeA) {
TypeA typeA = (TypeA) object;
...
...
}
else if(object instanceof TypeB) {
TypeB typeB = (TypeB) object;
...
...
}
...
...
}
После:
public void someMethod() {
...
...
this.whatYouDoInTheSwitch(object, some args);
...
...
}
А также:
private void whatYouDoInTheSwitch(Object object, some args) {
if(object instanceof TypeA) {
TypeA typeA = (TypeA) object;
...
...
}
else if(object instanceof TypeB) {
TypeB typeB = (TypeB) object;
...
...
}
}
Любая приличная IDE сделает это бесплатно.
Если вы находитесь в случае, который потребует шаблон посетителя
Оставьте код как этот, но документируйте его:
/** Needs fix: use Visitor Pattern, because... (growing set of operations, ...) */
private void whatYouDoInTheSwitch(Object object, some args) {
...
}
Если вы хотите использовать полиморфизм
Цель состоит в том, чтобы перейти от:
this.whatYouDoInTheSwitch(object, other args);
Для того, чтобы:
object.whatYouDoInTheSwitch(this, other args);
У вас есть небольшой рефакторинг, чтобы сделать:
A. Создайте метод для каждого случая в большом переключателе. Все эти методы должны иметь одинаковую подпись, кроме типа объекта:
private void whatYouDoInTheSwitch(Object object, some args) {
if(object instanceof TypeA) {
this.doIt((TypeA) object, some args);
}
else if(object instanceof TypeB) {
this.doIt((TypeB) object, some args);
}
}
Опять же, любая IDE сделает это бесплатно.
Б. Создайте интерфейс с помощью следующего метода:
doIt(Caller caller, args);
куда Caller
это тип класса, который вы рефакторинг (тот, который содержит большой переключатель).
C. сделать каждый TypeX
реализовать этот интерфейс путем преобразования каждого doIt(TypeX objX, some args)
в doIt(Caller, some args)
метод из TypeX
, По сути, это просто найти-заменить: заменить this
от caller
а также objX
от this
, Но это может занять немного больше времени, чем остальные.
D. Теперь у вас есть:
private void whatYouDoInTheSwitch(Object object, some args) {
if(object instanceof TypeA) {
((TypeA) object).doIt(this, some args);
}
else if(object instanceof TypeB) {
((TypeB) object).doIt(this, some args);
}
}
Это строго эквивалентно:
private void whatYouDoInTheSwitch(Object object, some args) {
if(object instanceof TypeA) {
object.doIt(this, some args);
}
else if(object instanceof TypeB) {
object.doIt(this, some args);
}
}
Потому что во время выполнения JVM найдет правильный метод для подходящего класса (это полиморфизм!). Таким образом, это также эквивалентно (если объект имеет один из перечисленных типов):
private void whatYouDoInTheSwitch(Object object, some args) {
object.doIt(this, some args);
}
E. Включите метод, и вы получите Caller
учебный класс:
public void someMethod() {
...
...
object.doIt(this, some args);
...
...
}
На самом деле, это всего лишь эскиз, и может произойти много особых случаев. Но это гарантированно будет относительно быстро и чисто. Это может быть сделано только для выбранного метода или для всех методов.
Обязательно тестируйте, если возможно, код после каждого шага. И не забудьте выбрать правильные названия для методов.
Похоже на полиморфизм. Такой код может происходить из разнородного набора классов бизнес-объектов, таких как Excel ReportX, Zip, TableY, и таких действий, как Открыть, Закрыть, Сохранить и тому подобное.
Как таковой, этот вид программирования вызывает огромную связь между классами и имеет проблемы с полнотой всех случаев, расширяемостью.
В случае полиморфизма фактическая оболочка для некоторого бизнес-объекта должна обеспечивать действия (Open, Save, Close).
Этот механизм похож на java swing, где поле редактирования имеет свой список действий ("Вырезать", "Копировать", "Вставить" и т. Д.), А в виде дерева - перекрывающийся набор действий. В зависимости от фокуса фактические действия будут установлены в меню действий.
Декларативная спецификация может быть в следующем порядке: скажем, XML, который "содержит" компоненты и их действия.
Если у вас есть парадигма MVC на радаре, учтите следующее: каждое действие может иметь параметры. Используйте PMVC (моя идея), класс Parameters отдельно от класса Model, так как эти данные имеют другой жизненный цикл и являются постоянными.
Дорога к этому может быть:
- прототип с двумя бизнес-объектами и двумя действиями.
- рефакторинг с одним полиморфным бизнес-объектом, содержащим все (старый код).
- медленно перемещая один класс за другим к собственному бизнес-объекту.
- первое удаление из бизнес-объекта polymorph, используемого для очистки новой архитектуры.
Я бы воздержался от использования наследования (BaseDocument с open/save), поскольку это, вероятно, не соответствует более гетерогенной реальности и может привести к параллельным иерархиям классов (XDoc с XContainer и XObject).
Как это на самом деле делается, все еще ваша работа. Я также хотел бы узнать, существует ли установленная парадигма.
Запрашиваемый псевдокод. Нужно немного разобраться с созданным прототипом - доказательством концепции. Однако существует обнаружение (динамических) возможностей / возможностей.
public interface Capabilities {
<T> Optional<T> as(Class<T> type);
}
Добавьте этот интерфейс к каждому классу дел, и вы можете сделать:
void f(Capabilities animal) {
int distance = 45;
animal.as(Flying.class).ifPresent(bird -> bird.fly(distance));
}
Инфраструктура будет: сначала регистрация возможностей и обнаружение могут быть помещены в отдельный класс.
/**
* Capabilities registration & discovery map, one can delegate to.
*/
public class CapabilityLookup implements Capabilities {
private final Map<Class<?>, Object> capabilitiesMap = new HashMap<>();
public final <T> void register(Class<T> type, T instance) {
capabilitiesMap.put(type, instance);
}
@Override
public <T> Optional<T> as(Class<T> type) {
Object instance = capabilitiesMap.get(type);
return instance == null ? Optional.empty()
: Optional.of(type.cast(instance));
}
}
Тогда унаследованные классы могут быть расширены:
/** Extended legacy class. */
public class Ape implements Capabilities {
private final CapabilityLookup lookup = new CapabilityLookup();
public Ape() {
lookup.register(String.class, "oook");
}
@Override
public <T> Optional<T> as(Class<T> type) {
return lookup.as(type); // Delegate to the lookup map.
}
}
/** Extended legacy class. */
public class Bird implements Capabilities {
private final CapabilityLookup lookup = new CapabilityLookup();
public Bird() {
lookup.register(Flying.class, new Flying() {
...
});
lookup.register(Singing.class, new Singing() {
...
});
}
@Override
public <T> Optional<T> as(Class<T> type) {
return lookup.as(type); // Delegate to the lookup map.
}
}
Как вы можете видеть с Bird
исходный код переместится в реальный класс интерфейса, здесь в Bird, так как экземпляр был создан в конструкторе. Но вместо анонимного класса можно сделать BirdAsFlying
класс, своего рода экшн-класс на языке Java. Внутренний класс имеет преимущество для доступа Bird.this
,
Рефакторинг может выполняться постепенно. Добавить возможности для всех устаревших классов instanceof. Последовательность if обычно представляет собой один интерфейс, но также может быть два или один интерфейс с двумя методами.