Абстрактные методы и принцип Open-Closed
Предположим, у меня есть следующий придуманный код:
abstract class Root
{
public abstract void PrintHierarchy();
}
class Level1 : Root
{
override public void PrintHierarchy()
{
Console.WriteLine("Level1 is a child of Root");
}
}
class Level2 : Level1
{
override public void PrintHierarchy()
{
Console.WriteLine("Level2 is a child of Level1");
base.PrintHierarchy();
}
}
Если я только смотрю на Level2
класс, я сразу вижу, что Level2.PrintHierarchy
следует принципу открытия / закрытия, потому что он что-то делает сам и вызывает базовый метод, который он переопределяет.
Однако, если я только смотрю на Level1
класс, кажется, что это нарушение OCP, потому что он не вызывает base.PrintHierarchy
- фактически, в C# компилятор запрещает это с ошибкой "Невозможно вызвать абстрактный базовый член".
Единственный способ сделать Level1
следовать OCP, чтобы изменить Root.PrintHierarchy
в пустой виртуальный метод, но тогда я больше не могу полагаться на компилятор для обеспечения реализации производных классов для реализации PrintHierarchy
,
Реальная проблема, с которой я сталкиваюсь при сохранении кода, - это десятки override
методы, которые не вызывают base.Whatever()
, Если base.Whatever
абстрактно, то хорошо, но если нет, то Whatever
Метод может быть кандидатом на включение в интерфейс, а не в конкретный переопределяемый метод, или же нужно реорганизовать класс или метод другим способом, но в любом случае это явно указывает на плохой дизайн.
Если не считать того, что запомнил Root.PrintHierarchy
это абстракция или комментарий внутри Level1.PrintHierarchy
, есть ли у меня другие варианты, чтобы быстро определить, был ли класс сформирован как Level1
нарушает OCP?
Было много хороших обсуждений в комментариях, и некоторые хорошие ответы тоже. Мне было трудно понять, что именно здесь спросить. Я думаю, что меня расстраивает то, что, как указывает @Jon Hanna, иногда виртуальный метод просто указывает "Вы должны реализовать меня", тогда как в других случаях это означает "вы должны расширить меня - если вы не можете вызвать базовую версию, вы сломай мой дизайн! Но C# не предлагает какого-либо способа указать, кто из тех, кого вы имеете в виду, кроме того, что абстрактно или интерфейс явно является "обязательной реализацией" ситуации. (Если в Code Contracts что-то не так, я думаю, что это немного выходит за рамки).
Но если бы у языка был декоратор, который должен быть реализован, а не должен расширяться, он, вероятно, создаст огромные проблемы для модульного тестирования, если его нельзя будет отключить. Есть ли такие языки? Это звучит скорее как дизайн по контракту, поэтому я не удивлюсь, если бы это было, например, в Eiffel.
Конечный результат, вероятно, как говорит @Jordão, и он полностью контекстуален; но я собираюсь на некоторое время оставить дискуссию открытой, прежде чем принимать какие-либо ответы.
3 ответа
Root
определяет следующее: корневые объекты имеют метод PrintHierarchy. Он ничего не определяет больше о методе PrintHierarchy, чем это.
Level1
имеет метод PrintHierarchy. Он не прекращает иметь метод PrintHierarchy, поэтому он никоим образом не нарушает принцип открытия / закрытия.
Теперь, ближе к делу: переименуйте вашу "PrintHierarchy" в "Foo". Уровень 2 следует или нарушает принцип открытого / закрытого?
Ответ в том, что мы понятия не имеем, потому что мы не знаем, какова семантика "Foo". Поэтому мы не знаем, должен ли base.Foo вызываться после остальной части тела метода, перед остальной частью, в середине или вообще не вызываться.
Должен 1.ToString()
верните "System.ObjectSystem.ValueType1" или "1System.ValueTypeSystem.Object", чтобы сохранить это притворство открытым / закрытым, или он должен назначить вызов base.ToString()
в неиспользуемую переменную перед возвратом "1"?
Очевидно, ничего из этого. Он должен возвращать как можно более значимую строку. Его базовые типы возвращают как можно более значимую строку и расширяют ее, что не приносит пользы от вызова ее базы.
Принцип открытия / закрытия означает, что когда я вызываю Foo(), я ожидаю, что произойдет некоторое Fooing, и я ожидаю некоторого соответствующего Fooing для Level1, когда я вызываю его на Level1, и некоторого соответствующего Fooing для Level2, когда я вызываю его на Level2. То, должен ли Level2 Fooing включать некоторый уровень Level1 Fooing, также зависит от того, что такое Fooing.
base
это инструмент, который помогает нам в расширении классов, а не требование.
Вы не можете решить, следует ли системе (или классу) следовать за OCP, просто взглянув на нее статически, без контекстной информации.
Только если вы знаете, каковы вероятные изменения в дизайне, вы можете судить, следует ли он OCP для этих конкретных изменений.
Там нет языковой конструкции, которая может помочь вам в этом.
Хорошей эвристикой было бы использование некоторой метрики, такой как нестабильность и абстрактность Роберта Мартина (pdf), или метрик из-за отсутствия сплоченности, чтобы лучше подготовить ваши системы к изменениям и иметь больше шансов следовать OCP и всем остальным важные принципы ООД.
OCP - это все о предварительных условиях и постусловиях: "когда вы можете заменить его предварительное условие более слабым, а его постусловие более сильным". Я не думаю, что вызов base в переопределенных методах нарушает его.