6 февраля 2013 г.

"Дружественность" в Delphi

Небольшое введение в то, как обстоят дела с "дружественностью" в Delphi.

Содержание


Инкапсуляция

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

В ООП языках программирования инкапсуляция реализуется через механизмы ограничения доступа к членам класса путём введения ключевых слов типа private или protected. Общая идея состоит в том, что каждый класс выставляет наружу публичный (public) интерфейс, который и позволяет управлять объектом. Все открытые методы интерфейса объекта гарантировано переводят объект из одного согласованного и корректного состояния в другое. При этом внутренние "кишки" объекта скрыты от внешнего доступа, так что никакой сторонний код не может перевести объект из согласованного состояния в недопустимое. Однако применение директив вида protected позволяет наследникам класса получать доступ к его внутренним членам.

Необходимость обхода инкапсуляции

Но объекты не существуют автономно, сами по себе. Поэтому на практике часто возникают ситуации, когда необходимо обеспечить более тесное взаимодействие объектов, не связанных иерархией. Это подразумевает, что один объект должен уметь обращаться к другому объекту в обход его публичного интерфейса. Простой пример: объект-коллекция и объект-элемент коллекции. Эти два класса, скорее всего, не будут наследоваться друг от друга, а будут двумя разными ветвями дерева наследования. Тем не менее, при добавлении элемента в коллекцию нужно установить владельца (для элемента), его индекс и другие данные для учёта. Само собой, подобный сервис не должен выставляться наружу, поскольку тогда сторонний код мог бы его менять и, таким образом, "испортить" данные объекта. Так что публичный интерфейс будет содержать методы типа "добавить элемент в коллекцию", но не "теперь этот объект — в той коллекции, а его индекс там — минус сто".

Например:
type
  TCollection = class;

  TCollectionItem = class(TPersistent)
  private
    FCollection: TCollection;
    ...
  public
    ...
    property Collection: TCollection read FCollection write SetCollection;
    ...
  end;

  TCollection = class(TPersistent)
  private
    FItems: TList;
    ...
  public
    ...
    function Add: TCollectionItem;
    ...
  end;
Здесь TCollection — коллекция, а TCollectionItem — элемент коллекции. Оба класса наследуются от общего предка TPersistent, но не друг от друга. И TCollection и TCollectionItem предоставляют открытый интерфейс для управления: метод добавления (Add) и получения информации (свойство Collection), но не позволяют менять их на произвольные значения стороннему коду (закрытые поля FCollection и FItems).

Разумеется, при добавлении элемента в коллекцию, необходимо внести элемент в список элементов (FItems) и назначить ему владельца (FCollection), поэтому этим двум классам необходимо иметь доступ к закрытым элементам друг друга в обход общего механизма разграничения доступа.

Понятие класса, дружественного другому классу

По этой причине во многих языках существует понятие класса, дружественного (friend) к другому классу (в некоторых языках дружественными могут быть и функции). Дружественный класс будет иметь полный доступ к членам другого класса, даже если они закрыты для прочего стороннего кода.

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

Поэтому в примере выше TCollection и TCollectionItem являются дружественными, поскольку расположены в одном модуле, так что допускается код типа такого:
function TCollection.Add: TCollectionItem;
begin
  Result := FItemClass.Create(Self);

  SetLength(FItems, Length(FItems) + 1);
  FItems[High(FItems)] := Result;

  Result.FCollection := Self; // <- обращение к закрытому полю
end;
или такого:
constructor TCollectionItem.Create(Collection: TCollection);
begin
  inherited Create;

  FCollection := Collection;

  FCollection.InsertItem(Self); // <- обращение к закрытому методу
end;

Проблемы дружественности в Delphi

Таким образом, получается, что деление классов на дружественные друг другу заключается в распределении их (классов) по модулям (unit). Однако на практике критерий дружественности крайне редко выступает в качестве основного критерия для распределения классов. Чаще модуль объединяет логические связанные классы, цельные компоненты библиотеки и т.п. При этом может получаться, как много не связанных друг с другом (в плане дружественности) классов в одном модуле, так и дружественные классы, разнесённые по разным модулям (если такие классы принадлежат разным логическим компонентам).

