在最深层的继承类中构造函数参数太多

Too many constructor arguments in deepest class of inheritance?

本文关键字:构造函数 参数 太多 继承      更新时间:2023-10-16

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

我为任何类型的人(士兵、巫师、商人等)都有一个基类,叫做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");
...

当然,这会产生许多额外的文本行,有些人几乎不反对getter和setter。它将摆脱没有意义的数字,但至少你可以读取每个数字的作用(wiz->setHealth(100);显然告诉我们在这里设置生命值)。

另一个解决方案是将一些属性组合成结构体。我可以很容易地合并firstName, surnamewizardName(甚至更好,使用nickname)到Name类或结构体。这将减少我在这个例子中论证的次数,但正如我所说的,我想要一个关于这样一个问题的一般答案。即使把其中的一些结合起来,仍然会有非常多的论点,或者你可能无法把它们结合起来,因为它们根本不相似。

您可以使用"Fluent constructor "

请参见以下链接:http://richarddingwall.name/2009/06/01/fluent-builder-pattern-for-classes-with-long-ish-constructors/

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

将所有内容分组到一个数据结构中的简单解决方案将使构造函数只接受一个参数,但会简单地将问题转移到数据结构的构造函数(如果您想定义一个)。

因此,您需要找到正确的平衡,以便您的构造函数将接受合理数量的参数(数据结构)-我认为绝对不超过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)

这是单元测试中常见的问题。一个好的测试是可读的,正如您所注意到的,一串幻数绝不是可读的。一个推荐的做法是引入"解释变量"。使用参数名作为测试类中的局部变量。

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 );

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

在不成熟的优化器过早地抱怨之前,这种做法不会给生产程序的大小或速度增加任何额外的东西。每个优化编译器都会在后台将这个调用优化成一堆字面量。这种编码风格通过使代码更清晰、更易读来影响可维护性。

除了其他人的建议之外,为了提高参数的清晰度,您可能想尝试下面的方法。包装器的显式构造函数将阻止某人隐式地将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;
};

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

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

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

我一直喜欢用构建器模式来解决这个问题,但几乎从不使用它,因为它不能确保在编译时包含所有参数。

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

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