Подтвердить что ты не робот

Абстракция против интерфейса - разделение определения и реализации в Delphi

Каков наилучший подход для разделения определения и реализации с использованием интерфейсов или абстрактных классов?

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

Но иногда мне нужно было бы получить класс из 2 или более классов/интерфейсов.

Каков ваш опыт?

4b9b3361

Ответ 1

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

  • Наследование класса отвечает на вопрос: "Какой это объект?"
  • Выполнение интерфейса отвечает на вопрос: "Что я могу сделать с этим объектом?"

Скажем, вы моделируете кухню. (Извиняюсь заранее за следующие пищевые аналогии, я только что вернулся с обеда...) У вас есть три основных типа посуды - вилки, ножи и ложки. Все они подпадают под категорию посуды, поэтому мы будем моделировать это (я пропускаю некоторые скучные вещи, такие как поля поддержки):

type
    TMaterial = (mtPlastic, mtSteel, mtSilver);

    TUtensil = class
    public
        function GetWeight : Integer; virtual; abstract;
        procedure Wash; virtual; // Yes, it self-cleaning
    published
        property Material : TMaterial read FMaterial write FMaterial;
    end;

Все это описывает данные и функциональные возможности, общие для любой посуды: от того, из чего он весит (что зависит от конкретного типа) и т.д. Но вы заметите, что абстрактный класс на самом деле ничего не делает. A TFork и TKnife на самом деле не имеют гораздо больше общего, чем вы можете поместить в базовый класс. Вы можете технически Cut с TFork, но a TSpoon может быть растянутым, поэтому как отразить тот факт, что только некоторые приборы могут делать определенные вещи?

Ну, мы можем начать расширять иерархию, но она становится беспорядочной:

type
    TSharpUtensil = class
    public
        procedure Cut(food : TFood); virtual; abstract;
    end;

Это касается острых, но что, если мы хотим сгруппировать этот путь вместо этого?

type
    TLiftingUtensil = class
    public
        procedure Lift(food : TFood); virtual; abstract;
    end;

TFork и TKnife оба подпадают под TSharpUtensil, но TKnife довольно паршиво для поднятия куска цыпленка. Мы в конечном итоге либо должны выбрать одну из этих иерархий, либо просто перетащить все эти функции в общий TUtensil, а производные классы просто отказываются реализовывать методы, которые не имеют смысла. Дизайн-мудрый, это не та ситуация, в которой мы хотим застрять.

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

type
    IPointy = interface
        procedure Pierce(food : TFood);
    end;

    IScoop = interface
        procedure Scoop(food : TFood);
    end;

Теперь мы можем разобраться, что делают конкретные типы:

type
    TFork = class(TUtensil, IPointy, IScoop)
        ...
    end;

    TKnife = class(TUtensil, IPointy)
        ...
    end;

    TSpoon = class(TUtensil, IScoop)
        ...
    end;

    TSkewer = class(TStick, IPointy)
        ...
    end;

    TShovel = class(TGardenTool, IScoop)
        ...
    end;

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

Независимо от того, хотите ли вы использовать абстрактный класс или один или несколько интерфейсов ниже по течению, действительно зависит от того, что вам нужно сделать с ним:

type
    TDishwasher = class
        procedure Wash(utensils : Array of TUtensil);
    end;

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

С другой стороны:

type
    THungryMan = class
        procedure EatChicken(food : TFood; utensil : TUtensil);
    end;

Это может быть не так хорошо. Он не может есть всего лишь TKnife (ну, не легко). И требуя как a TFork, так и TKnife тоже не имеет смысла; что, если это куриное крыло?

Это имеет больший смысл:

type
    THungryMan = class
        procedure EatPudding(food : TFood; scoop : IScoop);
    end;

Теперь мы можем дать ему либо TFork, TSpoon, либо TShovel, и он счастлив, но не TKnife, который по-прежнему является утилитой, но на самом деле не помогает здесь.

Вы также заметите, что вторая версия менее чувствительна к изменениям в иерархии классов. Если мы решили изменить TFork наследовать от TWeapon, наш человек все еще счастлив, пока он все еще реализует IScoop.


У меня также есть глянцевая проблема, связанная с подсчетом ссылок, и я думаю, что @Deltics сказал, что это лучше всего; просто потому, что у вас есть AddRef, это не значит, что вам нужно сделать то же самое с тем, что делает TInterfacedObject. Сопоставление ссылок на интерфейс - это своего рода случайная функция, это полезный инструмент для тех времен, когда он вам нужен, но если вы собираетесь смешивать интерфейс с семантикой класса (и очень часто вы это делаете), это не всегда делает смысл использовать функцию подсчета ссылок в качестве формы управления памятью.

