Використовуйте абстрактний клас у C # як визначення


21

Як розробник C ++ я досить звик до файлів заголовків C ++, і вважаю вигідним мати якусь вимушену "документацію" всередині коду. Зазвичай у мене поганий час, коли мені доводиться читати якийсь код C # через це: у мене немає такої ментальної карти класу, з якою я працюю.

Припустимо, що як програмний інженер я розробляю рамки програми. Чи було б занадто божевільним визначати кожен клас як абстрактний безреалізований клас, аналогічно тому, що ми робимо із заголовками C ++, і дозволити розробникам його реалізовувати?

Я здогадуюсь, що можуть бути деякі причини, чому хтось може вважати це жахливим рішенням, але я не знаю, чому. Що потрібно було б розглянути для такого рішення?


13
C # - це зовсім інша мова програмування, ніж C ++. Деякий синтаксис виглядає схожим, але вам потрібно зрозуміти C #, щоб зробити в ньому хорошу роботу. І вам слід зробити щось щодо шкідливої ​​звички, покладаючись на файли заголовків. Я працював з C ++ десятиліттями, ніколи не читав заголовкові файли, лише писав їх.
Бент

17
Одна з найкращих речей C # і Java - це свобода від файлів заголовків! Просто використовуйте хороший IDE.
Ерік Ейдт

9
Декларації у файлах заголовків C ++ не входять до складу створеного двійкового файлу. Вони там для компілятора і лінкера. Ці C # абстрактні класи були б частиною згенерованого коду, без жодної користі.
Марк Беннінгфілд

16
Еквівалентна конструкція в C # - це інтерфейс, а не абстрактний клас.
Роберт Харві

6
@DaniBarcaCasafont "Я бачу заголовки як швидкий та надійний спосіб зрозуміти, що таке клас та методи та атрибути, які типи параметрів він очікує та що повертає". Під час використання Visual Studio моєю альтернативою є ярлик Ctrl-MO.
wha7ever

Відповіді:


44

Причина цього було зроблено в C ++, пов’язана з тим, щоб зробити компілятори швидшими та легшими в реалізації. Це не було дизайном, щоб полегшити програмування .

Мета файлів заголовка полягала в тому, щоб компілятор міг зробити дуже швидкий перший пропуск, щоб дізнатися всі очікувані назви функцій та виділити для них місця пам'яті, щоб вони могли бути посилаються при виклику у файлах cp, навіть якщо клас, що їх визначав, мав ще не проаналізовано.

Намагання копіювати наслідки старих обмежень на обладнання в сучасних умовах розвитку не рекомендується!

Визначення інтерфейсу або абстрактного класу для кожного класу знизить вашу продуктивність; що ще ви могли зробити з тим часом ? Також інші розробники не дотримуватимуться цієї конвенції.

Насправді інші розробники можуть видалити ваші абстрактні класи. Якщо я знайду інтерфейс у коді, який відповідає обом цим критеріям, я видаляю його та перетворюю його з коду:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

Інша справа, що у Visual Studio є інструменти, які роблять те, що ви прагнете досягти автоматично:

  1. The Class View
  2. The Object Browser
  3. У Solution Explorerви можете натиснути на трикутниках затратити класи , щоб побачити свої функції, параметри і повертаються тип.
  4. Під вкладками файлів є три маленьких випадаючих меню, найправіший - список усіх членів поточного класу.

Спробуйте сформулювати вище, перш ніж приділяти час реплікації файлів заголовків C ++ у C #.


Крім того, є технічні причини цього не робити ... це зробить ваш кінцевий бінарний файл більшим, ніж потрібно. Я повторю цей коментар Марка Беннінгфілда:

Декларації у файлах заголовків C ++ не входять до складу генерованих бінарних файлів. Вони там для компілятора і лінкера. Ці C # абстрактні класи були б частиною згенерованого коду, без жодної користі.


Також технічно згаданий Роберт Харві технічно найближчим еквівалентом заголовка в C # буде інтерфейс, а не абстрактний клас.


2
Ви також отримаєте багато непотрібних віртуальних диспетчерських дзвінків (непогано, якщо ваш код залежить від продуктивності).
Лукас Тшесневський

5
ось про який йдеться в принципі розбиття інтерфейсу .
NH.

2
Можна також просто згортати весь клас (клацнути правою кнопкою миші -> Контур -> Згорнутись до визначень), щоб отримати його з виду пташиного польоту.
jpmc26