Сужение дружественности: strict

Понимая, что не всякий класс в модуле должен быть дружественным к другим классам этого же модуля, разработчики Delphi ввели новое ключевое слово strict, которое в комбинации с private и protected позволяют ограничивать доступ к членам класса для других классов в этом же модуле. Так, к примеру, к элементам класса в strict private не может получить доступа ни один код вне этого же класса, даже если этот код находится в том же модуле. Аналогично, доступ к элементам strict protected будут иметь только наследники этого класса, но не другие классы, даже если они будут находится в том же модуле.

Расширение дружественности: хак

Со второй проблемой (дружественные классы в разных модулях) несколько сложнее. В настоящее время Delphi не предоставляет никаких специальных языковых конструкций для разрешения этого конфликта. Вы не можете объявить классы дружественными друг другу, если они находятся в разных модулях. Поэтому программисту придётся использовать обходные пути.

Первый путь — самый простой и прямолинейный. Он заключается в использовании хака. Хотя обычно хак — это плохо, но данный конкретный хак безопасен. Заключается он в объявлении пустого класса-наследника в том же модуле, где нужно сделать дружественный класс. Например, пусть у нас есть класс:
unit Unit1;

interface 

type
  TSomeClass = class
  private
    FItem: Integer;
    ...
  end;
И в другом модуле есть класс, который должен обращаться к полю FItem:
unit Unit2;

interface 

uses
  Unit1;

type
  TAnotherClass = class
  public
    procedure DoSomething(AItem: TSomeClass);
  end;

implementation

procedure TAnotherClass.DoSomething(AItem: TSomeClass);
begin
  AItem.FItem := 0; // <- ошибка компиляции, т.к. у TAnotherClass нет доступа 
end;

end.
Чтобы разрешить этот конфликт (подразумевая, что мы не можем поместить TSomeClass и TAnotherClass в один модуль), мы можем сделать поле FItem как protected
type
  TSomeClass = class
  protected
    FItem: Integer;
    ...
  end;
А для TAnotherClass использовать такую конструкцию:
type
  TDummySomeClass = class(TSomeClass);

procedure TAnotherClass.DoSomething(AItem: TSomeClass);
begin
  TDummySomeClass(AItem).FItem := 0; // <- теперь доступ есть
end;
Объявляя класс-наследник (TDummySomeClass), мы делаем доступными ему все protected члены (и не важно, в каком модуле они расположены). А то, что этот класс-заглушка объявлен именно в модуле второго класса (TAnotherClass), сделает эти два класса дружественными, что и даст доступ к закрытому полю FItem. Обратите внимание, что при этом исходный класс (TSomeClass) не становится дружественным к TAnotherClass — вот почему нам необходимо выполнять явное преобразование типов.

Насколько безопасен такой хак? Ведь мы приводим объект одного класса (TSomeClass) к другому (TDummySomeClass), хотя он им не является. Ответ: достаточно безопасен. TDummySomeClass не содержит новых членов, мы не обращаемся к информации класса, мы используем только унаследованные элементы класса. Поэтому данное преобразование на уровне машинного кода — тождественно (т.е. вообще отсутствует). И никакие изменения в TSomeClass не приведут к поломке этого кода.

Разумеется, если у вас нет контроля над TSomeClass, то вы не сможете изменить область видимости FItem. В этом случае вы, конечно, можете использовать уже грязный хак (который является довольно опасным, т.к. произвольные изменения исходного класса могут его поломать). Но это не лучший вариант из-за опасной природы хака.

Расширение дружественности: интерфейсы

