0,0
рейтинг
27 февраля 2010 в 09:51

Разработка → Преобразование ссылки на интерфейс для реализации класса в Delphi 2010 перевод

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

Одна из возможностей Delphi 2010, которая, как мне кажется, породит массу споров — это возможность привести интерфейсную ссылку назад к типу класса, реализующего этот интерфейс.

Давайте представим, что у нас есть интерфейс IMyInterface и класс TMyClass, который реализует этот интерфейс:

IMyInterface = interface
 ['{7AD04980-5869-4C93-AC29-C26134511554}']
procedure Foo;
end;

TMyClass = class(TInterfacedObject, IMyInterface)
 procedure Foo;
 procedure Bar;
end;


Дальше, давайте представим что нам передали переменную типа IMyInterface. Что случится если мы захотим вызвать Bar? Попытка просто привести интерфейсную ссылку к типу TMyClass приведёт к ошибке компилятора.

Наиболее частое решение, которое я видел, это включить в интерфейс метод, который вернёт тип класса, но оно изничтожает ценность помещения интерфейса на первое место, привязывая интерфейс к конкретной реализации. Более того, оно отвратительно.

Такие приёмы теперь не нужны.

В Delphi 2010 вы можете использовать оператор is для того, чтобы проверить реализован ли интерфейс определённым классом, и если это так, привести его к этому классу и использовать неинтерфейсные методы, свойства и т.п.

Более того, если вы попробуете привести интерфейсную ссылку к типу класса из которого она не была фактически получена, оператор as вызовет исключение EInvalidCast. При тех же условиях жесткое приведение типа вернет nil.

Теперь этот код запускается успешно:
if MyInterface is TMyClass then
 TMyClass(MyInterface).Bar;


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



