Реализация по умолчанию или абстрактный метод?
Лучше ли поставить стандартную реализацию метода в суперкласс и переопределить его, если подклассы хотят отклониться от этого, или вы должны просто оставить абстрактный метод суперкласса и повторить обычную реализацию во многих подклассах?
Например, в проекте, в котором я участвую, есть класс, который используется для указания условий, в которых он должен останавливаться. Абстрактный класс выглядит следующим образом:
public abstract class HaltingCondition{
public abstract boolean isFinished(State s);
}
Тривиальная реализация может быть:
public class AlwaysHaltingCondition extends HaltingCondition{
public boolean isFinished(State s){
return true;
}
}
Причина, по которой мы делаем это с объектами, состоит в том, что мы можем тогда произвольно составить эти объекты вместе. Например:
public class ConjunctionHaltingCondition extends HaltingCondition{
private Set<HaltingCondition> conditions;
public void isFinished(State s){
boolean finished = true;
Iterator<HaltingCondition> it = conditions.iterator();
while(it.hasNext()){
finished = finished && it.next().isFinished(s);
}
return finished;
}
}
Однако у нас есть некоторые условия остановки, которые необходимо уведомлять о том, что произошли события. Например:
public class HaltAfterAnyEventHaltingCondition extends HaltingCondition{
private boolean eventHasOccurred = false;
public void eventHasOccurred(Event e){
eventHasOccurred = true;
}
public boolean isFinished(State s){
return eventHasOccurred;
}
}
Как лучше всего представить eventHasOccurred(Event e)
в абстрактном суперклассе? Большинство подклассов могут иметь не-op реализацию этого метода (например, AlwaysHaltingCondition
), в то время как для некоторых требуется значительная реализация для правильной работы (например, HaltAfterAnyEventHaltingCondition
), а другим ничего не нужно делать с сообщением, но нужно передать его на своих подчиненных, чтобы они работали правильно (например, ConjunctionHaltingCondition
).
У нас может быть реализация по умолчанию, которая уменьшит дублирование кода, но приведет к компиляции некоторых подклассов, но не будет корректно работать, если она не была переопределена, или мы могли бы объявить ее абстрактным, что потребовало бы автора каждый подкласс, чтобы подумать о реализации, которую они предоставляют, хотя девять раз из десяти он был бы не-op-реализацией. Каковы другие плюсы и минусы этих стратегий? Является ли это намного лучше, чем другой?
Ответы
Ответ 1
Один из вариантов состоит в том, чтобы иметь еще один абстрактный подкласс для использования в качестве суперкласса для всех реализаций, которые хотят использовать реализацию по умолчанию.
Лично я обычно оставляю абстрактные абстрактные методы в абстрактном классе (или просто использую интерфейсы вместо этого), но это определенно зависит от ситуации. Если у вас есть интерфейс со многими методами, и вы хотите иметь возможность просто выбрать некоторые из них, например, то абстрактный класс, который реализует интерфейс в режиме no-op для каждого метода, прекрасен.
Вы должны оценивать каждый случай по существу, в основном.
Ответ 2
Похоже, вы обеспокоены настройкой этой логической переменной, когда происходит событие. Если пользователь переопределяет eventHasOccurred()
, тогда логическая переменная не будет установлена, а isFinished()
не вернет правильное значение. Для этого у вас может быть один абстрактный метод, который пользователь переопределяет для обработки события и другой метод, который вызывает абстрактный метод и задает логическое значение (см. Пример кода ниже).
Кроме того, вместо того, чтобы поместить метод eventHasOccurred()
в класс HaltingCondition
, вы можете просто иметь классы, которые должны обрабатывать события, расширять класс, который определяет этот метод (например, класс ниже). Любой класс, который не нуждается в обработке событий, может просто расширить HaltingCondition
:
public abstract class EventHaltingCondition extends HaltingCondition{
private boolean eventHasOccurred = false;
//child class implements this
//notice how it has protected access to ensure that the public eventHasOccurred() method is called
protected abstract void handleEvent(Event e);
//program calls this when the event happens
public final void eventHasOccurred(Event e){
eventHasOccurred = true; //boolean is set so that isFinished() returns the proper value
handleEvent(e); //child class' custom code is executed
}
@Override
public boolean isFinished(){
return eventHasOcccurred;
}
}
ИЗМЕНИТЬ (см. комментарии):
final EventHaltingCondition condition = new EventHaltingCondition(){
@Override
protected void handleEvent(Event e){
//...
}
};
JButton button = new JButton("click me");
button.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent actionEvent){
//runs when the button is clicked
Event event = //...
condition.eventHasOccurred(event);
}
});
Ответ 3
Если вы собираетесь применить любую реализацию в абстрактном базовом классе, это должен быть код для подклассов, которые используют реализацию no-op, поскольку это реализация, которая имеет смысл и для базового класса. Если для базового класса не было разумной реализации (например, если для метода, который вы обсуждаете здесь, не было разумного no-op), я бы предложил оставить его абстрактным.
Что касается дублированного кода, если существуют "семейства" классов, которые используют одну и ту же реализацию метода, и вы не хотите дублировать код по всем классам в семье, вы можете просто использовать вспомогательные классы в семьи, которые поставляют эти реализации. В вашем примере помощник для классов, которые пропускают события, помощник для классов принимает и записывает событие и т.д.
Ответ 4
Я столкнулся с похожим сценарием, когда создал основной контур (иерархию классов) приложения, которое я разрабатывал вместе с другими на работе. Мой выбор для размещения метода abstract (и, следовательно, для его принудительной реализации) был для целей связи.
В принципе, другие товарищи по команде каким-то образом явно реализовали этот метод и поэтому прежде всего отмечают его присутствие и второй согласны на то, что они возвращают туда, даже если это просто реализация по умолчанию.
Стандартные реализации в базовых классах часто пропускаются.
Ответ 5
Если вы оставляете абзац метода суперкласса, вам может понадобиться изучить интерфейс (не путать с интерфейсом). Поскольку интерфейс - это тот, который не обеспечивает конкретной реализации.
Скотт
Чтобы расширить, когда мы, как программисты, наставляем код на интерфейс, часто возникающий как новый, а иногда и опытный, разработчики ошибочно предполагают, что он относится к ключевому слову Interface, в котором не могут быть найдены детали реализации. Тем не менее, более ясный способ сказать это, что любой объект верхнего уровня можно рассматривать как интерфейс, с которым можно взаимодействовать. Например, абстрактным классом, называемым Animal, был бы интерфейс, называемый классом Cat, который наследует Animal.