Итак, возвращаясь к нашим баранам, вариант второй разрешения конфликта — использование интерфейсов. Интерфейс (в смысле конструкции языка interface) — это набор методов (читай: действий), которые можно произвести с объектом. Иными словами, это как бы "копия" public секции объекта. Вкусность тут в том, что их может быть много у одного объекта. Любой внешний код может запросить у объекта любой его сервис, если он знает его "имя" (в терминах интерфейсов: имя = GUID).

Таким образом, класс TSomeClass может реализовывать один или несколько интерфейсов, предназначенных для его дружественных классов, расположенных в других модулях. Чтобы подчеркнуть внутреннюю направленность интерфейсов, их можно вынести в отдельный недокументированный модуль.
unit Internals;

interface

type
  IItemAccess = interface
  ['{07FBC0AE-C6CC-4ADD-A4D9-1AC08B7CA430}']
  // private
    function GetItem: Integer;
    procedure SetItem(const AValue: Integer); 
  // public
    property Item: Integer read GetItem write SetItem;
  end;

implementation

end.
interface

uses 
  Internals;

type
  TSomeClass = class(TObject, IUnknown, IItemAccess)
  private
    FItem: Integer;
    ...
    // IUnknown
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

    // IItemAccess
    function GetItem: Integer;
    procedure SetItem(const AValue: Integer); 

    ...
  end;

implementation

...

function TSomeClass._AddRef: Integer; stdcall;
begin
  Result := -1;
end;

function TSomeClass._Release: Integer; stdcall;
begin
  Result := -1;
end;

function TSomeClass.QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
begin
  if GetInterface(IID, Obj) then
    Result := S_OK
  else
    Result := E_NOINTERFACE;
end;

function TSomeClass.GetItem: Integer;
begin
  Result := FItem;
end;

procedure TSomeClass.SetItem(const AValue: Integer); 
begin
  FItem := AValue;
end;

...

end.
Тогда TAnotherClass может запросить у объекта класса TSomeClass интерфейс для внутренних манипуляций:
procedure TAnotherClass.DoSomething(AItem: TSomeClass);
var
  I: IItemAccess;
begin
  I := AItem;
  I.Item := 0; // <- теперь доступ есть
end;

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

Множественное наследование и интерфейсы

Но здесь, однако, кроется интересное для меня наблюдение. Если вы заметили, то объекты хорошо позволяют "реализовать что-то". У них есть наследование (и интерфейса и реализации), скрытие и дружественность, и т.д. Всё это позволяет легко реализовывать функциональность. Но не всегда это идеально удобно с точки зрения потребления/использования. Здесь уже ограничители (наследование, скрытие, ...) начинают скорее мешаться, чем приносить пользу. В самом деле, если я хочу добавить элемент в коллекцию, зачем мне всенепременно наследовать его от нужного класса? А если мне нужно добавить в коллекцию класс вне её дерева наследования? Ведь, по сути, здесь было бы достаточным, чтобы элемент поддерживал определённую функциональность (читай: "набор методов управления"). Для этого вовсе не обязательно строго наследовать от предопределённого класса, поскольку от элемента требуется лишь удовлетворять набору условий.

Насколько я понимаю, для решения этой проблемы (в том числе) в других языках программирования вводится поддержка множественного наследования: один класс может удовлетворять сразу нескольким функциональностям. К примеру, студент может быть не только человеком, но и рабочим, и музыкантом, и членом семьи (т.е. наследоваться от них в терминах множественного наследования). При этом, если наследуемые классы, в свою очередь, также наследуются друг от друга в различных комбинациях, то могут возникать конфликты (например, если "музыкант" и "член семьи" наследуют "прозвище" от "человек" с замещением реализации, то какую из реализаций должен брать "студент"?). Само собой, языки с поддержкой множественного наследования также содержат механизмы разрешения таких конфликтов. В целом же, множественное наследование часто критикуется. Проблемы, его чрезмерная сложность приводят критиков к выводу, что от него больше вреда, чем пользы. При этом также звучит мнение, что множественное наследование (как расширение "простого" одиночного наследования) — ошибочная концепция, порождённая неверным анализом и проектированием.

