Никогда не делайте публичных участников виртуальными / абстрактными - правда?


20

Еще в 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?


5
Создание методов virtual допускает необязательное переопределение. Ваш метод, вероятно, должен быть публичным, потому что он не может быть переопределен. Создание методов abstract заставляет вас отвергать их; они, вероятно, должны быть protected, потому что они не особенно полезны в publicконтексте.
Роберт Харви

4
На самом деле, protectedэто наиболее полезно, когда вы хотите представить частные члены абстрактного класса производным классам. В любом случае, я не особенно обеспокоен мнением твоего друга; выберите модификатор доступа, наиболее подходящий для вашей конкретной ситуации.
Роберт Харви

4
Ваш коллега защищал шаблонный метод . Возможны варианты использования для обоих способов, в зависимости от того, насколько взаимозависимы оба метода.
Грег Бургхардт

8
@GregBurghardt: звучит так, как будто коллега ОП всегда предлагает использовать шаблонный шаблон, независимо от того, требуется он или нет. Это типичное злоупотребление паттернами - если у кого-то есть молоток, рано или поздно каждая проблема начинает выглядеть как гвоздь ;-)
Док Браун

3
@PeterPerot: у меня никогда не было проблем с тем, чтобы начать с простых DTO с простыми открытыми полями, и когда оказалось, что для такого DTO требуются члены с бизнес-логикой, реорганизуйте их в классы со свойствами. Конечно, все иначе, когда кто-то работает в качестве поставщика библиотеки и должен заботиться о том, чтобы не менять общедоступные API, тогда даже превращение публичного поля в общедоступное свойство с тем же именем может вызвать проблемы.
Док Браун

Ответы:


30

поговорка

что создание паттерна общедоступных методов виртуальным или абстрактным является препятствием, поскольку разработчик производного класса, который реализует Method1 и переопределяет Method2, должен повторить проверку аргумента

смешивает причину и следствие. Предполагается, что каждый переопределяемый метод требует ненастраиваемой проверки аргумента. Но все наоборот

Если кто-то хочет спроектировать метод таким образом, чтобы он обеспечивал некоторую фиксированную проверку аргументов во всех производных класса (или - в более общем случае - настраиваемую и не настраиваемую части), то имеет смысл сделать точку входа не виртуальной. и вместо этого предоставьте виртуальный или абстрактный метод для настраиваемой части, которая вызывается внутри.

Но есть много примеров, когда имеет смысл иметь общедоступный виртуальный метод, поскольку нет фиксированной ненастраиваемой части: посмотрите на стандартные методы, такие как ToString или Equalsили GetHashCode- улучшит ли дизайн objectкласса, чтобы они не были публичными и виртуальный одновременно? Я так не думаю.

Или с точки зрения вашего собственного кода: когда код в базовом классе окончательно и намеренно выглядит следующим образом

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

наличие такого разделения Method1и Method1Coreтолько усложняет вещи без видимой причины.


1
В случае этого ToString()метода Microsoft лучше сделала его не виртуальным и внедрила метод виртуальных шаблонов ToStringCore(). Почему: из-за этого: ToString()- обратите внимание на наследников . Они заявляют, что ToString()не должны возвращать ноль. Они могли бы принудить это требование, осуществляя ToString() => ToStringCore() ?? string.Empty.
Питер Перо

9
@PeterPerot: эта рекомендация, на которую вы ссылаетесь, также рекомендует не возвращаться string.Empty, вы заметили? И он рекомендует много других вещей, которые не могут быть реализованы в коде путем введения чего-то вроде ToStringCoreметода. Таким образом, эта техника, вероятно, не подходит для ToString.
Док Браун

3
@Theraot: конечно, можно найти некоторые причины или аргументы, почему ToString и Equals или GetHashcode могли бы быть разработаны по-разному, но сегодня они такие, как есть (по крайней мере, я думаю, что их дизайн достаточно хорош, чтобы подать хороший пример).
Док Браун

1
@PeterPerot "Я видел много кода, anyObjectIGot.ToString()а не anyObjectIGot?.ToString()" - как это актуально? Ваш ToStringCore()подход предотвратит возврат пустой строки, но он все равно выдаст, NullReferenceExceptionесли объект является нулевым.
IMil

1
@PeterPerot Я не пытался передать аргумент от власти. Это не так много, что Microsoft использует public virtual, но есть случаи, в которых public virtualвсе в порядке. Мы могли бы поспорить за пустую ненастраиваемую часть, как за то, чтобы сделать код будущим ... Но это не работает. Возвращение и изменение может привести к поломке производных типов. Таким образом, он ничего не зарабатывает.
Theraot

6

Выполнение так, как предлагает ваш коллега, обеспечивает большую гибкость разработчику базового класса. Но с этим также приходит больше сложности, которая обычно не оправдывается предполагаемыми выгодами.

Помните, что повышенная гибкость для реализации базового класса достигается за счет меньшего гибкости переопределяющей стороны. Они получают какое-то навязчивое поведение, которое им не особенно важно. Для них все стало более жестким. Это может быть оправданным и полезным, но все зависит от сценария.

Соглашение об именах для реализации этого (что я знаю) заключается в том, чтобы зарезервировать хорошее имя для открытого интерфейса и добавить к имени внутреннего метода префикс «Do».

Один полезный случай - когда выполняемое действие нуждается в настройке и закрытии. Как открыть поток и закрыть его после завершения переопределения. В общем, такая же инициализация и финализация. Это допустимый шаблон для использования, но было бы бессмысленно разрешать его использование во всех абстрактных и виртуальных сценариях.


1
Do Приставка метод является одним из вариантов. Microsoft часто использует основной метод postfix.
Питер Перо

@ Питер Перо. Я никогда не видел префикс Core в каких-либо материалах Microsoft, но это может быть потому, что я не обращал особого внимания в последнее время. Я подозреваю, что они начали делать это недавно, чтобы продвинуть прозвище Core, чтобы сделать название для .NET Core.
Мартин Маат

Нет, это старая шляпа BindingList. Кроме того, я где-то нашел рекомендации, может быть, их Руководства по проектированию фреймворка или что-то подобное. И: это постфикс . ;-)
Питер Перо

Меньшая гибкость для производного класса - это главное. Базовый класс является границей абстракции. Базовый класс сообщает потребителю, что делает его общедоступный API, и определяет, какой API требуется для достижения этих целей. Если производный класс может переопределить открытый метод в базовом классе, вы рискуете нарушить принцип подстановки Лискова.
Адриан Маккарти

2

В C ++ это называется не-виртуальным шаблоном интерфейса (NVI). (Когда-то его называли «Метод шаблонов». Это сбивало с толку, но некоторые из более старых статей имеют такую ​​терминологию.) NVI пропагандирует Херб Саттер, который писал об этом по крайней мере несколько раз. Я думаю, что один из самых ранних здесь .

Если я правильно помню, предпосылка заключается в том, что производный класс не должен изменять то, что делает базовый класс, а то, как он это делает.

Например, Shape может иметь метод Move для перемещения фигуры. Конкретные реализации (например, квадраты и круги) не должны напрямую переопределять Move, потому что Shape определяет, что означает Moving (на концептуальном уровне). Квадрат может иметь отличия в деталях реализации от Круга с точки зрения того, как позиция представлена ​​внутренне, поэтому им придется переопределить некоторый метод для обеспечения функциональности Move.

В простых примерах это часто сводится к общедоступному Move, который просто делегирует всю работу частному виртуальному ReallyDoTheMove, так что кажется, что это бесполезно.

Но это однозначное соответствие не является обязательным требованием. Например, вы можете добавить метод Animate к общедоступному API Shape, и он может реализовать это, вызывая ReallyDoTheMove в цикле. В итоге вы получите два открытых API невиртуальных методов, которые оба используют один закрытый абстрактный метод. Ваши Круги и Квадраты не должны выполнять никакой дополнительной работы, а также не могут переопределять Animate .

Базовый класс определяет открытый интерфейс, используемый потребителями, и он определяет интерфейс примитивных операций, который необходим для реализации этих открытых методов. Производные типы отвечают за обеспечение реализации этих примитивных операций.

Я не знаю каких-либо различий между C # и C ++, которые могли бы изменить этот аспект дизайна классов.


Хорошая находка! Теперь я помню, что нашел именно эту запись, на которую указывает ваша 2-я ссылка (или ее копию) еще в 2000-х годах. Я помню, что я искал больше доказательств утверждения моего коллеги, и что я не нашел ничего в контексте C #, кроме C ++. Эта. Является. Это! :-) Но, вернувшись в землю C #, кажется, что эта модель не используется много. Возможно, люди поняли, что добавление базовой функциональности позже может нарушить и производные классы, и что строгое использование TMP или NVIP вместо общедоступных виртуальных методов не всегда имеет смысл.
Питер Перо

Примечание для себя: хорошо знать, что этот шаблон имеет название: NVIP.
Питер Перо
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.