Используйте абстрактный класс в 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. Class View
  2. Object Browser
  3. В Solution Explorerвы можете нажать на треугольники, чтобы расходовать классы, чтобы увидеть их функции, параметры и типы возвращаемых данных.
  4. Под вкладками файлов есть три маленьких выпадающих меню, в самом правом из которых перечислены все члены текущего класса.

Попробуйте один из вышеперечисленных, прежде чем уделять время репликации заголовочных файлов C ++ в C #.


Кроме того, есть технические причины не делать этого ... это сделает ваш окончательный двоичный файл больше, чем нужно. Я повторю этот комментарий Марка Беннингфилда:

Объявления в заголовочных файлах C ++ не становятся частью сгенерированного двоичного файла. Они там для компилятора и компоновщика. Эти абстрактные классы C # будут частью сгенерированного кода, без какой-либо выгоды.


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

TheCatWhisperer
источник
2
Вы также получите множество ненужных виртуальных диспетчерских вызовов (не очень хорошо, если ваш код чувствителен к производительности).
Лукас Тресневский
2
Можно также просто свернуть весь класс (щелкнуть правой кнопкой мыши -> Outlining -> Collapse to Definitions), чтобы увидеть его с высоты птичьего полета.
jpmc26
«что еще вы могли бы сделать с этим временем» -> Хм, скопируйте и вставьте и очень незначительную пост-работу? Занимает у меня около 3 секунд, если вообще. Конечно, я мог бы использовать это время, чтобы вытащить наконечник фильтра, чтобы приготовить сигарету. Не совсем актуальный момент. [Отказ от ответственности: я люблю идиоматический C #, а также идиоматический C ++ и некоторые другие языки]
phresnel
1
@phresnel: Хотелось бы, чтобы вы попробовали повторить определения класса и унаследовать исходный класс от абстрактного в 3 с, подтвержденного без ошибок, для любого класса, который достаточно велик, чтобы не иметь простого обзора с высоты птичьего полета это (так как это предложенный вариант использования OP: предположительно несложный в противном случае сложные классы). Почти по своей природе сложная природа исходного класса означает, что вы не можете просто написать определение абстрактного класса наизусть (потому что это будет означать, что вы уже знаете его наизусть), а затем подумать, сколько времени потребуется, чтобы сделать это для всей кодовой базы.
Флэтер
14

Во-первых, следует понимать, что чисто абстрактный класс - это просто интерфейс, который не может выполнять множественное наследование.

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

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

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

Теперь я не делаю это каждый раз, когда один объект использует другой. Я делаю это при пересечении границы. Я делаю это, когда не хочу, чтобы один объект точно знал, с каким другим объектом он разговаривает. Это единственный способ, которым полиморфизм будет работать. Я делаю это, когда ожидаю, что объект, о котором говорит мой клиентский код, может измениться. Я, конечно, не делаю этого, когда я использую класс String. Класс String хорош и стабилен, и я не чувствую необходимости защищать его от меня.

Когда вы решаете напрямую взаимодействовать с конкретной реализацией, а не через абстракцию, вы предсказываете, что реализация достаточно стабильна, чтобы доверять, чтобы не изменяться.

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

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

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

Поэтому, пожалуйста, не пишите абстракции только для того, чтобы помочь нам прочитать код. У нас есть инструменты для этого. Используйте абстракции, чтобы отделить то, что нужно отделить.

candied_orange
источник
5

Да, это было бы ужасно, потому что (1) Он вводит ненужный код (2) Это сбивает с толку читателя.

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

JacquesB
источник
1

Модульные тесты

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

Хорошо написанный модульный тест описывает не только интерфейс вашего класса (как это сделал бы заголовочный файл, абстрактный класс или интерфейс), но и, например, желаемую функциональность.

Пример: где вы могли бы написать заголовочный файл 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, но в вашем случае это было бы огромным улучшением.

Guran
источник
Как вы проводите модульное тестирование (изолированные тесты) = нужны макеты, без интерфейсов? Какая инфраструктура поддерживает макет из реальной реализации, большинство из тех, что я видел, использует интерфейс для замены реализации макетом.
Булан
Я не думаю, что издевательства обязательно нужны для целей ОП. Помните, что это не юнит-тесты как инструменты для TDD, а юнит-тесты как замена заголовочных файлов. (Конечно, они могут превратиться в «обычные» юнит-тесты с макетами и т. Д.)
Гуран
Хорошо, если я рассматриваю только OPs, я понимаю лучше. Прочитайте ваш ответ как более общий ответ. Спасибо за разъяснение!
Булан
@ Булан: необходимость обширных издевательств часто свидетельствует о плохом дизайне
TheCatWhisperer
@TheCatWhisperer Да, я хорошо знаю об этом, поэтому никаких аргументов нет, я не могу видеть, что я сделал это заявление в любом месте: DI говорил о тестировании в целом, что все Mock-фреймворки, которые я использовал, используют интерфейсы для обмена фактическую реализацию, и если был какой-то другой способ, как вы собираетесь издеваться, если у вас нет интерфейсов.
Булан