На самом деле, я бы сказал, что большую часть времени вы, вероятно, не хотите использовать семантику подсчета ссылок. Да, я это сказал. Я всегда чувствовал, что вся работа по пересчету только для поддержки автоматизации OLE и т.д. (IDispatch). Если у вас нет оснований для автоматического разрушения вашего интерфейса, просто забудьте об этом, не используйте TInterfacedObject вообще. Вы всегда можете изменить его, когда вам это нужно, - что точка использования интерфейса! Подумайте о интерфейсах с высокоуровневой проектной точки зрения, а не с точки зрения управления памятью/временем жизни.


Итак, мораль этой истории:

  • Если вам требуется объект для поддержки определенных функций, попробуйте использовать интерфейс.

  • Когда объекты одного семейства и вы хотите, чтобы они имели общие функции, наследуйте от общего базового класса.

  • И если применяются обе ситуации, используйте оба параметра:

Ответ 2

Я сомневаюсь, что это вопрос "лучшего подхода" - у них просто разные варианты использования.

  • Если у вас нет иерархии классов, и вы не хотите его строить, и даже не имеет смысла принуждать несвязанные классы к одной и той же иерархии - но вы хотите, чтобы некоторые классы были равны в любом случае, не зная конкретного имени класса →

    Интерфейсы - это путь (подумайте о Javas Comparable или Iterateable, например, если вам нужно будет извлечь из этих классов (при условии, что они были классами =), они были бы совершенно бесполезны.

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

Ответ 3

У вас могут быть интерфейсы без подсчета ссылок. Компилятор добавляет вызовы AddRef и Release для всех интерфейсов, но аспект управления жизненным циклом этих объектов полностью связан с реализацией IUnknown.

Если вы выйдете из TInterfacedObject, то время жизни объекта действительно будет подсчитано, но если вы выведете свой собственный класс из TObject и реализуете IUnknown без фактического подсчета ссылок и без освобождения "я" в реализации Release, вы получите базу класс, который поддерживает интерфейсы, но имеет явно управляемый ресурс как обычно.

Вам все равно нужно быть осторожным с этими ссылками интерфейса из-за автоматически сгенерированных вызовов AddRef() и Release(), введенных компилятором, но это действительно не сильно отличается от того, чтобы быть осторожным с "оборванными ссылками" на обычный TObject.

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

Ответ 4

В Delphi существует три способа отделить определение от реализации.

  • У вас есть разделение в каждом подразделении, где вы можете поместить классы publuc в разделе интерфейса и его реализацию в разделе реализации. Код по-прежнему находится в одном устройстве, но, по крайней мере, "пользователь" вашего кода должен только читать интерфейс, а не мужество реализации.

  • При использовании виртуальных или динамически объявленных функций в вашем классе вы можете переопределить подклассы. Это способ использования большинства библиотек классов. Посмотрите на TStream, и он получил такие классы, как THandleStream, TFileStream и т.д.

  • Вы можете использовать интерфейсы, когда вам нужна другая иерархия, а не только вывод класса. Интерфейсы всегда выводятся из IInterface, которая смоделирована на основе IUnknown на основе COM: вы получаете информацию о подсчете ссылок и типах запросов, которые продвигаются вместе с ним.

Для 3: - Если вы выходите из TInterfacedObject, подсчет ссылок действительно заботится о времени жизни ваших объектов, но это не так. Например, TComponent также реализует IInterface, но без учета ссылок. Это связано с предупреждением BIG: убедитесь, что ваши ссылки на интерфейс установлены на нуль, прежде чем уничтожить ваш объект. Компилятор будет по-прежнему вставлять декреты на ваш интерфейс, который выглядит по-прежнему действительным, но не является. Во-вторых: люди не ожидают такого поведения.

Выбор между 2 и 3 иногда весьма субъективен. Я имею тенденцию использовать следующее:

  • Если возможно, используйте виртуальные и динамические и переопределите их в производных классах.
  • При работе с интерфейсами: создайте базовый класс, который принимает ссылку на экземпляр interfcae как переменную и максимально упростит ваши интерфейсы; для каждого аспекта попытайтесь создать отдельную переменную intercae. Попытайтесь использовать реализацию по умолчанию, если не указан интерфейс.
  • Если вышеуказанное слишком ограничивает: начните использовать TInterfacedObject-s и действительно обратите внимание на возможные циклы и, следовательно, на утечки памяти.

Ответ 5

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

Ответ 6

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

Я знаю, что некоторые люди будут считать мое избегание интерфейсов слабым. Но я думаю, что весь код Delphi с использованием интерфейсов имеет своего рода "запах кода".

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