4

I want a general solution on how to avoid insanely high number of constructor arguments. The examples I'm providing here are just examples, and I don't want a specific answer for the exact example here. That being said, my problem is obviously having too many arguments in my constructor.

I have a base class for any type of person (soldiers, wizards, merchants, etc.) called Person. My Person class is fairly simple, it implements the basic things common for every person there is. Let's say I have the following attributes, and my constructor takes an argument for each of these attributes:

  • string firstName
  • string surname
  • uint health
  • uint maxHealth - we don't want anyone to have 999999999 health...
  • uint movementSpeed - and not everyone runs at same speed, right?

So the constructor would look like:

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)
{}

Now imagine a new class, deep down in the inheritance tree, called a Wizard. Most wizards are there for combat, so they can also attack, etc. Wizard's constructor could look like this:

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)
{}

There could be even more arguments, but I suppose you get the point. That might not even look bad, but imagine looking at a stranger's code and seeing something like this:

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

Someone might say that "it's still not that bad, you can just read the docs!" Imo. that is terrible and makes the code insanely hard to read and even write. Also the order of the arguments gets hard for one to keep track of.

There are few solutions I've thought of, but they have their downsides. One thing would be to use default constructor and give it no arguments at all, then use setters to do the job:

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

Of course this would create dozens of extra lines of text, and some people are hardly against getters and setters. It would get rid of the numbers with no meanings tho, atleast you could read what each number does (wiz->setHealth(100); obviously tells we're setting health here).

Another solution would be to combine some attributes into structs. I could easily merge firstName, surname and wizardName (even better, use nickname) into a Name class or struct. This would reduce the number of my arguments in this example, but as I said, I want a general answer for such a problem. There could still be awfully many arguments even after combining some of them, or you might not be able to combine any cause they're not similar at all.

Skamah One
  • 2,456
  • 6
  • 21
  • 31

6 Answers6

2

You could use "Fluent Constructors"

See for example this link: http://richarddingwall.name/2009/06/01/fluent-builder-pattern-for-classes-with-long-ish-constructors/

Marwijn
  • 1,681
  • 15
  • 24
2

Group those arguments into data structures that collect reasonably related information, and let your constructor accepts those data structures.

The trivial solution of grouping everything into one single data structure would make your constructor accept only one argument, but would simply move the problem to the constructor of the data structure (provided you want to define one).

Therefore, you need to find the correct balance so that your constructor will accept a reasonable number of arguments (data structures) - definitely no more than 5 I would say - and each data structure groups together pieces of information that "belong to each other".

Now since you asked for an abstract answer, this is as far as I can go if I want to stay absolutely general. Concerning a concrete example, you could have:

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

And then your Wizard constructor would look like:

Wizard::Wizard(name info const& ni, health_info const& hi, fight_info const& fi)
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
2

This is a common problem in unit tests. A good test is readable, and as you've noted, a string of magic numbers is anything but. One recommended practice is the introduction of "explanatory variables". Use the parameter names as local variables in the test class.

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

Now, when I look at that constructor call, I know exactly what it's testing because it's spelled out in front of me.

And before the premature optimizers complain prematurely, this practice doesn't add anything extra to the production program's size or speed. Every optimizing compiler out there will optimize this call into a bunch of literals under the covers. All this coding style does is impact maintainability by making the code clearer and more readable.

John Deters
  • 4,295
  • 25
  • 41
  • So simple, yet so ingenious! I'll totally use this someday! :) Still, the builder mechanism seems better since it removes the importance of parameters' order. – Skamah One May 16 '13 at 14:00
  • Absolutely, if the language supports it, use it because it's clean. The goal is simply to make the code readable. The same basic idea would work if you were following the suggestions to fill a struct, because you'd have a readable `struct.name=value;` construct for each element, and filling a struct is order independent. – John Deters May 16 '13 at 14:40
1

In addition to what others have suggested, to improve the clarity of your parameters, you may want to try something like below. The wrappers' explicit constructors will prevent someone from passing int implicitly to the Stats constructor, so that...

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;
};
  • Why would you use `_` prefix for member variables? Some underscore prefixed variables are reserved (e.g. `_L` is reserved by visual studio) and `_` makes it much harder to read. Please switch over to `m_` or `this->`, or simply don't use a prefix at all. – Skamah One May 15 '13 at 16:02
0

I think you'll find that if you categorize your arguments and group them into structs based on those categories you won't have many arguments. You can also do this hierarchically if you need to.

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

You can also just store the info in your class using these groups.

David
  • 27,652
  • 18
  • 89
  • 138
0

I always liked the builder pattern for solving this issue, but almost never use it because it can't ensure at compile time that all arguments have been included.

This solution is a bit messy, but gets the job done. It would be a good option especially in cases where code-gen is done.

#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;
}
Catskul
  • 17,916
  • 15
  • 84
  • 113