Пример вопроса
Когда я создавал абстрактный класс Java :: Geci AbstractFieldsGenerator
и AbstractFilteredFieldsGenerator
я столкнулся с не слишком сложной проблемой проектирования. Я хотел бы подчеркнуть, что эта проблема и дизайн могут показаться очевидными для некоторых из вас, но во время моего недавнего разговора с младшим разработчиком (мой сын, в частности Михай, который также просматривает мои статьи, потому что его английский намного лучше моего), я понял, что эта тема еще может иметь значение.
Так или иначе. У меня были эти два класса, поля и генератор отфильтрованных полей. Второй класс расширяет первый
1
2
|
abstract class AbstractFilteredFieldsGenerator extends AbstractFieldsGenerator {... |
добавляя дополнительную функциональность и в то же время он должен обеспечивать ту же сигнатуру для конкретной реализации. Что это означает?
Эти генераторы помогают генерировать код для определенного класса, используя отражение. Поэтому входная информация, с которой они работают, является объектом Class
. Класс генератора полей имеет абстрактный метод process()
, который вызывается для каждого поля. Он вызывается из реализованного метода, который зацикливается на полях и выполняет вызов отдельно для каждого. Когда конкретный класс extends AbstractFieldsGenerator
и, таким образом, реализует этот абстрактный метод, он будет вызван. Когда тот же конкретный класс изменяется так, что он extends AbstractFilteredFieldsGenerator
тогда конкретный метод будет вызываться только для отфильтрованного метода. Я хотел дизайн, чтобы ЕДИНСТВЕННОЕ изменение, которое было необходимо в конкретном классе, это изменение имени.
Аннотация класс определение проблемы
Та же проблема описана более абстрактно: есть два абстрактных класса A
и F
так что F extends A
а F
обеспечивает некоторую дополнительную функциональность. Оба объявляют абстрактный метод m()
который должен реализовать конкретный класс. Когда конкретное объявление класса C
изменено с C extends A
на C extends F
то вызов метода m()
должен измениться, но в классе C
не должно быть никаких других изменений. Метод m()
вызывается из метода p()
определенного в классе A
Как спроектировать F
?
В чем проблема с этим?
Расширение A
может быть сделано двумя существенно разными способами:
-
F
переопределяетm()
делая его конкретным реализующим дополнительные функции вm()
и вызывает новый абстрактный метод, скажем,mx()
-
F
переопределяет методp()
версией, которая обеспечивает дополнительную функциональность (фильтрация в приведенном выше примере) и вызывает все еще абстрактный методm()
Первый подход не удовлетворяет требованию, чтобы подпись, которая должна быть реализована конкретным классом C
оставалась неизменной. Второй подход выбрасывает уже реализованную функциональность A
в мусор и переопределяет его немного по-другому. На практике это возможно, но это определенно будет программирование копирования / вставки. Это проблематично, позвольте мне не объяснять почему.
Корень проблемы
В инженерном деле, когда мы сталкиваемся с подобной проблемой, обычно это означает, что проблема или структура недостаточно хорошо описаны, и решение находится где-то в совершенно другой области. Другими словами, есть некоторые предположения, движущие наше мышление, которые являются ложными. В этом случае проблема заключается в том, что мы предполагаем, что абстрактные классы предоставляют ОДНО расширение «API» для их расширения. Обратите внимание, что API — это не только то, что вы можете вызывать. В случае абстрактного класса API — это то, что вы реализуете, когда расширяете абстрактный класс. Подобно тому, как библиотеки могут предоставлять разные API для разных способов использования (HTTP-клиент Java 9 может send()
а также sendAsync()
) абстрактные (а на самом деле также не абстрактные) классы также могут предоставлять различные способы расширения для разных целей.
Нет способа кодирования F
достигающего нашей цели дизайна, без изменения A
Нам нужна версия A
которая предоставляет другой API для создания конкретной реализации и другую, не обязательно дизъюнктивную / ортогональную, для создания еще абстрактного расширения.
Разница между API в этом случае заключается в том, что конкретная реализация стремится быть в конце цепочки вызовов, в то время как абстрактное расширение хочет подключиться к последнему, но одному элементу цепочки. Реализация A
должна предоставлять API для подключения к последнему, но одному элементу цепочки вызовов. Это уже решение.
Решение
Мы реализуем метод ma()
в классе F
и хотим, чтобы p()
вызывал наш ma()
вместо прямого вызова m()
. Изменяя A
мы можем это сделать. Мы определяем ma()
в A
и вызываем ma()
из p()
. Версия ma()
реализованная в A
должна вызывать m()
без лишних слов, чтобы предоставить исходный «API» для конкретных реализаций A
Реализация ma()
в F
содержит дополнительную функциональность (фильтрация в примере) и затем вызывает m()
. Таким образом, любой конкретный класс может расширять A
или F
и может реализовывать m()
с точно такой же сигнатурой. Мы также избегали копирования / вставки, за исключением того, что вызов m()
является кодом, который одинаков в двух версиях ma()
.
Если мы хотим, чтобы класс F
расширялся более абстрактными классами, то реализация F::ma
должна напрямую вызывать не m()
а новую mf()
которая вызывает m()
. Таким образом, новый абстрактный класс может переопределить mf()
снова предоставив новую функциональность, и вызвать абстрактный m()
.
навынос
- Программирование абстрактных классов является сложным, и иногда трудно получить четкое представление о том, кто кого вызывает и какую реализацию. Вы можете преодолеть эту проблему, если поймете, что это может быть сложным вопросом. Документируйте, визуализируйте, обсуждайте любой способ, который может вам помочь.
- Когда вы не можете решить проблему (в примере, как кодировать
F
), вы должны бросить вызов среде (классA
мы безоговорочно предполагали неизменным, сформулировал вопрос: «Как реализоватьF
?»). - Избегайте копирования / вставки программирования. (Pasta содержит много CH и делает ваш код жирным, артерии забиваются и, наконец, сердце вашего приложения перестанет биться.)
- Хотя это и не подробно описано в этой статье, имейте в виду, что чем глубже иерархия абстракции, тем сложнее получить четкий обзор того, кто кому звонит (см. Также пункт 1).
- Найдите пример демонстрационного приложения по адресу https://github.com/verhas/abstractchain
- Найдите оригинальное, чуть более сложное приложение с таким шаблоном на https://github.com/verhas/javageci
Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Расширение абстрактных классов с помощью абстрактных классов в Java Мнения, высказанные участниками Java Code Geeks, являются их собственными. |