Які переваги та недоліки притаманні використанню класів для інкапсуляції числових алгоритмів?


13

Багато алгоритмів, що використовуються в наукових обчисленнях, мають іншу притаманну структуру, ніж алгоритми, які зазвичай розглядаються в менш інтенсивних математичних формах програмної інженерії. Зокрема, окремі математичні алгоритми, як правило, дуже складні, часто включають сотні чи тисячі рядків коду, але, тим не менш, не містять стану (тобто не діють на складній структурі даних) і часто можуть бути зменшені - з точки зору програмних інтерфейс - до однієї функції, що діє на масив (або два).

Це говорить про те, що функція, а не клас, є природним інтерфейсом більшості алгоритмів, що зустрічаються в наукових обчисленнях. Однак цей аргумент пропонує мало розуміння щодо того, як слід реалізувати реалізацію складних алгоритмів, що складаються з декількох частин.

Хоча традиційним підходом було просто мати одну функцію, яка викликає ряд інших функцій, передаючи відповідні аргументи по шляху, OOP пропонує інший підхід, де алгоритми можуть бути інкапсульовані як класи. Для наочності під інкапсуляцією алгоритму в класі я маю на увазі створення класу, в якому входи алгоритму вводяться в конструктор класів, і тоді публічний метод викликається для фактичного виклику алгоритму. Така реалізація багаторешітки в 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
Клас може служити простором імен без стану для організації функцій з метою управління складністю, але є й інші способи управління складністю в мовах, що надають класи. (
Намічаються простори

@GeoffOxberry Я не можу говорити про те, чи це добре чи погано використовувати - ось чому я питаю в першу чергу - але класи, на відміну від просторів імен чи модулів, також можуть керувати "тимчасовим станом", наприклад, ієрархією сітки в мультисети, що відкидається після завершення роботи алгоритму.
Бен

Відповіді:


13

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

  • Інкапсуляція важлива. Ви не хочете передавати вказівники на дані (як ви пропонуєте), оскільки це відкриває схему зберігання даних. Якщо ви відкриєте схему зберігання, ви більше не можете її змінити, оскільки ви отримаєте доступ до даних по всій програмі. Єдиний спосіб уникнути цього - це інкапсуляція даних у приватні змінні класу класу та дозволити на нього діяти лише функції-учасниці. Якщо я читаю ваше запитання, ви вважаєте функцію, яка обчислює власні значення матриці як стан без стану, приймаючи вказівник на записи матриці як аргумент і певним чином повертаючи власні значення. Я думаю, що це неправильний спосіб думати про це. На мій погляд, ця функція повинна бути функцією "const" члена класу - не тому, що вона змінює матрицю, а тому, що вона працює з даними.

  • Більшість мов програмування OO дозволяють мати функції приватних членів. Це ваш спосіб розділити один великий алгоритм на менший. Наприклад, різні допоміжні функції, необхідні для обчислення власних значень, все ще працюють на матриці, і, природно, це були приватні функції члена класу матриць.

  • У порівнянні з багатьма іншими програмними системами може бути правдою, що ієрархії класів часто менш важливі, ніж, скажімо, в графічних інтерфейсах користувача. У числовому програмному забезпеченні, безумовно, є місця, де вони помітні - Джед окреслює одну іншу відповідь на цей потік, а саме багато способів можна представляти матрицю (або, взагалі, лінійний оператор у кінцевому розмірному векторному просторі). PETSc робить це дуже послідовно, з віртуальними функціями для всіх операцій, які діють на матриці (вони не називають це "віртуальними функціями", але це все). Існують і інші області в типових кодах кінцевих елементів, де використовується цей принцип дизайну програмного забезпечення ОО. Ті, що спадають на думку, - це багато форм квадратурних формул і безліч видів кінцевих елементів, всі вони природно представлені як один інтерфейс / безліч реалізацій. Описи матеріального права також потрапляли б до цієї групи. Але може бути правдою, що мова йде про це, і що решта коду кінцевих елементів не використовує успадкування настільки широко, як це можна використовувати, скажімо, у графічних інтерфейсах.

Тільки з цих трьох пунктів повинно бути зрозуміло, що об'єктно-орієнтоване програмування, безумовно, застосовується і до числових кодів, і що було б нерозумно ігнорувати багато переваг цього стилю. Це може бути правдою, що BLAS / LAPACK не використовують цю парадигму (і звичайний інтерфейс, який піддається MATLAB, теж немає), але я б ризикну здогадатися, що кожне успішне числове програмне забезпечення, написане за останні 10 років, є насправді, об’єктно-орієнтований.


16

Інкапсуляція та приховування даних є надзвичайно важливими для розширюваних бібліотек наукових обчислень. Розглянемо матриці та лінійні розв’язувачі як два приклади. Користувачеві потрібно просто знати, що оператор лінійний, але він може мати внутрішню структуру, таку як розрідженість, ядро, ієрархічне подання, тензорний добуток або доповнення Шура. У всіх випадках методи Крилова не залежать від деталей оператора, вони залежать лише від дії MatMultфункції (а можливо, і суміжності). Аналогічно, користувач лінійного інтерфейсу розв'язувача (наприклад, нелінійного розв'язувача) піклується лише про те, що лінійна задача вирішена, і не повинен потребувати або бажати вказувати алгоритм, який використовується. Дійсно, конкретизація таких речей заважала б можливості нелінійного вирішувача (або іншого зовнішнього інтерфейсу).

Інтерфейси хороші. Залежно від реалізації це погано. Незалежно від того, чи досягаєте ви цього за допомогою класів C ++, об'єктів C, типів класів Haskell або будь-якої іншої функції, мова не має значення. У наукових бібліотеках важливі можливості, надійність та розширюваність інтерфейсу.


8

Класи слід використовувати лише в тому випадку, якщо структура коду є ієрархічною. Оскільки ви згадуєте Алгоритми, їх природна структура - це блок-схема, а не ієрархія об'єктів.

У разі OpenFOAM алгоритмічна частина реалізується з точки зору загальних операторів (div, grad, curl тощо), які є в основному абстрактними функціями, що працюють на різних типах тензорів, використовуючи різні типи числових схем. Ця частина коду в основному побудована з безлічі загальних алгоритмів, що працюють на класах. Це дозволяє клієнту написати щось на зразок:

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

Ієрархії, такі як транспортні моделі, моделі турбулентності, схеми розрізнення, градієнтні схеми, граничні умови тощо, реалізуються з точки зору класів C ++ (знову ж таки, загальних щодо тензорних величин).

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

Ієрархічна структура ==> класи

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


5

Навіть якщо це старе питання, я думаю, що варто згадати особливе рішення Юлії . Ця мова робить "OOP без класу": основними конструкціями є типи, тобто складені об'єкти даних, схожі на structs в C, для яких визначено відношення успадкування. Типи не мають "функцій-членів", але кожна функція має підпис типу і приймає підтипи. Наприклад, ви могли б мати абстрактний Matrixтип і підтипи DenseMatrix, SparseMatrixі мають загальний метод do_something(a::Matrix, b::Matrix)зі спеціалізацією do_something(a::SparseMatrix, b::SparseMatrix). Для вибору найбільш підходящої версії для виклику використовується багаторазове відправлення .

Цей підхід є більш потужним, ніж на основі класу OOP, який еквівалентний диспетчеризації на основі спадкування лише на першому аргументі, якщо ви приймаєте умову про те, що "метод є функцією з 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()f(x,y,z)zset_z()zcalculate_f()z

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.