"що ще ви могли зробити з тим часом" -> Ум, скопіюйте та вставте та дуже дійсно незначну поштову роботу? Займає у мене близько 3 секунд, якщо взагалі. Звичайно, я міг би використати цей час, щоб витягнути наконечник фільтру, готуючись до того, щоб накрутити сигарету. Не дуже важливий момент. [Відмова: Я люблю і ідіоматичний C #, і навіть ідіоматичний C ++, та ще деякі мови]
phresnel

1
@phresnel: Я б хотів, щоб ви спробували і повторили визначення класу та успадкували оригінальний клас із абстрактного в 3-х секундах, підтверджений без помилок, для будь-якого класу, який є достатньо великим, щоб не було легкого перегляду пташиного польоту це (як це є запропонованим випадком використання ОП: нібито не ускладнюючи інакше складні класи). Практично по суті, складний характер оригінального класу означає, що ви не можете просто написати абстрактне визначення класу напам’ять (адже це означатиме, що ви вже знаєте це напам’ять), а потім розгляньте час, необхідний для цього для цілої бази коду.
Flater

14

По-перше, зрозумійте, що суто абстрактний клас насправді є просто інтерфейсом, який не може зробити багатократне успадкування.

Клас запису, інтерфейс вилучення - це мозкова діяльність, що мертва. Настільки, що ми маємо для цього рефакторинг. Що шкода. Після цього "кожен клас отримує інтерфейс" шаблон не тільки створює безлад, він повністю пропускає крапку.

Інтерфейс не слід вважати просто формальним переглядом того, що може зробити клас. Інтерфейс слід розглядати як контракт, накладений за допомогою коду клієнта, що деталізує його потреби.

У мене взагалі не виникає проблем із написанням інтерфейсу, який наразі має лише один клас для його реалізації. Мені насправді байдуже, чи ще жоден клас не реалізує його. Тому що я думаю про те, що потрібно для мого використання коду. Інтерфейс виражає те, чого вимагає використання коду. Що пізніше відбуватиметься, може робити те, що йому подобається, якщо це задовольнить ці очікування.

Тепер я цього не роблю щоразу, коли один об’єкт використовує інший. Я роблю це при перетині кордону. Я це роблю, коли я не хочу, щоб один об’єкт точно знав, з яким іншим об’єктом йде мова. Це єдиний спосіб спрацювання поліморфізму. Я це роблю, коли очікую, що об'єкт, про який говорить мій клієнт, може змінитись. Я, звичайно, цього не роблю, коли я використовую клас String. Клас String хороший і стабільний, і я не відчуваю потреби в тому, щоб він мінявся на ньому.

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

Саме так є спосіб, яким я гартую принцип інверсії залежності . Не слід сліпо фанатично застосовувати це до всього. Додаючи абстракцію, ви дійсно говорите, що не довіряєте вибору класу-виконавця, щоб бути стабільним протягом життя проекту.

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

На це питання просто не знайдено мертвої відповіді. Інтерфейс чи абстрактний клас - це не те, що слід додати до кожного класу, і ви не можете просто підрахувати кількість класів, що реалізуються, і вирішити, що це не потрібно. Йдеться про боротьбу зі змінами. Що означає, що ви передчуваєте майбутнє. Не дивуйтеся, якщо ви помиляєтесь. Зробіть так просто, як не можете, не повертаючись у кут.

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


5

Так, це було б жахливо, тому що (1) Він вводить непотрібний код (2) Він бентежить читача.

Якщо ви хочете програмувати на C #, ви просто повинні звикнути читати C #. Код, написаний іншими людьми, в будь-якому випадку не піде за цією схемою.


1

Одиничні тести

Я настійно пропоную вам написати тести Unit, а не захаращувати код інтерфейсами або абстрактними класами (за винятком випадків, коли це передбачено з інших причин).

Добре написана одинична перевірка не тільки описує інтерфейс вашого класу (як, наприклад, файл заголовка, абстрактний клас чи інтерфейс), але також, наприклад, описує бажану функціональність.

Приклад: Де ви могли записати файл заголовка myclass.h, як це:

class MyClass
{
public:
  void foo();
};

Натомість у c # напишіть такий тест:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Цей дуже простий тест передає ту саму інформацію, що і файл заголовка. (У нас повинен бути клас під назвою "MyClass" з функцією без параметрів "Foo") Хоча файл заголовка більш компактний, тест містить набагато більше інформації.

Застереження: такий процес, коли старший інженер програмного забезпечення постачає (провалює) тести для інших розробників, щоб жорстоко вирішувати сутички з такими методологіями, як TDD, але у вашому випадку це було б величезне вдосконалення.


Як робити модульне тестування (ізольовані тести) = Необхідні макети, без інтерфейсів? Який фреймворк підтримує глузування з реальної реалізації, більшість із яких я бачив використовує інтерфейс для заміни програми на реалізацію макету.
Булан

Я не думаю, що макети необхідні для цілей ОП: s. Пам'ятайте, що це не одиничні тести як інструменти для TDD, а одиничні тести як заміна файлів заголовків. (Вони, звичайно, можуть перетворитись на «звичайні» одиничні тести з макетами тощо)
Гуран,

Гаразд, якщо я вважаю лише сторону ОП, це я краще розумію. Прочитайте свою відповідь як більш загальну застосовну відповідь. Thx для уточнення!
Булан

@Bulan необхідність широкого глузування часто свідчить про поганий дизайн
TheCatWhisperer

@TheCatWhisperer Так, я це добре знаю, тому жодного аргументу немає, я не бачу, що я і зробив це твердження де-небудь: DI говорив про тестування взагалі, що всі Mock-фреймворки, які я використовував, використовують Інтерфейси для обміну реальна реалізація, і якщо була якась інша техніка щодо того, як ви збиралися знущатися, якщо у вас немає інтерфейсів.
Булан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.