В Delphi проблемы множественного наследования решаются интерфейсами. Интерфейс — это полностью абстрактный класс, все методы которого виртуальны и абстрактны. Иными словами, говоря грубо, интерфейс — это запись (record) с указателями на функции. Каждый метод интерфейса должен быть реализован в классе. Причём реализацию вы либо пишете сами, либо наследуете, либо делегируете (для случая агрегации). Иными словами, любой интерфейс реализуется классом без конфликтов. Плюс, в каждый момент времени вы работаете с одним конкретным интерфейсом, а не с "объединённым набором интерфейсов", поэтому проблемы выбора нужной реализации тут просто нет — будет использоваться метод используемого интерфейса. Не та реализация? Берём другой (нужный) интерфейс с нужной реализацией.

Также замечу, что в Delphi отсутствует возможность множественного наследования не только классов, но и интерфейсов (множественное наследование интерфейсов существенно проще множественного наследования классов, поскольку интерфейсы не содержат реализации; тем не менее, даже здесь есть одна возможность для конфликтов).

В целом же, я считаю, что использование интерфейсов — это наиболее правильный вариант реализации множественного наследования, поскольку явное указание реализации исключает конфликты. Введение понятия интерфейсов является компромиссом, позволяющим получить преимущества множественного наследования, не реализуя его в полном объёме и, таким образом, не сталкиваясь со специфичными для него сложностями. Именно такой подход принят во многих современных языках — не только в Delphi, но и, к примеру, C# или Java.

Вы всё ещё используете объекты? Тогда мы идём к вам

Я уже кратко выразил основную мысль выше: объекты удобны для реализации функциональности (наследование/полиморфизм), но неудобны для использования. Добавлю только, что, помимо уже упомянутых ограничений, объекты неудобны ручным слежением за временем жизни. Интерфейсы же относятся к типам с автоматическим управлением временем жизни, что существенно упрощает разработку кода. Кроме того, интерфейсы — языко-независимы (т.е. ими можно обмениваться между кодом, написанных на разных языках), в отличие от классов, реализация которых своя в каждом языке программирования.

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

Иными словами, у интерфейсов есть куча плюсов, но есть и некоторые минусы. Во-первых, использование интерфейсов подвержено проблеме циклических ссылок (которую, впрочем можно легко обойти, следуя нескольким простым правилам разработки). Во-вторых, большая часть кода VCL и RTL написана в те времена, когда никакой поддержки интерфейсов в Delphi не было (историческая справка: изначально интерфейсы были введены в Delphi для поддержки технологии COM, но впоследствии их стали использовать более широко). Соответственно, весь этот код написан на объектах. И он наследуется и в современные версии Delphi. Более того, такой подход используют и сторонние библиотеки, руководствуясь "ну раз так поступает сам разработчик среды, то и мы тоже будем так делать". Итого, у вас может быть проблема состыковки кода с ручным и автоматическим управлением временем жизни. К сожалению, обычные интерфейсы в Delphi нельзя сделать "чистыми" (т.е. без обвеса автоматического управления). Вы можете добиться этого обходным путём, но это неудобно без поддержки со стороны языка.

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

К чему это я? Когда мы говорим про ООП — мы говорим в первую очередь про наследование. Это именно то свойство (из наследования, абстракции, инкапсуляции и полиморфизма), которое наиболее ярко определяет ООП. Когда нас учат ООП, нас учат моделировать мир иерархиями объектов: объекты в программе отражают сущности реального мира. Но в реальном мире не существует понятия наследования (кроме нескольких узких областей). В реальной жизни стул не является наследником мебели. И уж тем более студент не является множественным наследником "человек", "рабочий", "музыкант".

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

Проблема в том, что унаследовавшись от одного предка, класс уже не может наследоваться от других. Изменение предка становится опасным. Зачастую правильное использование private и protected требует от программиста неслабых телепатических способностей: что может понадобится нашим наследникам, а что нет?

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

