面向对象还是顺序?

时间:2008-11-25 19:31:28

标签: c++ oop

我在main()中重构了500行的C ++代码来解决微分方程。我想把我们的求解器的重要思想封装成更小的函数(即“SolvePotential(...)”而不是50行的数字代码)。

我应该使用非常长参数列表的一系列函数按顺序编码,例如:

int main(int *argc, void **argv){
   interpolate(x,y,z, x_interp, y_interp, z_interp, potential, &newPotential);
   compute_flux(x,y,z, &flux)
   compute_energy(x,y,z, &eng)
   ...
   // 10 other high-level function calls with long parameter lists
   ...
   return 0;
}    

或者我应该创建一个像这样调用的“SolvePotential”类:

int main(int *argc, void **argv){
   potential = SolvePotential(nx, ny, nz, nOrder);
   potential.solve();
   return 0;
}

我将在SolvePotential中定义使用成员变量而不是长参数列表的函数,例如:

SolverPotential::solve(){
  SolvePotential::interpolate()
  SolverPotential::compute_flux()
  SolverPotential::compute_energy()
  // ... 
  //  10 other high-level function calls with NO parameter lists (just use private member variables)
}

在任何一种情况下,我都怀疑我会非常重复使用这些代码......实际上,我只是在重构以帮助提高代码清晰度。

也许这就像争论“它是'12'还是'打了十几个'?”,但你怎么看?

13 个答案:

答案 0 :(得分:5)

按顺序编写,然后重构,如果有什么你认为可以重复使用或者会更清楚。

此外,SolvePotential类没有多大意义,因为一个类应该是一个具有SolvePotential方法的Object。

答案 1 :(得分:3)

“SolvePotential”是一个动词,类往往是附有动词的名词。我不太了解你的问题的细节,但这可能表明程序方法比OO更明确。无论如何,看起来你确实创建了这个类,它只不过是为函数打包。

除非我有第二个使用该类的位置,否则我只是使用显式参数声明函数 - 这将比使用类上的方法更清楚(特别是对于第一次查看此代码的新人)这需要隐藏状态。

答案 2 :(得分:3)

都不是。 “将我的所有代码从一个单独的函数移动到一个单独的类”不是OOP。 OOP的基本规则之一是,一个类应该具有一个责任区域。 这不是一个单一的责任,大约是15:

SolverPotential::solve(){
SolvePotential::interpolate()
SolverPotential::compute_flux()
SolverPotential::compute_energy()
// ... 
//  10 other high-level function calls with NO parameter lists (just use private member variables)
}

这也使得维护类不变量几乎是不可能的,不是吗?什么时候调用compute_flux有效?解决?插?什么阻止我以错误的顺序做这件事?如果我这样做,班级是否会处于有效状态?我会从中获得有效数据吗?

然而,为什么它是 - 或?为什么不能多个函数?

// This struct could be replaced with something like typedef boost::tuple<double,double,double> coord3d
struct coord3d {
double x, y, z;
};

coord3d interpolate(const coord3d& coord, const coord3d& interpolated, double potential); // Just return the potential, rather than using messy output parameters
double compute_flux(const coord3d coord&flux); // Return the flux instead of output params
double compute_energy(const coord3d& coord); // And return the energy directly as well

当然,这些功能不一定是功能。如果必要/方便的话,每个都可以成为一个类,或者可能更好,一个函子,以保持必要的状态,并且可能允许你有效地将它们作为参数传递给其他函数。

如果最佳性能很重要,您可能必须小心直接返回较大的结构,而不是使用输出参数,但我肯定会先查询,看它是否有问题,即使它是,你可能会避免使用表达模板输出参数。

如果你有一个可以执行许多独立操作的概念对象,它可能暗示你需要一些OOP,它应该被建模为一个具有许多成员函数的类,当然每一个都是无论他们如何,何时以及为何被召唤,都要保持班级不变。

如果您需要编写许多功能,将它们粘合在一起以形成新的,更大的功能,功能编程和仿函数,这些都是您所需要的。期望可组合函数的一个常见原因(但绝对不是唯一的)是,如果您需要在许多不同的数据集上执行相同的操作(甚至可能是几种不同的类型,都实现相同的概念)。使编译器完成繁重的操作允许它与std :: transform或std :: for_each一起使用。 您可能还希望使用currying来逐步组合函数(也许某些函数可以使用一组固定参数进行参数化,这些参数在调用之间不会有所不同)。再次,创建一个使用这些固定参数初始化的仿函数,然后在operator()中提供变化的数据。

