Каковы преимущества и недостатки использования классов для инкапсуляции численных алгоритмов?

13

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

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

Хотя традиционный подход состоял в том, чтобы просто иметь одну функцию, которая вызывает ряд других функций, передавая соответствующие аргументы по пути, ООП предлагает другой подход, в котором алгоритмы могут быть инкапсулированы как классы. Для ясности, под инкапсуляцией алгоритма в классе я имею в виду создание класса, в котором входные данные алгоритма вводятся в конструктор класса, а затем вызывается открытый метод для фактического вызова алгоритма. Такая реализация multigrid в C ++ psuedocode может выглядеть так:

class multigrid {
    private:
        x_, b_
        [grid structure]

        restrict(...)
        interpolate(...)
        relax(...)
    public:
        multigrid(x,b) : x_(x), b_(b) { }
        run()
}

multigrid::run() {
     [call restrict, interpolate, relax, etc.]
}

Мой вопрос заключается в следующем: каковы преимущества и недостатки такой практики по сравнению с более традиционным подходом без занятий? Существуют ли проблемы расширяемости или ремонтопригодности? Чтобы быть ясным, я не собираюсь запрашивать мнение, а скорее лучше понять последующие эффекты (то есть те, которые могут не возникнуть, пока кодовая база не станет достаточно большой) принятия такой практики кодирования.

Бен
источник
2
Это всегда плохой знак, когда ваше имя класса является прилагательным, а не существительным.
Дэвид Кетчон
3
Класс может служить пространством имен без сохранения состояния для организации функций для управления сложностью, но есть и другие способы управления сложностью в языках, которые предоставляют классы. (На ум приходят пространства имен в C ++ и модули в Python.)
Джефф Оксберри
@GeoffOxberry Я не могу говорить, является ли это хорошим или плохим использованием - именно поэтому я спрашиваю в первую очередь - но классы, в отличие от пространств имен или модулей, также могут управлять «временным состоянием», например, иерархией сетки в многосетке, которая отбрасывается после завершения алгоритма.
Бен

Ответы:

13

Проработав числовое программное обеспечение в течение 15 лет, я могу однозначно заявить следующее:

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

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

  • По сравнению со многими другими программными системами, иерархия классов часто менее важна, чем, скажем, в графических пользовательских интерфейсах. В числовом программном обеспечении, безусловно, есть места, где они выделяются - Джед обрисовывает один за другим ответ на эту тему, а именно, множество способов представления матрицы (или, в более общем случае, линейного оператора в конечномерном векторном пространстве). PETSc делает это очень последовательно, с виртуальными функциями для всех операций, которые работают с матрицами (они не называют это «виртуальными функциями», но это так и есть). В типичных конечно-элементных кодах есть и другие области, в которых используется принцип разработки программного обеспечения ОО. На ум приходят много видов квадратурных формул и множество видов конечных элементов, все из которых естественно представлены как один интерфейс / много реализаций. Материально-правовые описания также попадают в эту группу. Но это может быть правдой, что именно об этом, и что остальная часть кода конечных элементов не использует наследование так широко, как его можно использовать, скажем, в GUI.

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

Вольфганг Бангерт
источник
16

Инкапсуляция и сокрытие данных чрезвычайно важны для расширяемых библиотек в научных вычислениях. Рассмотрим матрицы и линейные решатели в качестве двух примеров. Пользователь просто должен знать, что оператор является линейным, но он может иметь внутреннюю структуру, такую ​​как разреженность, ядро, иерархическое представление, тензорное произведение или дополнение Шура. Во всех случаях методы Крылова не зависят от деталей оператора, они зависят только от действия MatMultфункции (и, возможно, ее сопряженной). Аналогичным образом, пользователь интерфейса линейного решателя (например, нелинейного решателя) заботится только о том, что линейная задача решена, и ему не нужно или не нужно указывать используемый алгоритм. Действительно, указание таких вещей будет препятствовать возможностям нелинейного решателя (или другого внешнего интерфейса).

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

Джед браун
источник
8

Классы следует использовать только в том случае, если структура кода является иерархической. Поскольку вы упоминаете алгоритмы, их естественная структура представляет собой блок-схему, а не иерархию объектов.

В случае OpenFOAM алгоритмическая часть реализована в терминах универсальных операторов (div, grad, curl и т. Д.), Которые в основном являются абстрактными функциями, работающими с различными типами тензоров, с использованием различных типов числовых схем. Эта часть кода в основном построена из множества общих алгоритмов, работающих с классами. Это позволяет клиенту написать что-то вроде:

solve(ddt(U) + div(phi, U)  == rho*g + ...);

Иерархии, такие как транспортные модели, модели турбулентности, разностные схемы, градиентные схемы, граничные условия и т. Д., Реализованы в терминах классов C ++ (опять же, общих для тензорных величин).

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

Иерархическая структура ==> классы

Процедурные, блок-схемы ==> алгоритмы

tmaric
источник
5

Даже если это старый вопрос, я думаю, что стоит упомянуть конкретное решение Юлии . Этот язык делает «ООП без классов»: основными конструкциями являются типы, т. Е. Составные объекты данных, подобные structs в C, для которых определяется отношение наследования. Типы не имеют «функций-членов», но каждая функция имеет сигнатуру типа и принимает подтипы. Например, вы могли бы иметь абстрактный Matrixтип и подтипы DenseMatrix, SparseMatrixи имеют общий метод do_something(a::Matrix, b::Matrix)со специализацией do_something(a::SparseMatrix, b::SparseMatrix). Многократная диспетчеризация используется для выбора наиболее подходящей версии для вызова.

Этот подход более эффективен, чем ООП на основе классов, что эквивалентно диспетчеризации на основе наследования только по первому аргументу, если принять соглашение о том, что «метод - это функция с thisпервым параметром» (обычно, например, в Python). Некоторая форма множественной диспетчеризации может эмулироваться, скажем, в C ++, но со значительными искажениями .

Основное различие заключается в том, что методы не принадлежат классам, но они существуют как отдельные объекты, и наследование может происходить по всем параметрам.

Некоторые ссылки:

http://docs.julialang.org/en/release-0.4/manual/methods/

http://assoc.tumblr.com/post/71454527084/cool-things-you-can-do-in-julia

https://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/

Федерико Полони
источник
1

Два преимущества подхода ОО могут быть:

  • βαcalculate_alpha()αcalculate_beta()calculate_alpha()α

  • calculate_f()е(Икс,Y,Z)Zset_z()Zcalculate_f()Z

ptomato
источник