Я не мог не согласиться больше. Иерархии классов имеют смысл для конкретных классов, когда конкретные классы знают возможные возвращаемые типы методов, которые они не пометили как финальные. Например, конкретный класс может иметь хук подкласса:
protected SomeType doSomething() {
return null;
}
Этот doSomething гарантированно будет иметь значение null или экземпляр SomeType. Предположим, у вас есть возможность обрабатывать экземпляр SomeType, но у вас нет сценария использования экземпляра SomeType в текущем классе, но вы знаете, что эта функциональность была бы действительно полезна в подклассах, и большинство всего конкретного. Нет смысла делать текущий класс абстрактным, если его можно использовать напрямую, по умолчанию ничего не делая со своим нулевым значением. Если бы вы сделали его абстрактным классом, то у вас были бы его потомки в иерархии этого типа:
- Абстрактный базовый класс
- Класс по умолчанию (класс, который мог быть неабстрактным, реализует только защищенный метод и ничего больше)
Таким образом, у вас есть абстрактный базовый класс, который нельзя использовать напрямую, когда класс по умолчанию может быть наиболее распространенным случаем. В другой иерархии существует на один класс меньше, так что функциональность может использоваться без создания по умолчанию бесполезного класса по умолчанию, потому что абстракцию просто нужно было навязать классу.
Теперь, конечно, иерархии можно использовать и злоупотреблять, и, если что-то не документировано четко или классы не разработаны должным образом, подклассы могут столкнуться с проблемами. Но те же проблемы существуют и с абстрактными классами, вы не избавляетесь от проблемы только потому, что добавляете «абстрактный» в свой класс. Например, если в контракте метода "doSomething ()", описанном выше, требуется, чтобы SomeType заполнил поля x, y и z, когда к ним обращались через методы получения и установки, ваш подкласс взорвался бы независимо от того, использовали ли вы конкретный класс, который возвратил ноль как ваш базовый класс или абстрактный класс.
Общее практическое правило для разработки иерархии классов - довольно простой вопрос:
Нужно ли мне поведение предложенного суперкласса в моем подклассе? (Y / N)
Это первый вопрос, который вам нужно задать себе. Если вам не нужно поведение, нет аргумента для подкласса.
Нужно ли мне состояние предложенного суперкласса в моем подклассе? (Y / N)
Это второй вопрос. Если состояние соответствует модели того, что вам нужно, это может быть кандидатом на создание подклассов.
Если подкласс был создан из предложенного суперкласса, действительно ли это будет отношение IS-A или это просто ярлык для наследования поведения и состояния?
Это последний вопрос. Если это просто ярлык, и вы не можете квалифицировать предложенный подкласс «как-а» суперкласс, тогда следует избегать наследования. Состояние и логику можно скопировать и вставить в новый класс с другим корнем или использовать делегирование.
Только если классу нужно поведение, состояние и можно считать, что экземпляр подкласса IS-A (n) суперкласса должен считаться наследником суперкласса. В противном случае существуют другие варианты, которые лучше подходят для этой цели, хотя для этого может потребоваться дополнительная работа, в долгосрочной перспективе это чище.