简体   繁体   中英

Design pattern for method returning different types/classes

This is a question for the Object Design Pattern specialists.

Let's assume I have a Parser class that is in charge of reading/parsing a stream of data (that carry information packets of different types ). Each of these packets carry a different type of information, so ideally I would have a class for each type of packet ( PacketTypeA , PacketTypeB , ... each one with its own interface).

class Parser {

public:

    /* ctor */
    /* dtor */


   void read_packet(/* arguments */);

   // methods...
private:
   // more methods...
}

The method Parser::read_packet would then go through the stream and return a class (or pointer or reference to a class) to the appropriate packet type.

Would you use void pointers for this? How about a generic class ( PacketBasicInterface ) that would provide a common (partial) interface to query about the type of packet (so that any decision could then be made at runtime)?

// Pure virtual (abstract) class to provide a common (and partial) interface
class PacketBasicInterface {
public:

    std::string whoAmI() const = 0;
    bool amIofType(const std::string& type) const = 0;

}

// Class to access data of type A packet
class PacketTypeA : public PacketBasicInterface {

public:
    // methodA_1()
    // methodA_2(), ...
}

// Class to access data of type A packet
class PacketTypeB : public PacketBasicInterface {

public:
    // methodB_1()
    // methodB_2(), ...
}

Any thought or feedback would be very much appreciated!

Many thanks!

This is what std::variant is for.

I would define an enumeration class, that enumerates all possible packet types:

enum class packet_type {initialization_packet, confirmation_type, ... };

And have read_packet return a tuple of packet_type and a variant:

typedef std::variant< ... > packet_info;

std::tuple<packet_type, packet_info> read_packet();

Don't really need a formal enumeration, but it makes it easier to figure out what to do with the variant.

A few variations on this general approach include:

  1. Using an opaque std::string , rather than a fixed enumeration, to specify the packet type.

  2. Using std::any instead of a formal std::variant .

  3. Instead of using a simple enumeration, or an opaque token like a std::string , use a slightly non-trivial class to define the packet type, with the class's methods taking the variant metadata as parameters, and encapsulating the operations that can be done on the packet.

Of course, as noted in the cited link, std::variant requires C++17. Which would be a good argument for you to update your compiler: you get a simple way to implement a completely type-safe approach.

Would you use void pointers for this?

No.

How about a generic class (PacketBasicInterface) that would provide a common (partial) interface to query about the type of packet (so that any decision could then be made at runtime)?

That makes most sense to me.

Let me refine that. Yes, it will be good to have a generic base class. However, when parsing the stream to construct sub-types of the base class, don't rely on an if-else type approach. Rather, use a factory pattern. Let the various factories construct the right object types based an on a key , which I assume will be available from the data being parsed.

If you encounter the string "PacketTypeA" in your data, you would expect that PacketTypeAFactory will be responsible for constructing the object.

FWIW, this approach is scalable for lots of sub-types of the base class. We use this approach at my work and it has served us well for over twenty years.


Here's the skeletal structure of the code base I am thinking of:


The classes.

class PacketBasicInterface { };

class PacketTypeA : public PacketBasicInterface { };

class PacketTypeB : public PacketBasicInterface { };

The interface of the factory.

// PacketFactory.h
class PacketFactory
{
   public:

      static PacketBasicInterface* makePacket(std::string const& packetData);

      static void registerFactory(std::string const& key, PacketFactory* factory);

      virtual PacketBasicInterface* make(std::string const& packetData) = 0;

      virtual ~PacketFactory() {}
};

Implementation of the framework that makes the factory work.

// PacketFactory.cpp

#include "PacketFactory.h"

namespace PacketBasicInterface_Impl
{
   using PacketFactoryMap = std::map<std::string, PacketFactory*>;

   PacketFactoryMap& getPacketFactoryMap()
   {
      static PacketFactoryMap theMap;
      return theMap;
   }
};

uisng namespace PacketBasicInterface_Impl;

PacketBasicInterface* PacketFactory::makePacket(std::string const& packetData)
{
   std::string key = extractKey(packetData);
   PacketFactoryMap& theMap = getPacketFactoryMap();
   PacketFactoryMap::iterator iter = theMap.find(key);
   if ( iter == theMap.end() )
   {
      return nullptr;
   }

   return iter->second->make(packetData);
}

void registerFactory(std::string const& key, PacketFactory* factory)
{
   getPacketFactoryMap()[key] = factory;
}

Code for making objects of type PacketTypeA using the factory pattern.

// PacketTypeAFactory.cpp

#include "PacketFactory.h"
#include "PacketTypeA.h"

class PacketTypeAFactory : public PacketFactory
{
   public:

      virtual PacketBasicInterface* make(std::string const& packetData)
      {
         PacketTypeA* packet = new PacketTypeA();

         // Flesh out packet with data pulled from packetData
         // ...
         //

         return packet;
      }

      struct Initializer
      {
         Initializer() { PacketFactory::registerFactory("PacketTypeA", new PacketTypeAFactory); }
      };
};


// Constructing this object at static initialization time makes sure
// that PacketTypeAFactory is registered with PacketFactory when the
// stream data need to be parsed.
static PacketTypeAFactory::Initializer initializer;

The code for making objects of type PacketTypeB is very similar to the code for making objects of type PacketTypeA using the factory pattern.

// PacketTypeBFactory.cpp


#include "PacketFactory.h"
#include "PacketTypeB.h"

class PacketTypeBFactory : public PacketFactory
{
   public:

      virtual PacketBasicInterface* make(std::string const& packetData)
      {
         PacketTypeA* packet = new PacketTypeA();

         // Flesh out packet with data pulled from packetData
         // ...
         //

         return packet;
      }

      struct Initializer
      {
         Initializer() { PacketFactory::registerFactory("PacketTypeB", new PacketTypeBFactory); }
      };
};


// Constructing this object at static initialization time makes sure
// that PacketTypeBFactory is registered with PacketFactory when the
// stream data need to be parsed.
static PacketTypeBFactory::Initializer initializer;

Client code.

std::string packetData;
while ( getPacketData(packetData) )
{
   PacketBasicInterface* packet = PacketFactory::makePacket(packetData);
   if ( packet == nullptr )
   {
      // Deal with error.
   }
   else
   {
      // Use packet
   }
}

Double dispatching can be the way to go if you are looking for a design pattern from the realm of object oriented programming.
It follows a minimal, working example:

#include<iostream>

struct Visitor;

struct PacketBasicInterface {
    virtual void accept(Visitor &) = 0;
};

struct PacketTypeA: PacketBasicInterface {
    void accept(Visitor &) override;
};

struct PacketTypeB: PacketBasicInterface {
    void accept(Visitor &) override;
};

struct Visitor {
    void visit(PacketTypeA) {
        std::cout << "PacketTypeA" << std::endl;
    }

    void visit(PacketTypeB) {
        std::cout << "PacketTypeB" << std::endl;
    }
};

void PacketTypeA::accept(Visitor &visitor) {
    visitor.visit(*this);
}

void PacketTypeB::accept(Visitor &visitor) {
    visitor.visit(*this);
}

struct Parser {
   PacketBasicInterface * read_packet() {
       return new PacketTypeB{};
   }
};

int main() {
    Visitor visitor;
    auto *packet = Parser{}.read_packet();
    packet->accept(visitor);
    delete packet;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM