Краткий обзор по сравнению с Интерфейсом - разделение определения и implemention в Delphi

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

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

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

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

22
задан Jim McKeeth 17 February 2010 в 16:43
поделиться

6 ответов

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

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

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

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 на самом деле нет ничего общего, что можно было бы поместить в базовый класс. Технически вы можете Резать с помощью 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;

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

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

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

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


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

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

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

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

24
ответ дан 29 November 2019 в 04:40
поделиться

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

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

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

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

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

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

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

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

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

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

  • Если у вас есть разумная иерархия классов , вы можете использовать абстрактные классы для обеспечения единой точки доступа. для всех классов этой иерархии, с тем преимуществом, что вы даже можете реализовать поведение по умолчанию и тому подобное.
8
ответ дан 29 November 2019 в 04:40
поделиться

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

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

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

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

5
ответ дан 29 November 2019 в 04:40
поделиться

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

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

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

1
ответ дан 29 November 2019 в 04:40
поделиться

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

1
ответ дан 29 November 2019 в 04:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: