我有两个选择。要么创建一个在其构造函数中接受许多参数的类,要么创建许多 setter 方法和一个 init 方法。我不确定哪个是首选选项,是否应该在构造函数中接受某些参数,而其他参数可以通过 setter 手动设置?还是我想太多了?
这是一个相关的问题,也是由我提出的:Conflicts between member names and constructor argument names。
我有两个选择。要么创建一个在其构造函数中接受许多参数的类,要么创建许多 setter 方法和一个 init 方法。我不确定哪个是首选选项,是否应该在构造函数中接受某些参数,而其他参数可以通过 setter 手动设置?还是我想太多了?
这是一个相关的问题,也是由我提出的:Conflicts between member names and constructor argument names。
如果在创建对象后必须调用set
或init
实际使用它……嗯,这只是一个糟糕的设计。
如果对象在没有按照您希望的方式初始化某些成员的情况下可用,则可以稍后设置它们。
这里的黄金法则是——如果你创建了一个对象,你应该能够在不进行任何其他类型的初始化的情况下使用它。
假设您有一个形状,它有 10 个边、10 个角、一种颜色和一个名称,可以连接到不同的形状。构造函数应如下所示:
MyShape(Point c1, Point c2,...., Point c10, Color c, Name n)
如您所见,我省略了连接形状,因为NULL
如果当前对象未连接,则可以将其设置为。但是,在没有任何其他参数的情况下,对象无效,因此应在构造函数中设置它们。
可能的重载(或者默认参数)可以是:
MyShape(Point c1, Point c2,...., Point c10, Color c, Name n,
MyShape* connectedShape /*=NULL*/)
您应该为保持类不变性所必需的所有成员提供构造函数参数。换句话说,对象从创建到销毁的那一刻都应该处于有效且一致的状态。其他一切都在招惹麻烦。
话虽如此,有时会做出让步,例如在需要调用虚拟方法以提供类型特定初始化的层次结构的情况下。通常,这可以通过使用模板类/方法(即静态多态性)来避免
如果有不影响类不变量的类成员,可以稍后通过 setter 或其他方法设置它们。
构建器模式将在这里有所帮助,还可以尝试合并参数以使它们在构建器的设置过程中有意义
根据经验,有很多构造函数参数是一个类做得太多的标志,所以先试着把它分成更小的类。
然后尝试将一些参数分组到更小的类或结构中,每个类或结构都有自己的、更简单的构造函数。
如果你有合理的默认值,你可以使用一个构造函数,它只为构造一个新对象时绝对必须给出的值提供参数,然后添加 setter,或者使用复制“starter”对象的静态函数,改变它的一部分进行中。这样,您始终拥有一致的对象(不变量 OK),以及更短的构造函数或函数调用。
我同意棘轮怪胎对构建器模式的建议,但需要权衡的是,典型的构建器模式不提供编译时检查以确保包含所有参数,并且您最终可能会得到不完整/不正确的构建的对象。
这对我来说是一个足够大的问题,我制作了一个编译时检查版本,如果你能原谅额外的机器,它可能会为你完成这项工作。(当然也有优化)
#include <boost/shared_ptr.hpp>
class Thing
{
public:
Thing( int arg0, int arg1 )
{
std::cout << "Building Thing with \n";
std::cout << " arg0: " << arg0 << "\n";
std::cout << " arg1: " << arg1 << "\n";
}
template <typename CompleteArgsT>
static
Thing BuildThing( CompleteArgsT completeArgs )
{
return Thing( completeArgs.getArg0(),
completeArgs.getArg1() );
}
public:
class TheArgs
{
public:
int arg0;
int arg1;
};
class EmptyArgs
{
public:
EmptyArgs() : theArgs( new TheArgs ) {};
boost::shared_ptr<TheArgs> theArgs;
};
template <typename PartialArgsClassT>
class ArgsData : public PartialArgsClassT
{
public:
typedef ArgsData<PartialArgsClassT> OwnType;
ArgsData() {}
ArgsData( const PartialArgsClassT & parent ) : PartialArgsClassT( parent ) {}
class HasArg0 : public OwnType
{
public:
HasArg0( const OwnType & parent ) : OwnType( parent ) {}
int getArg0() { return EmptyArgs::theArgs->arg0; }
};
class HasArg1 : public OwnType
{
public:
HasArg1( const OwnType & parent ) : OwnType( parent ) {}
int getArg1() { return EmptyArgs::theArgs->arg1; }
};
ArgsData<HasArg0> arg0( int arg0 )
{
ArgsData<HasArg0> data( *this );
data.theArgs->arg0 = arg0;
return data;
}
ArgsData<HasArg1> arg1( int arg1 )
{
ArgsData<HasArg1> data( *this );
data.theArgs->arg1 = arg1;
return data;
}
};
typedef ArgsData<EmptyArgs> Args;
};
int main()
{
Thing thing = Thing::BuildThing( Thing::Args().arg0( 2 ).arg1( 5 ) );
return 0;
}
这取决于你在做什么。通常最好在构造函数中设置一些东西,因为这些有助于塑造对象在其生命周期后期的使用方式。一旦创建了对象(例如计算因子或文件名),更改值也可能会产生影响,这可能意味着您必须提供重置对象的功能 - 非常混乱。
有时会提供一个在构造函数之后调用的初始化函数的参数(当调用纯虚拟函数时,很难直接从构造函数初始化),但是您必须保留对象状态的记录,这会增加更多复杂性。
如果对象是一个直接的无状态数据容器,那么访问器和修改器可能没问题,但它们会增加大量维护开销,而且很少全部使用。
我倾向于坚持在构造函数中设置你的值,然后添加访问器以及何时允许你只读访问你可能需要的参数。
这取决于您的架构和工具:
如果您计划开发/原型化大型 OO 层次结构,如果您没有好的 IDE/编辑器,我将不愿意通过构造函数传递大量信息。在这种情况下,您可能会在每个重构步骤中完成大量工作,这可能会导致错误,而不会被编译器捕获。
如果您计划使用一组集成良好的对象(例如,通过强烈使用设计模式),这些对象不跨越一个大层次结构,而是具有强大的迭代,通过构造函数传递更多数据是一件好事,因为更改一个对象构造函数确实不要破坏所有子构造函数。
如果该设置是必需的且无法赋予默认值,请在构造函数中将其设为必需。这样你就知道它实际上会被设置。
如果不需要设置并且可以给定一个默认值,请为其创建一个设置器。这使构造函数变得更加简单。
例如,如果您有一个发送电子邮件的类,则构造函数中可能需要“To”字段,但其他所有内容都可以在 setter 方法中设置。
我的经验指出我在构造函数中使用参数而不是 getter 和 setter。如果您有很多参数,则建议可以默认可选参数,而必需/强制参数是构造函数参数。