Если же вместо объектов вы будете оперировать интерфейсами, то ваш код будет более приближен к реальному миру. У вас может быть "студент", и он не будет наследоваться от "человека", но он будет иметь имя, т.к. студент одновременно является и "человеком". И если кому-то нужен "студент", то ему совершенно не обязательно наследоваться от "человека", "млекопитающего" или ещё более низкого класса, если его всего-лишь интересуют университет, курс и группа "студента". Конечно, вы можете и должны использовать объекты и наследование при реализации интерфейсов: в конце концов, наследование — удобный способ повторного использования кода. Иными словами, суммируя мысль: объекты — язык описания реализации, интерфейсы — язык описания реального мира.

Бонус: секция published

В заключение — несколько слов о секции published. Эта секция введена в Delphi для работы встроенного механизма сериализации (к примеру, именно благодаря ему загружаются формы из ресурсов программы). Помещение свойств, методов и полей в секцию published заставит компилятор генерировать мета-информацию (RTTI) для них, что позволит объекту "узнавать о самом себе" во время выполнения. Именно это (генерация RTTI) — основное назначение секции published. Но кроме этого секция published также имеет ту же область видимости, что и public. К примеру, все компоненты любой формы доступны любому коду, т.к. находятся в секции published, т.е. имеют видимость public. Конечно же, это нарушает инкапсуляцию (концепцию "чёрного ящика"). Любой код может проводить произвольные манипуляции с формой, даже приводя её в недопустимое состояние (к примеру, отключив все кнопки на форме).

Поэтому нужно всегда помнить о назначении секции published (генерировать RTTI) и не пытаться манипулировать компонентами формы в обход её публичного интерфейса. С этой точки зрения дизайн Delphi был бы более удачен, если бы секция published имела бы ту же область видимости, что и protected. В настоящее же время наилучшим вариантом будет вообще скрыть форму за фасадом. Например — интерфейсом.
unit Dialog;

interface

type
  IInputDialog = interface
  ['{9CA0FC9A-5AFF-4A57-B5A8-C906F461FEB6}']
  // private
    function GetCaption: String;
    procedure SetCaption(const AValue: String);
    function GetText: String;
    procedure SetText(const AValue: String);
    function GetValue: String;
    procedure SetValue(const AValue: String);
  // public
    property Caption: String read GetCaption write SetCaption;
    property Text: String read GetText write SetText;
    property Value: String read GetValue write SetValue;
    function Show: Boolean;
  end;

  // Это - только пример. 
  // Само собой, открытый интерфейс и закрытую форму лучше разносить по разным модулям
  TDialogForm = class(TForm, IInputDialog)
    Label1: TLabel;
    Edit1: TEdit;
    Button1: TButton;
  protected
    // IInputDialog
    function GetCaption: String;
    procedure SetCaption(const AValue: String);
    function GetText: String;
    procedure SetText(const AValue: String);
    function GetValue: String;
    procedure SetValue(const AValue: String);
    function Show: Boolean;
  end;

var
  DialogForm: TDialogForm;

implementation

{$R *.dfm}

function TDialogForm.GetCaption: String;
begin
  Result := Caption;
end;

function TDialogForm.GetText: String;
begin
  Result := Label1.Caption;
end;

function TDialogForm.GetValue: String;
begin
  Result := Edit1.Text;
end;

procedure TDialogForm.SetCaption(const AValue: String);
begin
  Caption := AValue;
end;

procedure TDialogForm.SetText(const AValue: String);
begin
  Label1.Caption := AValue;
end;

procedure TDialogForm.SetValue(const AValue: String);
begin
  Edit1.Text := AValue;
end;

function TDialogForm.Show: Boolean;
begin
  Result := (ShowModal = mrOk);
end;

end.
...
var
  Dialog: IInputDialog;
