构造函数应该接受参数还是应该创建setter

Should constructors accept parameters or should I create setters?

本文关键字:创建 setter 该接受 参数 构造函数      更新时间:2023-10-16

我有两个选项。要么创建一个在构造函数中接受大量参数的类,要么创建大量的setter方法和init方法。我不确定哪一个是首选选项,是否应该在构造函数中接受一些参数,而其他参数可以通过setter手动设置?还是我想得太多了?

这也是我提出的一个相关问题:成员名称和构造函数参数名称之间的冲突。

如果在创建一个对象后,必须调用setinit才能实际使用它……那么,这只是一个糟糕的设计。

如果对象在没有按照您希望的方式初始化某些成员的情况下是可用的,您可以稍后设置它们。

这里的黄金法则是-如果您创建了一个对象,您应该能够在不进行任何其他初始化的情况下使用它

扩展答案:

假设你有一个有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"对象的静态函数,在这个过程中更改它的一部分。这样,您总是有一致的对象(不变量可以),以及更短的构造函数或函数调用。

我同意棘轮怪胎对构建器模式的建议,只是有一个折衷之处,即典型的构建器模式不提供编译时检查来确保所有参数都包含在内,并且最终可能会生成一个不完整/不正确的对象。

这对我来说是一个足够大的问题,我制作了一个编译时检查版本,如果你能原谅额外的机器,它可能会为你完成这项工作。(当然也有一些优化)

#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/编辑器,我不愿意通过构造函数传递大量信息。在这种情况下,您可能会在每个重构步骤中完成大量工作,这可能会导致编译器无法捕获的错误。

如果您计划使用一个集成良好的对象集(例如,通过强烈使用设计模式),这些对象不跨越一个大型层次结构,而是具有强大的迭代能力,那么通过构造函数传递更多数据是一件好事,因为更改一个对象构造函数不会破坏所有子构造函数。

如果该设置是必需的,并且不能给定默认值,则在构造函数中使其成为必需的。这样你就知道它会被设置好。

如果该设置不是必需的,并且可以给它一个默认值,那么为它创建一个setter。这会使构造函数变得简单得多。

例如,如果你有一个发送电子邮件的类,那么构造函数中可能需要"To"字段,但其他所有内容都可以在setter方法中设置。

我的经验表明,构造函数中有参数,而不是getter和setter。如果你有很多参数,它建议可以默认可选参数,而必需/强制性参数是构造函数参数。