最深的继承类中的构造函数参数太多了?

时间:2013-05-15 14:59:03

标签: c++ design-patterns parameters constructor arguments

我想要一个关于如何避免疯狂大量构造函数参数的通用解决方案。 我在这里提供的示例只是示例,我不想在这里给出具体示例的具体答案。 话虽这么说,我的问题显然是在我的构造函数中有太多的参数。

我有一个名为Person的任何类型的人(士兵,巫师,商人等)的基类。 我的Person课程相当简单,它实现了每个人共同的基本事物。 假设我有以下属性,我的构造函数为每个属性接受一个参数:

  • string firstName
  • string surname
  • uint health
  • uint maxHealth - 我们不希望任何人拥有999999999健康......
  • uint movementSpeed - 并非所有人都以同样的速度跑,对吗?

所以构造函数看起来像:

Person::Person(const string &firstName="Missing first name",
               const string &surname="Missing surname",
               const uint health=100,
               const uint maxHealth=100,
               const uint movementSpeed=50) :
    m_firstName(firstName),
    m_surname(surname),
    m_health(health),
    m_maxHealth(maxHealth),
    m_movementSpeed(movementSpeed)
{}

现在假设一个新的类,在继承树的深处,称为Wizard。大多数巫师都在那里进行战斗,所以他们也可以攻击等。 Wizard的构造函数可能如下所示:

Wizard::Wizard(const string &firstName="Missing first name",
               const string &surname="Missing surname",
               const string &wizardName="Missing wizard name",
               const uint health=100,
               const uint maxHealth=100,
               const uint mana=100,
               const uint maxMana=100,
               const uint strength=50, // physical damage
               const uint witchcraft=50, // spell damage
               const uint armor=30, // vs. physical damage
               const uint resistance=30, // vs. spell damage
               const uint movementSpeed=50) :
    Warrior(firstName, surName, health, maxHealth, strength, armor, resistance, movementSpeed),
    m_wizardName(wizardName),
    m_mana(mana),
    m_maxMana(maxMana),
    m_witchcraft(witchcraft)
{}

可能会有更多的争论,但我想你明白了。 这可能看起来甚至不好看,但想象一下陌生人的代码并看到类似的东西:

Wizard *wiz = new Wizard("Tom", "Valedro", "Lord Voldemort", 300, 400, 200, 200, 10, 500, 30, 400, 300)

有人可能会说“它仍然没有那么糟糕,你可以只阅读文档!” 伊莫。这很糟糕,使代码难以阅读甚至写作。 此外,参数的顺序很难让人跟踪。

我想到的解决方案很少,但它们有缺点。 有一件事是使用默认构造函数并且根本不给它任何参数,然后使用setter来完成这项工作:

Wizard *wiz = new Wizard;
wiz->setFirstName("Tom");
wiz->setSurname("Valedro");
...

当然,这会产生数十个额外的文字行,有些人几乎不会反对吸气者和制定者。 它会摆脱没有任何意义的数字,至少你可以读出每个数字的作用(wiz->setHealth(100);显然告诉我们在这里设置健康。)

另一个解决方案是将一些属性组合到结构中。 我可以轻松地将firstNamesurnamewizardName(甚至更好,使用nickname)合并到Name类或结构中。 这会减少我在这个例子中的参数数量,但正如我所说,我想要一个解决这个问题的一般答案。 即使在将它们中的一些组合在一起之后,仍然可能存在非常多的争论,或者您可能无法将任何原因组合在一起它们根本不相似。

6 个答案:

答案 0 :(得分:2)

您可以使用“流利的构造函数”

例如,请参阅此链接:http://richarddingwall.name/2009/06/01/fluent-builder-pattern-for-classes-with-long-ish-constructors/

答案 1 :(得分:2)

将这些参数分组为收集合理相关信息的数据结构,并让构造函数接受这些数据结构。

将所有内容分组到一个单一数据结构中的简单解决方案将使您的构造函数只接受一个参数,但只是将问题移动到数据结构的构造函数(假设您要定义一个)。