begin
  Dialog := DialogForm; // форма должна быть создана, использует существующий объект
  // Это - только пример: на практике интерфейс обычно будет возвращаться каким-то методом, который имеет доступ к переменной формы

  Dialog.Caption := 'Заголовок'; 
  Dialog.Text := 'Вопрос'; 
  Dialog.Value := '';

  if Dialog.Show then
  begin
    ShowMessage(Dialog.Value);
    // ...
  end;
end;
или:
unit Dialog;

interface

type
  IInputDialog = interface
  ['{9CA0FC9A-5AFF-4A57-B5A8-C906F461FEB6}']
  // private
    function GetCaption: String;
    procedure SetCaption(const AValue: String);
    function GetText: String;
    procedure SetText(const AValue: String);
    function GetValue: String;
    procedure SetValue(const AValue: String);
  // public
    property Caption: String read GetCaption write SetCaption;
    property Text: String read GetText write SetText;
    property Value: String read GetValue write SetValue;
    function Show: Boolean;
  end;

function CreateInputDialog: IInputDialog;

implementation

type
  TDialogForm = class(TForm, IInterface, IInputDialog)
    Button1: TButton;
    Edit1: TEdit;
    Label1: TLabel;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
  private
    // IUnknown
    FRefCount: Integer;
  protected
    // IUnknown
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;

    // IInputDialog
    function GetCaption: String;
    procedure SetCaption(const AValue: String);
    function GetText: String;
    procedure SetText(const AValue: String);
    function GetValue: String;
    procedure SetValue(const AValue: String);
    function Show: Boolean;
  public
    // IUnknown
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    class function NewInstance: TObject; override;
  end;

{$R *.dfm}

function CreateInputDialog: IInputDialog;
begin
  Result := TDialogForm.Create(nil);
end;

{ TDialogForm }

function TDialogForm._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
end;

function TDialogForm._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Destroy;
end;

class function TDialogForm.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TDialogForm(Result).FRefCount := 1;
end;

procedure TDialogForm.AfterConstruction;
begin
  inherited;
  InterlockedDecrement(FRefCount);
end;

procedure TDialogForm.BeforeDestruction;
begin
  Assert(FRefCount = 0);
  inherited;
end;

// ... остальные методы - без изменений

end.
...
var
  Dialog: IInputDialog;
begin
  Dialog := CreateInputDialog; // создаёт новый диалог

  Dialog.Caption := 'Заголовок'; 
  Dialog.Text := 'Вопрос'; 
  Dialog.Value := '';

  if Dialog.Show then
  begin
    ShowMessage(Dialog.Value);
    // ...
  end;
end;
Подобный подход не только прост в реализации, но и добавляет в код высокую степень полиморфизма: в любой момент вы можете заменить форму на консольный ввод или ответ от удалённого сервера. Для этого достаточно будет заменить реализацию IInputDialog. Любой иной код, который его использует, совершенно не изменится.

Читать далее про интерфейсы (и не только): Полиморфизм на практике - "как".

