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