Еще в 2000-х мой коллега сказал мне, что делать публичные методы виртуальными или абстрактными - это нехорошо.
Например, он считал такой класс не очень хорошо спроектированным:
public abstract class PublicAbstractOrVirtual
{
public abstract void Method1(string argument);
public virtual void Method2(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
// default implementation
}
}
Он заявил, что
- разработчик производного класса, который реализует
Method1
и переопределяет,Method2
должен повторить проверку аргумента. - в случае, если разработчик базового класса решит добавить что-то вокруг настраиваемой части
Method1
илиMethod2
позже, он не сможет это сделать.
Вместо этого мой коллега предложил этот подход:
public abstract class ProtectedAbstractOrVirtual
{
public void Method1(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
this.Method1Core(argument);
}
public void Method2(string argument)
{
if (argument == null) throw new ArgumentNullException(nameof(argument));
this.Method2Core(argument);
}
protected abstract void Method1Core(string argument);
protected virtual void Method2Core(string argument)
{
// default implementation
}
}
Он сказал мне, что делать открытые методы (или свойства) виртуальными или абстрактными - это так же плохо, как делать открытыми поля. Оборачивая поля в свойства, можно перехватить любой доступ к этим полям позже, если это необходимо. То же самое относится к общедоступным виртуальным / абстрактным членам: их обтекание, как показано в ProtectedAbstractOrVirtual
классе, позволяет разработчику базового класса перехватывать любые вызовы, поступающие в виртуальные / абстрактные методы.
Но я не рассматриваю это как руководство по дизайну. Даже Microsoft не следит за этим: просто посмотрите на Stream
класс, чтобы убедиться в этом.
Что вы думаете об этом руководстве? Это имеет какой-то смысл, или вы думаете, что это слишком усложняет API?
virtual
допускает необязательное переопределение. Ваш метод, вероятно, должен быть публичным, потому что он не может быть переопределен. Создание методовabstract
заставляет вас отвергать их; они, вероятно, должны бытьprotected
, потому что они не особенно полезны вpublic
контексте.protected
это наиболее полезно, когда вы хотите представить частные члены абстрактного класса производным классам. В любом случае, я не особенно обеспокоен мнением твоего друга; выберите модификатор доступа, наиболее подходящий для вашей конкретной ситуации.Ответы:
поговорка
смешивает причину и следствие. Предполагается, что каждый переопределяемый метод требует ненастраиваемой проверки аргумента. Но все наоборот
Если кто-то хочет спроектировать метод таким образом, чтобы он обеспечивал некоторую фиксированную проверку аргументов во всех производных класса (или - в более общем случае - настраиваемую и не настраиваемую части), то имеет смысл сделать точку входа не виртуальной. и вместо этого предоставьте виртуальный или абстрактный метод для настраиваемой части, которая вызывается внутри.
Но есть много примеров, когда имеет смысл иметь общедоступный виртуальный метод, поскольку нет фиксированной ненастраиваемой части: посмотрите на стандартные методы, такие как
ToString
илиEquals
илиGetHashCode
- улучшит ли дизайнobject
класса, чтобы они не были публичными и виртуальный одновременно? Я так не думаю.Или с точки зрения вашего собственного кода: когда код в базовом классе окончательно и намеренно выглядит следующим образом
наличие такого разделения
Method1
иMethod1Core
только усложняет вещи без видимой причины.источник
ToString()
метода Microsoft лучше сделала его не виртуальным и внедрила метод виртуальных шаблоновToStringCore()
. Почему: из-за этого:ToString()
- обратите внимание на наследников . Они заявляют, чтоToString()
не должны возвращать ноль. Они могли бы принудить это требование, осуществляяToString() => ToStringCore() ?? string.Empty
.string.Empty
, вы заметили? И он рекомендует много других вещей, которые не могут быть реализованы в коде путем введения чего-то вродеToStringCore
метода. Таким образом, эта техника, вероятно, не подходит дляToString
.anyObjectIGot.ToString()
а неanyObjectIGot?.ToString()
" - как это актуально? ВашToStringCore()
подход предотвратит возврат пустой строки, но он все равно выдаст,NullReferenceException
если объект является нулевым.public virtual
, но есть случаи, в которыхpublic virtual
все в порядке. Мы могли бы поспорить за пустую ненастраиваемую часть, как за то, чтобы сделать код будущим ... Но это не работает. Возвращение и изменение может привести к поломке производных типов. Таким образом, он ничего не зарабатывает.Выполнение так, как предлагает ваш коллега, обеспечивает большую гибкость разработчику базового класса. Но с этим также приходит больше сложности, которая обычно не оправдывается предполагаемыми выгодами.
Помните, что повышенная гибкость для реализации базового класса достигается за счет меньшего гибкости переопределяющей стороны. Они получают какое-то навязчивое поведение, которое им не особенно важно. Для них все стало более жестким. Это может быть оправданным и полезным, но все зависит от сценария.
Соглашение об именах для реализации этого (что я знаю) заключается в том, чтобы зарезервировать хорошее имя для открытого интерфейса и добавить к имени внутреннего метода префикс «Do».
Один полезный случай - когда выполняемое действие нуждается в настройке и закрытии. Как открыть поток и закрыть его после завершения переопределения. В общем, такая же инициализация и финализация. Это допустимый шаблон для использования, но было бы бессмысленно разрешать его использование во всех абстрактных и виртуальных сценариях.
источник
BindingList
. Кроме того, я где-то нашел рекомендации, может быть, их Руководства по проектированию фреймворка или что-то подобное. И: это постфикс . ;-)В C ++ это называется не-виртуальным шаблоном интерфейса (NVI). (Когда-то его называли «Метод шаблонов». Это сбивало с толку, но некоторые из более старых статей имеют такую терминологию.) NVI пропагандирует Херб Саттер, который писал об этом по крайней мере несколько раз. Я думаю, что один из самых ранних здесь .
Если я правильно помню, предпосылка заключается в том, что производный класс не должен изменять то, что делает базовый класс, а то, как он это делает.
Например, Shape может иметь метод Move для перемещения фигуры. Конкретные реализации (например, квадраты и круги) не должны напрямую переопределять Move, потому что Shape определяет, что означает Moving (на концептуальном уровне). Квадрат может иметь отличия в деталях реализации от Круга с точки зрения того, как позиция представлена внутренне, поэтому им придется переопределить некоторый метод для обеспечения функциональности Move.
В простых примерах это часто сводится к общедоступному Move, который просто делегирует всю работу частному виртуальному ReallyDoTheMove, так что кажется, что это бесполезно.
Но это однозначное соответствие не является обязательным требованием. Например, вы можете добавить метод Animate к общедоступному API Shape, и он может реализовать это, вызывая ReallyDoTheMove в цикле. В итоге вы получите два открытых API невиртуальных методов, которые оба используют один закрытый абстрактный метод. Ваши Круги и Квадраты не должны выполнять никакой дополнительной работы, а также не могут переопределять Animate .
Базовый класс определяет открытый интерфейс, используемый потребителями, и он определяет интерфейс примитивных операций, который необходим для реализации этих открытых методов. Производные типы отвечают за обеспечение реализации этих примитивных операций.
Я не знаю каких-либо различий между C # и C ++, которые могли бы изменить этот аспект дизайна классов.
источник