因此,您需要找到正确的平衡,以便您的构造函数接受合理数量的参数(数据结构) - 绝对不超过5我会说 - 并且每个数据结构将“属于”的信息组合在一起彼此“。

既然你要求一个抽象的答案,如果我想保持绝对一般,这就是我可以去的。关于一个具体的例子,你可以:

struct name_info
{
    // Constructor(s)...

    const std::string firstName;
    const std::string surname;
    const std::string wizardName;
};

struct health_info
{
    // Constructor(s)...

    const uint health;
    const uint maxHealth;
    const uint mana;
    const uint maxMana;
};

struct fight_info
{
    // You got it...
};

然后你的Wizard构造函数看起来像:

Wizard::Wizard(name info const& ni, health_info const& hi, fight_info const& fi)

答案 2 :(得分:2)

这是单元测试中的常见问题。一个好的测试是可读的,正如你所指出的,一串魔术数字不过是。一种推荐的做法是引入“解释变量”。在测试类中使用参数名称作为局部变量。

string firstName("Tom");
string surname("Valedro");
string wizardName("Lord Voldemort");
uint health=300;
uint maxHealth=400;
uint mana=200;
uint maxMana=200;
uint strength=10; // physical damage
uint witchcraft=500; // spell damage
uint armor=30; // vs. physical damage
uint resistance=400; // vs. spell damage
uint movementSpeed=300;

Wizard *wiz = new Wizard( firstName, surname, wizardName, health, maxHealth, 
                          mana, maxMana, strength, witchcraft, 
                          armor, resistance, movementSpeed );

现在,当我查看构造函数调用时,我确切地知道它正在测试什么,因为它是在我面前拼写出来的。

在过早优化者过早抱怨之前,这种做法并没有为生产计划的规模或速度增加任何额外的东西。每个优化编译器都会将这个调用优化为一堆文字。所有这些编码风格都通过使代码更清晰,更易读而影响可维护性。

答案 3 :(得分:1)

除了其他人建议的内容之外,为了提高参数的清晰度,您可能需要尝试以下内容。包装器的显式构造函数将阻止某人隐式地将int传递给Stats构造函数,因此......

Stats a(10, 18, 19); // will not compile
Stats b(Health(10), Health(18), Mana(19)); // will compile

// int wrappers
struct Health {
  explicit Health(int v)
  : _val(v) 
  {}

  int _val;
};

struct Mana {
  explicit Mana(int v)
  : _val(v) 
  {}

  int _val;
};

struct MoveSpeed{
  explicit MoveSpeed(int v)
  : _val(v) 
  {}

  int _val;
};


struct Stats{
  Stats(const Health& maxhealth, const Health& curhealth, const Mana& maxmana/*... etc*/)
  : _maxhealth(maxhealth)
  , _curhealth(curhealth)
  , _maxmana(maxmana)
  // ... etc
  {}

  Health _maxhealth;
  Health _curhealth;
  Mana _maxmana ;
  // ... etc
};


class Warrior{
  public:
    Warrior(const Stats& stats /* ... etc */)
    : _stats(stats)
    // ... etc
    {}

  private:
    Stats _stats;
};

答案 4 :(得分:0)

我认为你会发现,如果你对你的论点进行分类并根据这些类别将它们分组成结构,你就不会有很多参数。如果需要,您也可以按层次结构执行此操作。

struct Stats
{
     uint health;
     uint maxHealth;
     uint mana;
     uint maxMana;
     uint strength;
     uint witchcraft;
     uint armor;
     uint resistance;
     uint movementSpeed;
};

class Wizard
{
public:
    static Stats defaultWizardStats;

    Wizard(Name name, Stats stats = defaultWizardStats)
    : m_Name(name)
    , m_Stats(stats)
    {}

private:
    Name m_Name;
    Stats m_Stats;
};

您也可以使用这些组将信息存储在班级中。

答案 5 :(得分:0)

我总是喜欢使用构建器模式来解决这个问题,但几乎从不使用它,因为它无法在编译时确保所有参数都已包含在内。

这个解决方案有点乱,但是完成了工作。这将是一个很好的选择,特别是在代码生成的情况下。

#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;
}