Jens Bodal
Jens Bodal

Reputation: 1757

Proper design setup for derived classes with common attributes but different values

So I can think of a few ways to do this but I feel like I am just retyping the same thing with each new subclass. Is there a design pattern where I can set up the full structure for my subclasses in a way where I reduce the amount of code needed for implementation (and also enforce proper implementation if possible?)

This seems simple but the ideas I've had don't work, and the best I've found is to either hard code in the parameters when calling the constructor or to set up new constants within each child class then use those.

What I currently have is something like this:

"parent.hpp"
class Parent {
    private:
        std::string name;
        int someValue;

    protected:
        Parent(std::string name, int someValue); // NOTE: There will be 7 parameters/attributes that need initial base values
        void setName(std::string name) { this->name = name; }
        void setSomeValue(int someValue) { this->someValue = someValue; }

    public:
        std::string getName() { return this->name; }
        int getSomeValue() { return this->someValue; }
};

"parent.cpp"
Parent::Parent(std::string name, int someValue) {
    setName(name);
    setSomeValue(someValue);
}

"child.hpp"
class Child : public Parent {
    public:
        Child();
};

"child.cpp - option 1"
static const std::string DEFAULT_NAME = "Jon";
static const int DEFAULT_SOME_VALUE = 100;

Child::Child() : Parent(DEFAULT_NAME, DEFAULT_SOME_VALUE) {
    // other stuff if needed
}

"child.cpp - option 2"
Child::Child() : Parent("Jon", 100) {
    // other stuff if needed
}

There will be virtual methods and such I'll add later, but for now I just want to know of the right design pattern for having (potentially) many subclasses. There are also more parameters that will be in common which are all int values. It would seem unclear to me to have the constructors be Child::Child("string", 1, 2, 3, 4, 5, 6) albeit it would be easier to implement new subclasses.

On the other hand if I am just retyping the boiler plate constants for the base values in each subclass, the constructors will be more descriptive, but there would be a lot of code reuse.

It would seem to me what I would want to do is have virtual protected constants in the Parent class which the Child classes would need to define, then call those from the constructors, but that is not allowed. Is one of the two options a better one? Is there a better "long-term" setup for this?

I looked through all of the Similar Questions and the closest I found was this: Proper way to make base class setup parent class. Though I'm not really sure if that idea would fix my issue or make anything clearer.

Another idea I had was to call pure virtual methods from the default constructor, but as I learned that is also not allowed.

Upvotes: 1

Views: 146

Answers (2)

Kenny Ostrom
Kenny Ostrom

Reputation: 5871

I would use another object to hold the state like Ami, although I would have done it for a different reason. Since the state is a separate class, you can fully construct it before the actual Parent and Child are constructed, and it can have its own hierarcy.

header

class Parent {
protected:
    struct ParentState {
        std::string name;
        int someValue;
    };

    Parent(ParentState);

    void setName(std::string name) { data.name = name; }
    void setSomeValue(int someValue) { data.someValue = someValue; }

public:
    std::string getName() { return data.name; }
    int getSomeValue() { return data.someValue; }

private:
    ParentState data;
};

class Child : public Parent {
    struct ChildDefaults : public Parent::ParentState {
        ChildDefaults();
    };

public:
    Child();
};

implementation

Parent::Parent(ParentState init) {
    // since you have setters, they should be used
    // instead of just data=init;
    setName(init.name);
    setSomeValue(init.someValue);
}

Child::ChildDefaults::ChildDefaults(){
    name = "Jon";
    someValue = 100;
}

Child::Child() : Parent(ChildDefaults()){
    // other stuff if needed
}

If you put the ParentState and ChildDefault classes in a separate file, you can use that file to put all the defaults in one place where you can easily look them up or change them. They also might be prettier if they are not hidden inside the classes, forcing the extra scope syntax.

addendum: To put the whole default settings heirarchy together in its own header, just move them all to one header. Be sure to do an include guard to avoid multiply defining the constructors.

#ifndef THE_DEFAULTS_H
#define THE_DEFAULTS_H

struct ParentState {
    std::string name;
    int someValue;
};

struct ChildDefaults : public Parent::ParentState {
    ChildDefaults() {
        name = "Jon";
        someValue = 100;
    }
};

// more default settings for other classes

#endif

Upvotes: 1

Ami Tavory
Ami Tavory

Reputation: 76346

Perhaps you could combine here two ideas:

  1. Avoiding a large number of args passed to a function in general (including a ctor).

  2. Method chaining.

(The first one is more fundamental here, and the second one is less essintial, and is here just for improved readability.)

In more detail:

Having any function, a ctor of a base class in particular, taking 7 parameters, seems very verbose & fragile. Suppose you realize that you needed to add another parameter. Would you now have to go over all the derived classes? That's problematic.

So let's start with something like:

class Parent
{
protected:
    explicit Parent(const ParentParams &params);
};

And ParentParams looks something like this:

class ParentParams
{
public:
     // Initialize with default stuff.
     ParentParams(); 

     // Changing only the foo aspect (via method chaining).
     ParentParams &setFoo(Foo foo_val)
     {
         m_foo = foo_val;
         return *this;
     }

     // Changing only the bar aspect (via method chaining).
     ParentParams &setBar(Bar bar_val)
     {
         m_bar = bar_val;
         return *this;
     }

     // Many more - you mentioned at least 7.
     .... 
};

Now a child could look something like this:

// A child that happens to have the property that it changes foo and bar aspects.
class FooBarChangingChild :
    public Parent
{
public:
     FooBarChangingChild();
};

And in its implementation:

// Static cpp function just creating the params.
static ParentParams makeParams() 
{
     // Note the clarity of which options are being changed.
     return ParentParams()
          .setFoo(someFooVal)
          .setBar(someBarVal);
}

FooBarChangingChild::FooBarChangingChild() :
    Parent(makeParams())
{

}

Upvotes: 1

Related Questions