20 комментариев:

  1. Статья вызывает противоречивые чувства... На мой взгляд, применительно к тексту статьи интерфейсы могут полезны для создания фасадов (как в последнем примере).
    Утверждения вроде "Также замечу, что, применяя объекты, вы вынуждены писать декларативный интерфейс рядом с реализацией. Это, в свою очередь, означает, что все ваши объявления и типы данных будут размазаны по разным модулям, как это требует реализация. С другой стороны, интерфейсы, как не имеющие реализации, могут быть собраны в одном месте." достаточно спорны, т.к. применение интерфейсов не отменяет создание реализации через наследование/делегирование/дублирование кода, поэтом призыв все оборачивать в интерфейсы может быть воспринят неправильно.
    Другое дело, если разработчик осмысленно вводит в код фасады на интерфейсах, чтобы разделить разные уровни абстракции исходного кода и сделать код самого высокого уровня абстракции наиболее близким к языку предметной области...

    ОтветитьУдалить
  2. Александр, при всём уважении... не убедил. Может пример не очень удачный?

    ОтветитьУдалить
  3. ...почему у меня ощущение, что читают только последний раздел? :)

    ОтветитьУдалить
  4. >>почему у меня ощущение, что читают только последний раздел?

    Создается впечатление, что все было написано ради последнего раздела

    ОтветитьУдалить
  5. Не, у меня план был точно не такой. Я сначала всё написал, а потом вспомнил, что было бы неплохо про published пару слов сказать. Это в тексте отражено приставкой "бонус".

    ОтветитьУдалить
  6. Такое ощущение, что сайт взломан...
    Если нужны френд-классы - делайте все public, если таки нужна инкапсуляция, то френд-классы - зло, поскольку нарушают её. Это такая же дыра, как если всё сделать public, причем неконтролируемая. Как class helpers прямо.
    Другое дело, что что-то типа явовского package private бы не помешало, но это уже другой вопрос - о чем там в ЕМБТ думают, вводя вместо этого удивительно бесполезные вещи, типа тех же strict'ов.
    Что касается этой фразы:
    "Также замечу, что в Delphi отсутствует возможность множественного наследования не только классов, но и интерфейсов"
    то либо я чего-то не понял, либо автор, ибо наследование интерфейсов в Delphi множественное.

    ОтветитьУдалить
  7. Да, забыл:
    > Интерфейс - это полностью абстрактный класс
    Интерфейс - не класс. И не запись. Это интерфейс.

    ОтветитьУдалить
  8. Интересный последний раздел с примером TInterfacedForm. Жалко, что приходится тянуть реализацию TInterfacedObject целиком и полностью, только ради того, чтобы использовать подсчет ссылок.

    И один момент:

    class function TDialogForm.NewInstance: TObject;
    begin
    Result := inherited NewInstance;
    TForm4(Result).FRefCount := 1;
    end;

    наверное
    TDialogForm(Result).FRefCount := 1;

    ?

    ОтветитьУдалить
  9. Никак не могу согласиться, что интерфейсы нужны для всего. Интерфейсы нужны там, где они нужны исходя из их плюсов. У интерфейсов это:
    1. Автоматический подсчет ссылок. Но могут возникнуть (и часто возникают) проблемы с циклическими ссылками.
    2. Независимость от компилятора. Однако на границу модулей все равно многие вещи выводить проблематично, из-за того что нельзя вывести те же самые массивы например.
    3. Возможность множественного наследования.
    У объектов же плюс, что можно удалить именно тогда когда нужно мне, а не когда там счетчик ссылок станет равен нулю. В случае, когда реализован механизм обратной связи для чистки ссылок (как TComponent.FreeNotify например) - это очень удобно. Кроме того, иногда уничтожать надо именно сейчас, т.к. могут быть захвачены ресурсы (в том числе аппаратные). В С# например ввели костыльное решение из IDisposable. Жаль такого нет на уровне языка, и приходится вручную это делать.

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

    ОтветитьУдалить
  10. По какой то причине все открещиваются от использования интерфейсов, между тем концепция "чёрного ящика" насильно заставляет неопытного члена команды разработчиков использовать «договор» и не выходить за его рамки, либо если он выходит за рамки это видно другим и они сразу могут начать использовать новое свойство/метод или оградить его от ошибок ...
    А опытного заставляет пойти единственным путём решения а не плодить сотни одинаковых решений нагромождая класс используя разные способы реализации одной и той же цели

    ОтветитьУдалить
  11. >> У объектов же плюс, что можно удалить именно тогда когда нужно мне, а не когда там счетчик ссылок станет равен нулю.

    если у тебя в текущий момент времени есть левые ссылки на удаляемый объект, то у тебя появятся битые ссылки, а значит AV - не за горами. Если же у тебя есть механизм нотификации(когда внешние ссылки очищаются), то и с интерфейсами ты всегда можешь принудительно освободить память
    Intf.RemoveFreeNotifications(); // а-ля TComponent.RemoveFreeNotifications() все внешние ссылки ссылки на интерфейс очищаются
    Intf := nil; // память гарантирована освобождена.

    ОтветитьУдалить
  12. > ибо наследование интерфейсов в Delphi множественное

    Да ну. Так нельзя:

    IMyInterface = interface(IInterface1, IInterface2, IInterface3)
    end;

    > Это такая же дыра, как если всё сделать public, причем неконтролируемая.

    См. к примеру http://www.transl-gunsmoker.ru/2010/10/blog-post_14.html Ключевые слова: "Идея в том, что в этот момент в вашей голове должен прозвенеть звонок: "Хм, а может мне не следует это трогать?". Так же и здесь: если у чего-то есть интерфейс для всех (public) и интерфейс для кого-то определённого (внутренний интерфейс), то при использовании внутреннего интерфейса публичным кодом должно быть то же самое: стоп, тут что-то не то.

    Вы отталкиваетесь от неявного утверждения, что любой контракт должен быть одинаков для всех. Вот откуда идёт ваше "нарушение инкапсуляции". А кто вообще сказал, что это правильно? К примеру, в реальной жизни это не так. У нас есть, скажем, бухгалтер. И он имеет доступ к финансовой отчётности фирмы - нарушая её инкапсуляцию (фин. отчётность недоступна другим фирмам).

    > Интерфейс - не класс. И не запись. Это интерфейс.

    Синтаксически - нет. Но я и не говорил про синтаксис, я говорил про концепцию. Ключевые слова были: "грубо говоря".

    > Кроме того, иногда уничтожать надо именно сейчас, т.к. могут быть захвачены ресурсы (в том числе аппаратные).

    Сам же решение и указал. Чем не нравится?

    ОтветитьУдалить
  13. Статья интересная, но...
    Это конечно мое ИМХО, но я всегда с большим подозрением отношусь к любым объектам, контролирующим собственный RefCount.
    Это из серии как ты озвучил: "Хм, а может мне не следует это трогать?"

    ОтветитьУдалить
  14. >> если у тебя в текущий момент времени есть левые ссылки на удаляемый объект, то у тебя появятся битые ссылки, а значит AV - не за горами. Если же у тебя есть механизм нотификации(когда внешние ссылки очищаются), то и с интерфейсами ты всегда можешь принудительно освободить память
    Intf.RemoveFreeNotifications(); // а-ля TComponent.RemoveFreeNotifications() все внешние ссылки ссылки на интерфейс очищаются
    Intf := nil; // память гарантирована освобождена.

    Только в случае AV мы видим где это произошло, если вдруг что-то не так пошло с механизмом чистки ссылок, а с Intf.RemoveFreeNotifications() мы надеемся, что при Intf := nil; у нас почистится память. В противном случае мы как минимум огребем утечку, которую будет трудно найти, а как максимум - не будут освобождены ресурсы, к которым у объекта был эксклюзивный доступ, что обернется в неработающий софт. Я кстати не спроста упомянул про IDisposable в дотнете. Это своеобразный "костыль", решающий проблему чрезмерных утечек + освобожение ресурсов здесь и сейчас, а не когда GC вздумает чистить память.

    ОтветитьУдалить
  15. А не является ли такая форма с интерфейсом изобретением очередного вилосипеда ?
    Например, уже давно существуют ActiveX формы и компоненты.

    ОтветитьУдалить
  16. Хотелось бы добавить что доступ (как чтение так и запись) к private и protected елементам класса можно осуществлять и из class helper что тоже является вариацией чистого хака и как по мне лучше чем создание пустого наследника

    ОтветитьУдалить
  17. Не сочтите "рекламой"...

    Прочитайте - http://programmingmindstream.blogspot.ru/2014/03/blog-post_12.html

    Там НЕТ ответов...

    Но по-моему - я там написал "хорошие вопросы". Мне так кажется...

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

    ОтветитьУдалить
  19. Спасибо за интересную статью!

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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

Примечание. Отправлять комментарии могут только участники этого блога.