Дата публикации: Feb 16, 2013 7:58:50 PM
Полная поддержка объектно-ориентированного программирования появилась в Стандарте Fortran-2003. До того (начиная с Fortran-90) уже были модули и перегрузка, которые позволяли кое-что исполнить "по-объектному": скрыть часть кода и данных, использовать модули для разработки других модулей и т.п. Часто можно услышать что-то вроде "...даже ООП, хотя зачем ООП в Фортране?" и т.п. В общем-то, хорошо, что есть. Если не надо, можно и не пользоваться. Зато если надо, то очень полезно. Собственно, для чисто вычислительных задач, для которых Фортран предназначен, ООП действительно нужно редко. Но оно ведь и вообще нужно редко, если вдуматься. Была мода на все объектное, были (и есть) ОО-языки, Java хоть взять. Но для написания программы, которая что-то делает, ООП вообще ни зачем не нужно. ООП полезно для создания библиотек - вот в этом его сила и мощь. А библиотеки для Фортрана - вещь весьма даже полезная. Так или иначе, но ООП в Фортране пока известно мало и, соответственно, мало используется.
Добавлю еще, что подход к ООП в Фортане достаточно здравый: оно не навязывается как единственно правильный стиль, но присутствует в полной мере. Притом реализовано в духе Фортрана.
Принципы ООП: абстракция, инкапсуляция, наследование, полиморфизм (статический и динамический). Первое - это создание объектов данных, которые ведут себя как объекты предметной области: окна могут перемещаться, открываться, менять размер и т.п., матрицы могут умножаться, транспонироваться и знают свой ранг, след и определитель, регулярное выражение умеет искать совпадения в строках оптимальным образом, система дифференциальных уравнений знает, как себя решать, файл способен выдавать или принимать информацию. Как это все реализовано, для потребителя объектной библиотеки - не важно.
Собственно, абстракция не является монопольным владением ООП: модули и перегрузка решают проблему, хотя и не столь изящно. В ОО-Фортране абстракцию реализуют производные типы и перегрузка операций.
Инкапсуляция - это объединение кода и данных. Точнее, данных и процедур (методов), которые с ними работают. С позиции абстракции - это наличие у объекта поведения. Если у нас есть матрица, как объект данных, то в процедурном стиле мы вызываем процедуру ВЫЧИСЛИТЬ_ОПРЕДЕЛИТЕЛЬ и передаем ей матрицу, как аргумент. Определитель получаем как результат функции или, если хочется, во втором, исходящем, аргументе. В ОО-стиле мы просим матрицу сообщить ее определитель. Технически мы вызываем метод ВЫЧИСЛИТЬ_ОПРЕДЕЛИТЕЛЬ, которому автоматически передается объект-хозяин в качестве первого аргумента, так что в чем же разница? Разница есть. Дело не в том, что можно создать новую операцию .det. , которая будет вычислять определитель, это и без ООП возможно. Разница в стиле. Еще разница связана со второй особенностью инкапсуляции - сокрытием данных. В ООП не приветствуется доступ к данным объекта, в некоторых языках он вообще запрещен, или запрещен по умолчанию. В Фортране по умолчанию разрешен, но можно запретить. А если данные меняются только посредством методов, то, например, определитель не обязательно пересчитывать всякий раз: только если изменились элементы матрицы, а она, матрица, должна об этом знать. В Фортране инкапсуляция реализована через процедуры, связанные с типом, в том числе - завершающие (деструктор в обще терминологии).
Наследование - это возможность строить новые объекты на базе старых. Это естественно при расширении функциональности или сужении множества объектов. Например, объект "система дифуров" может себя решать различными методами, и при желании добавить методов разумно их именно добавить, не переделывая все с нуля. А объект "квадратная матрица" является матрицей и, будучи таковой, может складываться с такими же, умножаться на подходящие по размеру, вычислять свой ранг. Но может и многое другое. Релизовано наследование как расширение типа, при этом в расширенном типе есть скрытое поле типа предка с тем же именем, так что есть доступ ко всем полям и методам предка. Психологическая тонкость: расширение типа может быть связано с сужением множества объектов, но это обычное дело: чем больше пространство, тем меньше к нему сопряженное.
Наконец, полиморфизм. Это одинаковое поведение для разных объектов. Точнее, различное поведение при одинаковом его внешнем виде. Так, матрицы могут складываться; это могут быть прямоугольные, квадратные, ортогональные, симметричные, диагональные и другие матрицы, состоящие в сложной системе отношений "предок-потомок": все матрицы прямоугольные, ортогональные квадратны, диагональные симметричны и т.п. Операции "сложение" это должно быть не важно, какой там тип у слагаемых. Причем тип может даже не существовать на момент компиляции. Например, если в библиотеке не было диагональных матриц, мы вправе расширить тип симметричных и создать такой тип, и операция + должна работать с ним без дополнительных действий. Это - полиморфизм. Разница между статическим и динамическим отражает разницу между временем компиляции и времени исполнения. Статический полиморфизм - это возможность процедур принимать аргументы различных типов. В Фортране это реализовано через родовые имена: несколько функций, не обязательно методов, объединяются под одним именем, а вызов конкретной функции осуществляется по типу фактических аргументов. То же можно проделать для методов, то есть связанных с типом процедур. Истинно объектным свойством является полиморфизм динамический, то есть проверка соответствия типов на этапе выполнения программы. Он реализован через полиморфные переменные, тип которых объявляется как "этот тип или его наследник" или "любой тип", а также полиморфные процедуры, параметры которых полиморфны. Собственно, полиморфные переменные - это формальные аргументы процедур, а еще указатели и размещаемые массивы (или скаляры). При этом реальный тип определяется по, соответственно, фактическому аргументу, при связывании указателя с целью и/или при размещении (при этом необходим спецификатор SOURCE или MOLD, первый задает объект, определяющий тип и содержимое, а второй - только тип). Существует конструкция выбора типа, позволяющая получить доступ к полям и методам расширенного типа. В самом деле, нужно различать две ситуации: если процедура работает с "квадратной матрицей", то она должна работать и с другими типами матриц, лишь бы они были квадратными. Формально "ортогональная матрица" будет уже другим типом, расширяющим тип "квадратная матрица", и концепция полиморфизма требует, чтобы "ортогональная матрица" годилась там, где требуется "квадратная". В этом случае все новое, что добавлено в тип, не играет роли, да и просто недоступно. Однако возможно, что процедура "знает", что матрица может быть просто "квадратная", а может быть "ортогональная" - и ортогональность можно использовать (например, при вычислении определителя). Чтобы получить доступ к новому, добавленному в расширенный тип, используется конструкция выбора типа. Тип объекта проверяется на совпадение с заданным типом, на совпадение с заданным типом или его потомком, и есть блок "по умолчанию".
Еще несколько понятий ООП. Деструктором называется метод, который автоматически вызывается при уничтожении объекта. Он освобождает память, закрывает файлы, в общем, убирает за собой. В Фортране это завершающие процедуры. Они объявляются с помощью слова FINAL. Конструктор - метод, который создает объект. В Фортране в явном виде отсутствуют. Однако можно эмулировать конструктор в стиле C++, объявляя процедуру или родовой интерфейс (объединяющий ряд процедур) с тем же именем, что у типа. Тогда можно писать что-то вроде
TYPE(MATRIX):: M
M = MATRIX(2,2)
Перегрузка операций - это определение или переопределение операций для работы с объектами, а также введение новых операций. В Фортране подлежат перегрузке собственно операции (+,-,*,/,**,// и т.п.), а также присваивание и ввод/вывод (форматный и неформатный). Присваивание и ввод/вывод есть по умолчанию, при перегрузке они переопределяются, а операций по умолчанию не предусмотрено. Можно вводит свои операции в стиле .БУКВЫ., то есть набор букв между двумя точками.
Перегрузка в стиле ООП - это функции с двумя аргументами для бинарных операций и с одним - для унарных. При этом первым аргументом идет объект-хозяин метода. Присваивание перегружает процедура с двумя аргументами. Ввод/вывод - тоже процедуры, с набором параметров.
Абстрактный тип - тип, объекты которого не могут быть созданы. Он служит для того, чтобы быть предком иерархии типов. Собственно, препятствием для создания объектов является отсутствие реализации некоторых (отложенных) методов. Например, векторы, матрицы и тензоры могут быть потомками абстрактного типа "многомерный числовой объект", который имеет операцию сложения и отложенную операцию умножения, которая никак не реализована, хотя и объявлена. Реализуют ее потомки, причем по-разному.
При наследовании возникает ситуация, при которой тип-потомок имеет методы родителя и может их при необходимости переопределить. Родительский метод всегда доступен через скрытое поле-предка. Но есть возможность запретить переопределение метода - в этом случае потомок вынужден пользоваться родительским методом или определять свой, с новым именем.
Теперь немного о ОО-технологии без ООП, средствами Fortran-90/95. Модули позволяют скрыть код и данные - это инкапсуляция. Импортируя модуль, пользователь получает тип и работающие с ним процедуры и/или перегруженные операции - это абстракция. Модули могут использовать другие модули, что отчасти означает наследование. Правда, видимость по импорту не транзитивна, то есть то, что импортировано в модуль из другого модуля, не видно в программе, импортирующей модуль. Полиморфизм - статический - реализует перегрузка процедур посредством родовых интерфейсов. Несколько процедур объединяются под одним именем, а вызов процедуры с этим именем приводит к вызову одной из процедур, в зависимости от числа и типа аргументов. Динамический полиморфизм можно моделировать, но довольно искусственно. Хорошее раскрытие темы - в статье А.М. Горелик "ООП в современном Фортране". В ряде случаев нет смысла "городить огород" с ООП - например, если объект ровно один в программе. Пример: стек. Можно реализовать стек объектно, с операциями PUSH, POP, функциями типа ROLL, SWAP, DUP, SUM, MUL и т.п. - ну, см. Постскрипт. И создавать объекты-стеки. Но смысл в том, чтобы стек просто был. Тогда зачем это все? Создаем модуль, в нем тип, описывающий стек, так или иначе. Объект этого типа - собственно, стек. Функции и перегруженные операции, работающие с ним. Импорт модуля сразу дает стек и средства для работы с ним. Признаки ООП налицо: абстракция в полной мере, инкапсуляция - тоже (данные модуля вообще не видны); наследование в принципе возможно - можно создать новый модуль, реализующий новую функциональность на базе старого, без необходимости модифицировать код старого (код вообще может не быть доступен). Полиморфизма вот нет. Но это как раз и не обязательно: полиморфизм нужен, когда есть предки и потомки, а стек ровно один. Наличие стека позволяет, кстати, пользоваться анонимными переменными, что удобно, когда длинные вычисления: многострочное выражение неудобно читать, а кучу вспомогательных переменных не хочется заводить и потом от них не избавиться.