2

In the code I am now creating, I have an object that can belong to two discrete types, differentiated by serial number. Something like this:

class Chips {
public:
    Chips(int shelf) {m_nShelf = shelf;}
    Chips(string sSerial) {m_sSerial = sSerial;}
    virtual string GetFlavour() = 0;
    virtual int GetShelf() {return m_nShelf;}
protected:
    string m_sSerial;
    int m_nShelf;
}

class Lays : Chips {
    string GetFlavour() 
    {
        if (m_sSerial[0] == '0') return "Cool ranch";
        else return "";
    }
}

class Pringles : Chips {
    string GetFlavour() 
    {
        if (m_sSerial.find("cool") != -1) return "Cool ranch";
        else return "";
    }
}

Now, the obvious choice to implement this would be using a factory design pattern. Checking manually which serial belongs to which class type wouldn't be too difficult.

However, this requires having a class that knows all the other classes and refers to them by name, which is hardly truly generic, especially if I end up having to add a whole bunch of subclasses.

To complicate things further, I may have to keep around an object for a while before I know its actual serial number, which means I may have to write the base class full of dummy functions rather than keeping it abstract and somehow replace it with an instance of one of the child classes when I do get the serial. This is also less than ideal.

Is factory design pattern truly the best way to deal with this, or does anyone have a better idea?

dkb
  • 551
  • 5
  • 14
  • 1
    Is this a valid way to declare pure virtual function of class without key words of "virtual" ahead of that? – DarkHorse Nov 12 '13 at 09:23
  • @DarkHorse yeah, forgot to add the virtual since this is not the actual code :) will edit it in – dkb Nov 12 '13 at 09:25
  • 1
    The example seems a bit contrived to me.. Why not make serial a class, if the behavior depends directly on it? GetFlavour could become a [virtual] method of the class Serial. – Alex Jenter Nov 12 '13 at 09:25
  • 2
    try the state/strategy pattern – Karoly Horvath Nov 12 '13 at 09:26
  • Really not sure what the "this" is that you're trying to deal with. But, if you think you're gonna end up with a big switch statement (or even worse, nested big switch statements) then you should use a factory pattern; what you seem to have is a small `if-else`. – Grimm The Opiner Nov 12 '13 at 09:28
  • 1
    This question and its answers may be of interest to you: http://stackoverflow.com/questions/17378961/elegant-way-to-implement-extensible-factories-in-c – Marc Claesen Nov 12 '13 at 09:28
  • @AlexJenter in the actual code I'm writing, [Object]SerialNumber *is* the class I'm trying to design. Since I have a couple of different formats, and since I don't always know the serial number when I'm instantiating the serial number object (confusing, huh? Legacy code, man...), the question stands. – dkb Nov 12 '13 at 09:28
  • 2
    Well from my point of view your problem is not really the creation of the objects but more likely the behaviour of your objects. Without knowing much of the actual problem it's quite hard to help. But as far as I understood the behaviour of `GetFlavour()`depends on the state of your SerialNumber. --> State Pattern? – Sambuca Nov 12 '13 at 09:32
  • @dkb You have to instantiate an object to wrap/represent a serial number object BEFORE you know what the serial number is? And there might be many of these objects instantiated at once? – Grimm The Opiner Nov 12 '13 at 10:18

3 Answers3

2

You can create a factory which knows only the Base class, like this:

add pure virtual method to base class: virtual Chips* clone() const=0; and implement it for all derives, just like operator= but to return pointer to a new derived. (if you have destructor, it should be virtual too)

now you can define a factory class:

Class ChipsFactory{
  std::map<std::string,Chips*> m_chipsTypes;

public:
  ~ChipsFactory(){
     //delete all pointers... I'm assuming all are dynamically allocated.
     for( std::map<std::string,Chips*>::iterator it = m_chipsTypes.begin();
          it!=m_chipsTypes.end(); it++) {
        delete it->second;
     }
  }
  //use this method to init every type you have
  void AddChipsType(const std::string& serial, Chips* c){
    m_chipsTypes[serial] = c;
  }      
  //use this to generate object
  Chips* CreateObject(const std::string& serial){
    std::map<std::string,Chips*>::iterator it = m_chipsTypes.find(serial);
    if(it == m_chipsTypes.end()){
       return NULL;
    }else{
       return it->clone();
    }   
  }
};

Initialize the factory with all types, and you can get pointers for the initialized objects types from it.

SHR
  • 7,940
  • 9
  • 38
  • 57
1

From the comments, I think you're after something like this:

    class ISerialNumber
    {
    public:
        static ISerialNumber* Create( const string& number )
        {
            // instantiate and return a concrete class that 
            // derives from ISerialNumber, or NULL
        }

        virtual void DoSerialNumberTypeStuff() = 0;
    };

    class SerialNumberedObject
    {
    public:
        bool Initialise( const string& serialNum ) 
        {
            m_pNumber = ISerialNumber::Create( serialNum );
            return m_pNumber != NULL;
        }

        void DoThings()
        {
            m_pNumber->DoSerialNumberTypeStuff();
        }

    private:
        ISerialNumber* m_pNumber;
    };

(As this was a question on more advanced concepts, protecting from null/invalid pointer issues is left as an exercise for the reader.)

Grimm The Opiner
  • 1,778
  • 11
  • 29
1

