2010-02-17 3 views
20

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

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

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

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

+1

+1, очень хороший вопрос. – mghie

+0

Связанный вопрос: http://stackoverflow.com/questions/918380/abstract-classes-vs-interfaces-vs-mixins. Конкретный вопрос для Delphi кажется оправданным. – mghie

+0

Я знаю разницу, но на самом деле я чувствую, что чего-то не хватает :) Может быть, компромисс между ними? Что же касается нового объявления: TBase = abstract class ... end; Действует как интерфейс, но без подсчета ссылок. –

ответ

22

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

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

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

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; 

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

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

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

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


Таким образом, мораль этой истории такова:

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

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

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

3

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

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

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

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

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

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

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

Мне нравится это писать. Много деталей. Обратите внимание, что FPC имеет интерфейсы Corba для работы по проблеме «для 3», но на самом деле он никогда не конкретизируется. –

+0

+1 для простых интерфейсов. Будучи вынужденным реализовывать 15 методов интерфейса, когда вам нужно использовать только один из них, это не весело. –

8

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

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

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

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

+1. И если варианты использования перекрываются, конечно, вполне разумно иметь классы в интерфейсах реализации иерархии классов. – mghie

+0

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

+0

Чем больше вы начинаете делать TDD, тем больше вы используете интерфейсы. Интерфейсы позволяют упростить запись тестовых файлов. * Положительная композиция над наследованием * начинает иметь смысл. Мы переживаем разные фазы, пытаясь приобрести полное мастерство. Я нахожусь в * давайте сделаем все как интерфейс * фаза :) После этого может быть интересно прочитать http://bsix12.com/shu-ha-ri/ –

5

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

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

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

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

+0

Вывод из TComponent также отключит подсчет ссылок (на всякий случай, если вы ленивы). Недостатком, по сравнению с вашей собственной реализацией, является то, что он добавляет некоторые накладные расходы. – dummzeuch

1

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

1

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

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

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

+1

Было бы полезно, если бы вы расширили причины. Что было так больно в интерфейсах COM? – reinierpost

+0

Некоторые разработчики даже мечтают о кодовых строках, таких как ISmellVeryBadly = interface (...) :) – mjn

+0

Да, у меня аллергия на COM тоже. Возможно, это повлияло на DirectX, который отталкивает меня. –