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


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: звучить так, як колега ОП пропонує завжди використовувати шаблон шаблону, незалежно від того, потрібен він чи ні. Це типова надмірність використання візерунків - якщо у вас молоток, рано чи пізно кожна проблема починає виглядати як цвях ;-)
Doc Brown

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()підхід перешкоджатиме поверненню нульового рядка, але він все одно буде кидати a, NullReferenceExceptionякщо об'єкт є null.
Іміль

1
@PeterPerot Я не намагався передати аргумент від авторитету. Microsoft не стільки використовує Microsoft public virtual, а є випадки, коли public virtualце нормально. Ми можемо стверджувати, що порожня, що не налаштовується, як майбутній доказ коду ... Але це не працює. Якщо повернутися назад і змінити його, можна порушити похідні типи. Таким чином, вона нічого не заробляє.
Тераот

6

Робити це так, як пропонує ваш колега, забезпечує більшу гнучкість для виконавця базового класу. Але разом із цим також виникає більш висока складність, яка, як правило, не виправдана припущеними перевагами.

Зверніть увагу, що підвищена гнучкість для реалізатора базового класу відбувається за рахунок меншої гнучкості для переважаючої сторони. Вони отримують певну поведінку, про яку вони можуть не особливо піклуватися. До них речі тільки стали більш жорсткими. Це може бути виправдано і корисно, але все це залежить від сценарію.

Конвенція про іменування для реалізації цього (про що я знаю) полягає в тому, щоб зарезервувати гарне ім'я для загальнодоступного інтерфейсу та встановити ім'я внутрішнього методу на "Do".

Один корисний випадок, коли виконувана дія потребує певного налаштування та закриття. Як відкрити потік і закрити його після того, як буде зроблено переопределитель. Загалом, такий самий варіант ініціалізації та доопрацювання. Це дійсна модель, але використовувати її в усіх абстрактних та віртуальних сценаріях було б безглуздо.


1
Do Приставка метод є одним з варіантів. Microsoft часто використовує постфікс методу Core .
Петро Перо

@ Петер Перо. Я жодного разу не бачив префікса 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 невіртуальних методів, які обидва покладаються на один приватний абстрактний метод. Ваші кола та квадрати не потребують додаткових робіт, а також не можуть переосмислювати анімацію .

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

Я не знаю жодних відмінностей між C # і C ++, які змінили б цей аспект дизайну класу.


Гарна знахідка! Тепер я пам’ятаю, що саме у 2000-му я знайшов саме цей пост свого 2-го посилання (або його копію). Я пам’ятаю, що шукав більше доказів претензії свого колеги, і що я не знайшов нічого в контексті C #, окрім C ++. Це. Є. Це! :-) Але, повертаючись у C # землю, схоже, ця модель використовується не так сильно. Можливо, люди зрозуміли, що додавання базової функціональності пізніше може також порушити похідні класи і що суворе використання TMP або NVIP замість публічних віртуальних методів не завжди має сенс.
Петро Перо

Зауважте для себе: добре знати, що цей шаблон має назву: NVIP.
Петро Перо
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.