Вы можете помочь в улучшении перевода.
translated.by/you/casting-an-interface-reference-to-the-implementing-class-in-delphi-2010/into-ru
Переводчики: r3code, debose, VesninAndrey
Перевод: Malcolm Groves
Дмитрий Синявский @r3code
карма
67,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (34)

  • –2
    Да, замечательная возможность, уже вовсю использую. Раньше приходилось реализовать специальный интерфейс с методом возвращающим ссылку на объект, сейчас стало все проще… Еще понравилось, что в Exit теперь можно результат функции аргументом передать.
  • +5
    Гениально! А нафига вам интерфейсы тогда, если нужна ссыла на объект? :) Нет, мне положительно нравятся такие вот извращения: сначала всеми силами лезем в абстракции, а потом резко ищем приземлённости и конкретики. Костыльно-ориентированное программирование во плоти.
    • –3
      Вот пример из практики. У меня интерфейсы торчат наружу для Javascript-а, соответственно они все от IDispatch и работают только с типами данных поддерживающими автоматизацию. Вот я сделал объект — битмап, выставил наружу только необходимые методы для манипуляции скриптом и хочу реализовать метод отрисовки одного битмапа на другом. Самое простое это как раз проверить аргумент метода на IS, а потом привести через AS и выполнить работу на низком уровне, чем городить новый интерфейс для получения необходимых для выполнения работы данных.

      По поводу архитектуры — ваша правда, все это как бы идеологически неправильно, но в реальном программировании нужно бывает чтобы это работало здесь и сейчас и как бы ты архитектуру не «вылизывал» — изъяны будут. Идеальная архитектура недостижима — как коммунизм… :)
    • 0
      Потому что интерфейс, в отличие от класса не несет в себе реализации, а несет набор функций, которые можно как угодно реализовать. Что прибавляет гибкости, и позволяет код сделать более аккуратным.
      • 0
        Спасибо, капитан, я в курсе, что такое интерфейс и зачем он нужен. Моя ирония была направлена совсем на другое. Внимательнее вчитайтесь.

        Где в вашем комментарии соотношение с темой топика и с темой возмущения?

        > Как вы теперь это реализуете без интерфейсов?
        Элементарно.
    • 0
      Вот вам кстати простейший пример: программе надо периодичеки сохранять какие-то свои настройки. Для этого нам нужно 2 метода у объекта: saveSetting(name, value) и getSetting(name). Допустим, изначально решено сохранять их на диск и сделать для этого класс допустим diskStorage. А завтра вдруг выясняется, что программу усложнили, и теперь в некоторых версиях должно быть сохранение на диск, а в другой — допустим в реестр, или в БД. Как вы теперь это реализуете без интерфейсов?
    • 0
      Вообще-то использование объекта — это обращение к его публичному интерфейсу.
      И непонятно, почему у публичного интерфейса должно быть меньше прав чем у выделенных.
      Другое дело, что ссылка на объект — это привязка к Delphi, но недостаток здесь не в исторических костылях — классы вводили, не заботясь об их переносимости из языка в язык.
  • –4
    Спасибо большое за статьи по Delphi. Может кто подскажет литературу для начинающего программера..?)
    • –3
      Литературы по программированию полный интернет. ТЕбе надо конкретно что? Алгоритмы, синтаксис языка, что такое ООП и т.д.
    • +2
      может начинающему стоит обратить внимание на другие языки?
    • 0
      Книжки Архангельского поищи. Но вообще обучаться программированию лучше с чего-нибудь попроще, или наоборот — сразу с функциональщины.
  • –3
    Что-то не верится, что is появился в Delphi только сейчас.
    • –3
      он точно был и раньше
  • +1
    Если в большей части кода использование обобщённое, но в некотрых местах зависимое от реализации, это свидетельствует о плохой архитектуре. Постараюсь показать альтернативы на примере потока ввода-вывода (I/O stream). Итак, есть несколько стандартный путей:

    1) Большой интерфейс. Пустые реализации ненужных методов. Например, не для всех потоков ввода-вывода имеет смысл операция Flush(), но она может присутствовать в интерфейсе всегда. Для TCP/IP сокета реализация может быть пустой, а для файла — нет.

    Плюс в унифицированности использования.

    Минус в сложности отладки, так как редко реализуемые методы зачастую забывают вызвать. Скажем, если основной используемый поток сокетный, то забывают вызвать Flush() и при аварийном сбое в файл не попадают последние записи.

    2) Маленькие интерфейсы. Переразбиение большого интерфейса на несколько. Например, IInputStream, IOutputStream, IFlushableStream.

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

    Минус в дублировании. У файла открытого как на запись, так и на чтение будет две длины: от IInputStream и от IOutputStream, две текущих позиции и т.д. Кроме того, всегда существует множество способов разбить большой интерфейс на мелкие. Разбиение: IStream, IDimensionalStream, ISeekableStream не менее правомерно. В общем случае разбиение отражает сценарии использования и не может быть универсальным.

    3) Большой интерфейс с метаинформацией. Те же пустые реализации ненужных методов, но разбавленные вспомогательными маркерами реализуемой функциональности: CanRead, CanWrite, CanFlush, CanSeek.

    Плюс в унифицированности использования.

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

    4) Использование патерна Декоратор. Вместо декларации метода в интерфейсе создать реализацию выполняющую работу метода в рамках вызова других методов. Например, создать помимо очевидной реализации FileStream, отдельную реализацию-обёртку LogFileStream, которая сама вызывает Flush() после каждого Write().

    Плюс в полной прозрачности для вызывающей стороны.

    Минус в узкости области применения, так как Flush после Write это конкретный фиксированный сценарий. Кроме того, количество реализаций интерфейса растёт существенно быстрее, что так же может оказаться минусом. Немаловажно так же то, что классы должны быть дружественны к оборачиванию. Например в примере выше, должна быть возможность получить в обёртке значение дескриптора файла.

    Исходя их того, куда движется программирование, а оно движется в сторону сервис-провайдеров, мировая общественность за вариант №2.
    • –3
      1) Single Responsibility Principle и Interface Segregation Principle

      2) Дублирования не будет, если сделать отдельный универсальный класс Stream, которым будут пользоваться для своих целей реализации указанных интерфейсов. Интерфейсы отражают сценарии использования, но реализация следовать этому принципу не обязана ибо как раз благодаря интерфейсам от неё никто не зависит!

      3) Tell, don't ask — не спрашивайте (CanRead? CanWrite?), а указывайте.

      4) Опять же SRP (конкретные сценарии — это хорошо, если сценарий реализован и работает — не думаем больше о нём и сосредотачиваемся на других частях программы) и Tell, don't ask (не спрашивайте дескриптор, а сразу указывайте, что нужно делать (вызывайте метод) — тем самым явно обозначая своё намерение).

      А вообще, вот полезный набор практик: SOLID.
      • –2
        1) Беда в том, что responsibility штукенция субъективная. Для кого-то responsibility это работа с файлом в целом, а для кого-то чтение файла и запись файла разные responsibility.

        Можно сказать с уверенностью, чтение файлов и расчёт числа пи уже не single responsibility, и только. То есть принцип, конечно, верный, но чёткого критерия нет и применяется он на уровне опыта и интуиции.

        Правило «no client should be forced to depend on methods it does not use» в примере не нарушено. Наличие неиспользуемого Flush() не обязывает (forced to depend) писать дополнительный код или иначе писать код использующий другие методы. Так что ISP немного не про то.

        2) Ну вот в стандартной библиотеке Си++ дублирование есть: istream, ostream содержат множество похожих методов. Правда в Си++ есть множественное наследование, что спасает.

        3) Смотри класс Stream из .Net. Без множественного наследования, проблемы дублирования из примера №2 становятся неразрешимыми. В итоге имеем пресловутые маркеры CanRead, CanWrite.
        • –3
          Не надо путать фреймворки и обычный рабочий код. Рабочий код пишется для решения конкретных задач, строить из него универсальный фреймворк пустая трата времени и сил.
          • –1
            Что то вы ИМХО в демагогию ударились.
  • –3
    Такой вопрос: это в Delphi for .net, или в native-коде тоже поддерживается?
    • –3
      Да, поддерживается в «native-коде».
  • –4
    Эх, Deplhi, конечно, строгий язык, удобочитаемый код итд, но я не могу понять, почему раньше тот код с явным кастом не компилировался. Я так понимаю, в предыдущих версиях приходилось работать через промежуточный каст к Object'у?
    //
    Мне кажется, здесь действительно лучше всего — позиция Java и C#, в котором код, содержащий кастинг ссылочных объектов, будет в любом случае скомпилирован, а корректность кастинга проверяется в рантайме. Если уж нужна строгость и полный контроль за тем, что происходит — то лучше смотреть в сторону С++, где есть разные типы кастов (в данном случае, наиболее подходящим выбором будет dynamic_cast<ImplementationPointer*>.
    • 0
      Эх, Deplhi, конечно, строгий язык, удобочитаемый код итд, но я не могу понять, почему раньше тот код с явным кастом не компилировался.

      А как он компилироваться мог? TMyClass() весьма низкоуровневое приведение типов, просто указатель одного типа приводится к другому типу. Если мы говорим о наследовании, то таблица виртуальных методов наследника включает в себя таблицу предка, поэтому все работает так, как надо.
      TMyClass(object1) приводит указатель одного типа к указателю другого. object1 — указатель на структуру класса, причем по смещению 0 в этой структуре находится указатель на VMT. Таким образом, при выполнении TMyClass(object1).Bar если метод Bar — виртуальный, произойдет чтение адреса VMT структуры object1, и чтение адреса Bar по некоему смещению от начала VMT.
      Если бы было так: (MyInterface as TMyClass).Bar было бы логично, а зачем такой синтаксис — непонятно.
    • 0
      В дельфи тоже есть разные типы явных кастов. Статический — «имятипа(выражение)» и динамический «выражение as имятипа».
  • –2
    Они изобрели dynamic_cast (qobject_cast)?
    • 0
      Нет, аналог dynamic_cast — оператор as.
  • +2
    Необходимость получения из интерфейса указателя на объект, его реализующий — свидетельствует о проблемах в проектировании.

    Смысл интерфейса в частности в том, что мы не знаем и не должны знать, какой именно объект скрывается за ним.
    • +1
      Вот потому, автор то и пишет, что возможность эта достаточно спорная. Кому-то сгодиться ведь, нашлись же люди выше.
    • 0
      Не обязательно это проблема в проектировании — нововведением собственный публичный интерфейс класса всего лишь уравнивается в правах с выделенным, прямого доступа к данным объекта указатель не дает, следовательно инкапсуляцию не нарушает.
      Просто для межмодульного взаимодействия она не годится, так как в отличие от выделенных интерфейсов Delphi-only. Но это совсем другая проблема.
  • 0
    Укажи список переводчиков. Зря что ли трудились?
  • 0
    Че-то Эмбаркадера совсем задурила… Глядишь, и «Паблика Морозова» стандартизируют, потому что «бывают случаи».
    • 0
      Конкретно это — само по себе не дурь
    • 0
      Паблик Морозов в дельфи и так делается одной строкой — достаточно объявить фиктивного наследника «кулака» — и вуаля! Не сработает только для секции strict protected. Используется для хаков архитектурных ошибок в сторонних библиотеках.
  • 0
  • 0
    UBbccYnzJGA

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.