Why bother with inheritance here? As far as I can see the behaviour is the same for all Chips instances. That behaviour is that the flavour is defined by the serial number.

If the serial number only changes a couple of things then you can inject or lookup the behaviours (std::function) at runtime based on the serial number using a simple map (why complicate things!). This way common behaviours are shared among different chips via their serial number mappings.

If the serial number changes a LOT of things, then I think you have the design a bit backwards. In that case what you really have is the serial number defining a configuration of the Chips, and your design should reflect that. Like this:

class SerialNumber {
public:
    // Maybe use a builder along with default values
    SerialNumber( .... ); 
    // All getters, no setters.
    string getFlavour() const;
private:
    string flavour;
    // others (package colour, price, promotion, target country etc...)
}

class Chips {
public:
    // Do not own the serial number... 'tis shared.
    Chips(std::shared_ptr<SerialNumber> poSerial):m_poSerial{poSerial}{} 
    Chips(int shelf, SerialNumber oSerial):m_poSerial{oSerial}, m_nShelf{shelf}{}
    string GetFlavour() {return m_poSerial->getFlavour()};
    int GetShelf() {return m_nShelf;}
protected:
    std::shared_ptr<SerialNumber> m_poSerial;
    int m_nShelf;
}

// stores std::shared_ptr but you could also use one of the shared containers from boost.
Chips pringles{ chipMap.at("standard pringles - sour cream") };

This way once you have a set of SerialNumbers for your products then the product behaviour does not change. The only change is the "configuration" which is encapsulated in the SerialNumber. Means that the Chips class doesn't need to change. Anyway, somewhere someone needs to know how to build the class. Of course you could you template based injection as well but your code would need to inject the correct type.

One last idea. If SerialNumber ctor took a string (XML or JSON for example) then you could have your program read the configurations at runtime, after they have been defined by a manager type person. This would decouple the business needs from your code, and that would be a robust way to future-proof.

Oh... and I would recommend NOT using Hungarian notation. If you change the type of an object or parameter you also have to change the name. Worse you could forget to change them and other will make incorrect assumptions. Unless you are using vim/notepad to program with then the IDE will give you that info in a clearer manner.

@user1158692 - The party instantiating Chips only needs to know about SerialNumber in one of my proposed designs, and that proposed design stipulates that the SerialNumber class acts to configure the Chips class. In that case the person using Chips SHOULD know about SerialNumber because of their intimate relationship. The intimiate relationship between the classes is exactly the reason why it should be injected via constructor. Of course it is very very simple to change this to use a setter instead if necessary, but this is something I would discourage, due to the represented relationship.

I really doubt that it is absolutely necessary to create the instances of chips without knowing the serial number. I would imagine that this is an application issue rather than one that is required by the design of the class. Also, the class is not very usable without SerialNumber and if you did allow construction of the class without SerialNumber you would either need to use a default version (requiring Chips to know how to construct one of these or using a global reference!) or you would end up polluting the class with a lot of checking.

As for you complaint regarding the shared_ptr... how on earth to you propose that the ownership semantics and responsibilities are clarified? Perhaps raw pointers would be your solution but that is dangerous and unclear. The shared_ptr clearly lets designers know that they do not own the pointer and are not responsible for it.

Dennis
  • 3,683
  • 1
  • 21
  • 43
  • The comments under the question show that he needs to instantiate the object (of class `Chips` in your code) before he knows the serial number - your design prevents that. Also, why does the party instantiating `Chips` need to know anything about the `SerialNumber` class? You're forcing them to know of both it AND `shared_ptr` in order to use `Chips` at all. – Grimm The Opiner Nov 12 '13 at 11:29
  • @user1158692 - The design decisions I am making are due to the nature of the relationship that is being represented. See my edit. – Dennis Nov 12 '13 at 12:16
  • 1
    The quesetioner clearly states in a comment that they don't always know the serial number when instantiating the object. "I really doubt that it is absolutely necessary to create the instances of chips without knowing the serial number" is an inadequate response! I would avoid use of `shared_ptr` simply by not sharing a pointer. If an object needs to decide on implementation after construction, then that object should be responsible for managing the lifetime of that implementation and the caller/user of the wrapper object needn't know anything about the details of how that is achieved. – Grimm The Opiner Nov 12 '13 at 12:36
  • As I also said... using a setter is a possibility if required but I would prefer to refactor the application to build the objects only when they are properly defined (when the implementation is known). In your answer you can call `DoThings` without initialising, and this would cause problems. My solution if modified to use a setter would have the same issues as yours, and the only difference is that yours uses a factory and mine leaves the responsibility of deciding the implementation up to the caller. In your case `Chips` has a dependency on another object (the factory), which is not the case – Dennis Nov 12 '13 at 12:57
  • in my solution. Instead my `Chips` is provided with the class without needing to know how to get it. This is more flexible, allowing a map, a factory, straight CTOR or any other mechanism to that the user wishes. `shared_ptr`: it is shared so that memory is not used needlessly. As this class represents an implementation it is the same for all users and holds no state, and so it is appropriate that it is shared. As it is shared then the class into which it is injected does not own it. – Dennis Nov 12 '13 at 13:00
  • What we are discussing are preferences and really not a reason to mark down answers. I could take the same approach to your question as I think that your initialisation mechanism is error-prone, but I won't, as it is a valid answer and depends on the preferences of the OP. – Dennis Nov 12 '13 at 13:06