最后,如果您只需要对某些可变数据执行序列操作,那么普通的过程编程可能最适合您的需求。

最后,使用泛型编程,模板化必要的类和函数,使它们能够一起工作,而不必跳过像指针间接或继承那样的箍。

不要太挂OOP。使用您可以使用的工具。

我对你的问题的背景知之甚少,但我觉得你真正需要的不是一个类,它只是一个功能层次结构。 您的用户代码调用solve()。 solve()内部调用,比如说(为了示例而构成),interpolate()和compute_energy()。 compute_energy()在内部调用compute_flux(),依此类推。每个函数只进行几次调用以执行构成函数职责的逻辑步骤。所以你没有一个庞大的班级,有十几个不同的职责,或者是一个庞大的整体功能,可以顺序完成所有工作。

在任何情况下,“非常长的参数列表”都没有错(你通常可以通过将它们中的一些组合在一起来缩短它们,但即使你不能,也没有任何“un-OOP”关于传递一个很多参数。相反,它意味着函数很好地封装了其他所有东西。它所需要的只是在参数中传递,所以它并没有真正与应用程序的其余部分相关联。

答案 3 :(得分:2)

实际上C ++不仅仅是一种OO语言,它混合了其他范例,包括程序范式。能够使用类不会使它们更适合任何问题。

在我看来,函数在这里更有意义,因为您实现的数学过程不是基于状态而不需要重用数据。 在这里使用OO意味着构造对象只是为了调用一个方法然后销毁它们。对于我而言,这听起来比程序API更容易出错且更不直观。另外,正如bradheintz所说,一个明确的参数列表也消除了在实际使用它之前必须记住初始化类的问题(重构时的典型错误)。

顺便说一下,在函数方面,使用返回值而不是i / o参数通常会使API看起来更清晰。

我甚至敢说你可能想要混合OO和程序,使用类似概念的类,比如Vectors(我看到一些x,y,z)。如果您非常关注这一点,那么这也会删除一些参数。

float SolvePotential(const Vector3& vn, float nOrder)
{
    // ...
    const float newPotential = interpolate(vn, v_interp, potential);
    const float flux         = compute_flux(vn);
    const float energy       = compute_energy(vn);
    // ...
    return result;
}

最后,你不提性能,所以我想你不介意。但是如果你这样做,在这种情况下,使用程序方法比使用OO更快更容易和清理

希望它有所帮助!

答案 4 :(得分:1)

我为这个类投票,因为它将数据包装在一个更整洁的包中,并使你的main()函数非常清晰。

从某种意义上说,你已经清理了main()函数,现在有了一个凌乱的类,你可以自行决定进一步清理它。一种分而治之的方法。或者也许是“阁楼上所有垃圾填塞”的方法,至少房子里最常用的部分是干净的。

答案 5 :(得分:0)

由于您不希望此时在其他地方重用代码,因此我将专注于使代码可读且干净。通过这种方式,当你需要再次求解微分方程或者当你发现实际上想要重用代码时,你可以弄清楚它一年后会做些什么。恕我直言,带参数列表的函数现在似乎是一个很好的方法。如果你发现它太笨重或者你确实需要重复使用它,那么在这一点上去对象路径会很有意义。

答案 6 :(得分:0)

如果您正在进行严格的MartinFowler-esque重构,那么您就是在正确的轨道上。寻找代码的隐喻,定义职责并创建符合这些分歧的类。如果您创建具有清晰且易于理解的名称的类和成员,这应该使代码更具可读性。

你可能想要创建一些参数对象来传递参数而不是长参数列表。

我在Java中工作了很长时间,现在在C / C ++中工作。你有没有看看周围是否有500行主要方法?实例化对象和检查虚拟表等会有开销。性能是一个问题吗?看起来它会像这样的数学计算。如果是这样的话,那么所有的赌注都会以Martin-Fowler式的方式进行。

祝你好运。

答案 7 :(得分:0)

你忽略了提到一个中间层,即开始编写一个对象,但是将参数传递给它的方法。伪代码-LY:

SolverPotential::solve(a, b, c, d){
  SolvePotential::interpolate(a, b);
  SolverPotential::compute_flux(b, c);
  SolverPotential::compute_energy(c, d)

通过这种方式,您可以通过思考(可能更简单)“我需要手头解决这一步骤的顺序模式”开始您的重构?此外,也许你会看到参数和延迟的序列,它们表明了对象的划分(“在这一步之后我再也不会使用'a'了。也许前两个步骤应该被封装在一个不同的类中。”)

此外,当你只有一个“大包”的实例变量时,并行化就更难了。如果您使用显式参数开始工作,您将更好地了解依赖关系,并且您可以找到可以轻松并行化的无依赖性步骤。

如果你有大量的参数,重构实例变量(和多个对象)是有意义的,但我建议你的初始步骤结合这些方法。

答案 8 :(得分:0)

既然你提到你的一个目标是封装,那么我会指出你的OO方法。

在您的示例代码中,我认为您的类名有点偏差。我首先将你正在进行的重构(Extract Method)应用到较小的函数中。之后,分析哪些数据与哪些逻辑和转换过程设计到对象(Fowler没有这个“大重构”的链接)。

这是自下而上的方法。我将采用自上而下的方法是Command pattern,这基本上就是你在原始问题中所拥有的,保存可怜的类名:创建一个名为PotentialEquation的类,通过构造函数,工厂或setter初始化它,无论你喜欢什么,然后暴露一个solve()方法,也许是这样的:

PotentialSolution solve()

在PotentialSolution中,您可以进一步将解决方案封装到等式中,这可能比原始类型更复杂。

答案 9 :(得分:0)

我并不是真的推荐这种设计作为一种实用的方法,但作为这种域的可能解决方案,它可能非常有趣。

如果要将差分解算器实现为创建的不可变类的集合,然后使用一些方便的无计算“compute()”方法来调用,使用实例变量和存储来计算与该类关联的值并返回答案。然后,您就可以在每个类中构建一个缓存机制,这样如果您已经为相同的参数计算了它,就不必重新评估答案。

我担心我不懂C ++语法,所以我会改用Java。

public class ValuePlusOne implements Computable {
    private int value;
    private int result;
    private Boolean hasRun;
    private static Map instanceMap = new HashMap();

    // Creates an instance reusing an existing one if possible
    public static getInstance(int value) {
        ValuePlusOne instance = (ValuePlusOne)instanceMap.get(value);

        if (instance = null) {
            instance = new ValuePlusOne(value);
            instanceMap.put(value,instance);
        }
        return instance;
    }

    // Private constructor
    private ValuePlusOne(int value) {
        this.value = value;
        hasRun = false;
    }

    // Computes (if not already computed) and returns the answer
    public int compute() {
        if (!hasRun) {
            hasRun = true;
            result = value + 1;
        }

        return result;
    }
}

这意味着您将能够无形地重用之前已完成的任何计算。如果您经常使用相同的参数重做计算,并且在差异的(近似)连续域中这可能很少发生,这只会使您加快速度。这种方法也适用于并行化,但需要对其进行修改以确保安全。

除非缓存提供了真正的好处,否则我宁愿使用扁平的C风格的程序方法。任何人都可以使用无状态数据输入数据方法轻松阅读和理解您的代码。

答案 10 :(得分:0)

听起来我觉得这是一个接受输入的函数(虽然它可能很大)并返回一个输出(可能很大)。听起来我觉得你愿意采用功能方法,但是担心长参数列表。

在这种情况下,我认为你做的不仅仅是繁忙的工作,试图以某种方式将其转化为对象。我会考虑使用一些结构来保存相似的参数,并将这些结构传递给函数而不是单个参数。我喜欢将这个庞大的方法分解为子方法的想法,但是对我来说听起来不像是绑定到一个类上,尝试这样做可能需要做更多的工作,没有太多明显的好处。

如果在将函数分解为子函数并将参数合并到结构体中时,你会在那里看到一个好的对象,去寻找它,但如果问题没有,我就不会花时间试图将它变成一个。那里有吸引力。

答案 11 :(得分:0)

为清楚起见,我将注释解释我在做什么的代码,并在适当的空格下使用可读的单个函数。对于封装来说,类不是一个坏主意。我要做的是将步骤设为私有方法,这样就不会孤立地或以错误的顺序调用它们。然后有一个公共的计算方法,并使其以正确的顺序执行所有步骤(调用私有方法)。这是假设您不期望中间方法的重用。我还认为拥有记录良好的长函数可以处理整个事情也没错。无论选择什么理论/常规,Id都会选择对我来说最合适的方法。

答案 12 :(得分:-1)

由于你使用的是面向对象的语言,你应该选择对象,假设你将来可以重用

只有一条建议:尝试设计一个好的类图。你有什么实体计划?我会看到一个带有派生“differentialEquation”的“等式”类,依此类推。