使用类封装数值算法的固有优点和缺点是什么?


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
当您的班级名称是形容词而不是名词时,这始终是一个不好的信号。
David Ketcheson

3
类可以用作组织函数的无状态命名空间,以管理复杂性,但是还有其他方法可以通过提供类的语言来管理复杂性。(我想到了C ++中的命名空间和Python中的模块。)
Geoff Oxberry 2012年

@GeoffOxberry我不能说这是好用法还是坏用法-这就是为什么我首先要问的问题-但是与名称空间或模块不同,类也可以管理“临时状态”,例如网格层次结构在多网格中,算法完成后将其丢弃。
2012年

Answers:


13

从事数值软件工作已有15年,我可以明确地说出以下几点:

  • 封装很重要。您不希望传递数据指针(如您建议的那样),因为它公开了数据存储方案。如果公开了存储方案,则永远无法再次更改它,因为您将访问整个程序中的所有数据。避免这种情况的唯一方法是将数据封装到类的私有成员变量中,并仅让成员函数对其进行操作。如果我读了您的问题,您会想到一个函数,该函数将矩阵的特征值计算为无状态,将指向矩阵条目的指针作为参数并以某种方式返回特征值。我认为这是错误的思考方式。在我看来,该函数应该是类的“ const”成员函数-不是因为它更改了矩阵,而是因为它是一个处理数据的函数。

  • 大多数OO编程语言都允许您具有私有成员函数。这是将一种大型算法分解为一种较小算法的方法。例如,特征值计算所需的各种辅助函数仍在矩阵上运行,因此自然是矩阵类的私有成员函数。

  • 与许多其他软件系统相比,类层次结构通常不如在图形用户界面中重要。数值软件中肯定有一些突出的地方-Jed对此线程的另一种回答进行了概述,即人们可以用多种方式表示矩阵(或更普遍地,表示有限维向量空间上的线性算子)。PETSc非常一致地执行此操作,并为所有对矩阵起作用的操作提供了虚拟函数(它们不称为“虚拟函数”,但这就是事实)。在典型的有限元代码中还有其他领域可以使用OO软件的这种设计原理。我想到的是多种正交公式和多种有限元,所有这些自然都表示为一个接口/许多实现。物质法的描述也属于这一类。但这可能就是事实,并且有限元素代码的其余部分并未像在GUI中那样广泛地使用继承。

仅从这三点来看,应该清楚的是,面向对象的编程也绝对适用于数字代码,而忽略这种风格的许多好处是愚蠢的。BLAS / LAPACK可能没有使用这种范例(MATLAB公开的常用界面也没有),但我敢猜测,过去10年编写的每个成功的数值软件实际上都是面向对象。


16

封装和数据隐藏对于科学计算中的可扩展库极为重要。考虑将矩阵和线性求解器作为两个示例。用户只需要知道算子是线性的,但是它可能具有内部结构,例如稀疏性,核,分层表示,张量积或Schur补码。在所有情况下,Krylov方法都不依赖于运算符的细节,它们仅取决于MatMult函数的作用(可能还取决于它的伴随物)。类似地,线性求解器界面(例如非线性求解器)的用户仅关心线性问题的求解,而不需要或不想指定所使用的算法。实际上,指定此类内容会阻碍非线性求解器(或其他外部接口)的功能。

界面很好。依靠实现是不好的。无论您是使用C ++类,C对象,Haskell类型类还是其他某种语言功能实现的,都是无关紧要的。接口的功能,健壮性和可扩展性在科学图书馆中至关重要。


8

仅当代码的结构是分层结构时,才应使用类。既然您提到的是算法,则它们的自然结构是流程图,而不是对象的层次结构。

对于OpenFOAM,算法部分是根据通用运算符(div,grad,curl等)实现的,这些运算符基本上是使用不同类型的数值方案在不同类型的张量上运行的抽象函数。这部分代码基本上是由许多对类进行操作的通用算法构建的。这允许客户端编写如下内容:

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

诸如运输模型,湍流模型,差分方案,梯度方案,边界条件等层次结构是根据C ++类(同样,在张量上是通用的)实现的。

我注意到CGAL库中的结构类似,其中各种算法作为功能对象的组打包在一起,这些对象与几何信息捆绑在一起以形成几何核(类),但是再次这样做是为了将操作与几何分离(从点数据类型的人脸)。

层次结构==>类

程序流程图==>算法


5

即使这是一个老问题,我也认为值得一提朱莉娅的特殊解决方案。这种语言的作用是“无类的OOP”:主要构造是类型,即类似于structC中s的复合数据对象,在其上定义了继承关系。这些类型没有“成员函数”,但每个函数都有一个类型签名并接受子类型。例如,你可以有一个抽象Matrix型和亚型DenseMatrixSparseMatrix和有一个通用的方法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

OO方法的两个优点可能是:

  • 漫长的计算,您可能会或可能不会想要不同的结果。例如,如果是最终输出,但它取决于中间结果,则可以有一个在实例内部缓存结果的方法。然后,当您调用时,它还会调用是否尚未缓存结果。α α αβαcalculate_alpha()αcalculate_beta()calculate_alpha()α

  • 包含多个输入的计算,如果一个输入发生更改,则不必完全完成整个计算。例如,该calculate_f()方法返回。然后,如果您决定对另一个值重做计算,则可以调用并将参数在内部标记为'dirty',以便再次调用时,仅重做依赖的计算部分。z z zf(x,y,z)zset_z()zcalculate_f()z

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.