Аннотация против интерфейса - разделение определения и реализации в Delphi - PullRequest
22 голосов
/ 17 февраля 2010

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

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

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

Какой у тебя опыт?

Ответы [ 6 ]

24 голосов
/ 17 февраля 2010

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

  • Наследование классов отвечает на вопрос: «Что это за объект?»
  • Реализация интерфейса отвечает на вопрос: «Что я могу сделать с этим объектом?»

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

type
    TMaterial = (mtPlastic, mtSteel, mtSilver);

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

Все это описывает данные и функциональные возможности, общие для любой посуды - из чего она состоит, что она весит (зависит от конкретного типа) и т. Д. Но вы заметите, что абстрактный класс на самом деле не делает что угодно. У TFork и TKnife на самом деле не так много общего, что вы можете поместить в базовый класс. Технически вы можете Cut с TFork, но 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;

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

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

* * 1068

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

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

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

Это может быть не так хорошо. Он не может есть только с TKnife (ну, не легко). И требовать как 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. Вы всегда можете изменить его, когда вам это нужно - это смысл использования интерфейса! Подумайте об интерфейсах с точки зрения проектирования высокого уровня, а не с точки зрения управления памятью и временем жизни.


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

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

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

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

8 голосов
/ 17 февраля 2010

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

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

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

  • Если у вас имеется разумный класс hiearchy , вы можете использовать абстрактные классы для предоставления единой точки доступа ко всем классам этой иерархии, с тем преимуществом, что вы даже сможете реализовать поведение по умолчанию и тому подобное.
5 голосов
/ 17 февраля 2010

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

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

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

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

3 голосов
/ 17 февраля 2010

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

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

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

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

Для 3: - Если вы наследуете от TInterfacedObject, подсчет ссылок действительно заботится о времени жизни ваших объектов, но это не так. - TComponent, например, также реализует интерфейс II, но БЕЗ подсчета ссылок. Это сопровождается БОЛЬШИМ предупреждением: убедитесь, что ссылки на интерфейс установлены в ноль, прежде чем уничтожать ваш объект. Компилятор все равно будет вставлять вызовы decf в ваш интерфейс, который выглядит все еще действительным, но это не так. Второе: люди не ожидают такого поведения.

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

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

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

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

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

1 голос
/ 17 февраля 2010

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

...