Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения". |
Проектирование семейства классов
Эффективная реализация динамического связывания
Издержки времени на вызов динамически связываемого метода в сравнении с вызовом статически связываемого метода сводятся к стоимости поиска нужного метода. Гибкость, обеспечиваемая полиморфизмом и динамическим связыванием, имеет свою цену, однако любую цену мы платить не готовы. Но, как только что показано, принципиально возможно выполнять операцию поиска за константное время независимо от числа типов и числа методов в классе.
Наивная реализация сохраняет в памяти структуру, задающую иерархию наследования, и выполняет обход этой структуры в процессе поиска метода. Такая техника не приемлема (она особенно плоха в случае множественного наследования). Все обсуждаемые реализации построены на массивах (массив, индексируемый типом, программой, двумерный массив). Все они обеспечивают требуемое время поиска.
Следует учитывать и другую сторону эффективности программы — стоимость требуемой памяти. Во всех трех вариантах потребуются структуры, имеющие в общей сложности T * R входов. Этому требованию иногда трудно удовлетворить. Например, EiffelStudio использует примерно 6000 типов и 50000 методов. Но таблица, показанная выше, избыточна, поскольку практически большинство входов будут пусты, — каждый метод имеет смысл только для нескольких типов, например, метод load применим только для VEHICLE и его потомков.
Считать нужно программы, а не имена программ, поскольку в разных классах могут существовать программы с одним и тем же именем.
В варианте, который использует таблицы методов, мы можем отрезать края каждой из этих таблиц, удалив в каждом столбце все входы перед первым непустым входом ("эффективное начало") и после последнего не пустого входа.
Вышеприведенный код все еще будет работать при условии, что вход в таблицу routine_table[i] индексируется по отношению к эффективному началу, а не относительно физической точки начала столбца. Достичь этого нетрудно.
Такая оптимизация не представляет особого интереса, если непустые входы распределены по всему столбцу, задающему таблицу методов. Предположим, что при наличии 600 типов номер 1 отведен типу VEHICLE, 3000 для TRAM и 6000 для TAXI. Даже если load существует только для этих трех типов, нам все же придется иметь столбец с 600 входами, хотя все из них кроме трех будут пустыми. Для улучшения ситуации заметим, что у нас есть свобода для выбора номеров, присваиваемых типам, так что можно воспользоваться преимуществами следующего свойства.
Теорема о соседстве методов
Это предполагает выбор такой схемы нумерации, которая дает соседние номера потомкам любого данного класса. К этому моменту вы уже знаете, что может помочь в данной ситуации, — топологическая сортировка. Отношение "Быть потомком" является отношением частичного порядка, так как наследование ациклично.
Выполнение топологической сортировки существенно уменьшает размер таблиц методов — в EiffelStudio примерно на 85%. Эта техника носит принципиальный характер, поскольку без нее издержки памяти были бы критичными.
Что же касается издержек времени, то, как говорилось, они невелики, требуют константного времени и сравнимы с доступом к полю или элементу массива. Но все же лучше, если можно от них вообще избавиться, и такое в некоторых ситуациях вполне возможно. Компилятор может обнаружить, что:
- некоторая программа имеет только одну версию;
- некоторое выражение не является полиморфным.
В таких ситуациях можно применить схему статического связывания на этапе компиляции и избежать любых потерь времени при выполнении.
По этой причине в ряде языков (С++, Java, C# и других) статическое связывание предполагается по умолчанию, а динамическое — резервируется только для виртуальных методов. Такая политика приводит к появлению проблем, поскольку программисту легко ошибиться, предположив, что вызов является статическим, в то время как он полиморфен. Возможно, что когда-то принятое решение было корректным, но в ходе эволюции добавился новый класс-потомок с новой версией метода, а изменение объявления метода как виртуального так и не было сделано.
Правило говорит, что динамическое связывание всегда задает корректную семантику ОО-вызовов. Как следствие, статическое связывание применимо только тогда, когда оно имеет ту же семантику, что и динамическое связывание. Типично это бывает тогда, когда имеет место один из двух вышеописанных случаев. Из-за трудностей обнаружения таких случаев эту задачу целесообразно передать компилятору.
В EiffelStudio компилятор действительно занимается подобной оптимизацией.
Эти наблюдения дополняют наш краткий экскурс в технику реализации. Надеюсь, это дало лучшее понимание значимости наследования и связанных с ним приемов для выполнения ОО-программ. Конечно, на практике приходится учитывать много деталей. Если вы хотите дойти "до сути вещей", то стоит перейти к анализу кода на языке С, генерируемого компилятором EiffelStudio в "классическом" варианте. На следующем шаге следует изучить сам Eiffel-код.
Все доступно, все является открытым кодом. Как итог, два ключевых момента.
- Издержки времени на динамическое связывание могут требовать константного времени и невелики. В ряде случаев применение подходящих приемов может свести их к нулю.
- На реализацию динамического связывания требуется дополнительная память — одно поле на каждый объект, и память для таблиц, которая может быть ограничена до приемлемых размеров, если использовать разумную технику программирования.