Home > other >  How to structure base class where derived classes operate on different data types
How to structure base class where derived classes operate on different data types

Time:07-16

I have a class that is supposed to fetch an object from the server.

// T types
struct RequestLicense
{
    // arbitrary data
};
struct RequestTrial
{
    // arbitrary data
}

// U types
struct LicenseData
{
    // arbitrary data
};
struct TrialData
{
    // arbitrary data
}

struct Fetcher
{
    template <typename T>
    std::wstring FetchBlob(T requestParameters);
    
    template <typename U>
    std::unique_ptr<U> DeserializeBlob(const std::wstring& serializedBlob);

    template <typename U>
    bool ValidateResponse(U* deserializedResponse);
    
    // Many different virtual, helper functions that make use of the types T and U
}

struct LicenseFetcher : Fetcher
{ 
    // overrides for the different helper functions mentioned in Fetcher
}

struct TrialFetcher : Fetcher
{
    // overrides for the different helper functions mentioned in Fetcher
}

// specialization FetchBlob with T=RequestLicense
// specialization for DeserializeBlob and ValidateResponse with U=LicenseData

// specialization FetchBlob with T=RequestTrial
// specialization for DeserializeBlob and ValidateResponse with U=TrialData

// specializations for many of the helper functions. 2 specializations for each of the functions--one RequestLicense/LicenseData and one for RequestTrial/TrialData.

Normally I would make the functions virtual, but I can't since they are templated. As I see it, I have two options: 1) proceed with the route I am on or 2) remove the templating and leverage downcasting.

I was thinking about creating LicenseBase that LicenseData and TrialData would inherit from. As LicenseData and TrialData have nothing in common, LicenseBase would be empty. I would then replace all references to the template type U with a pointer to LicenseBase. Since each derived class would be operating on a known type, it would be safe to downcast within each overriden function. e.g. LicenseFetcher::ValidateResponse(LicenseBase* r) { const LicenseData& data = static_cast<const LicenseData&>(r); } I would do something similar for the Request* types.

I know downcasting is frowned upon and it also wouldn't help when I am returning one of these types (like with DeserializeBlob) but I was wondering about future development. All this templatization seems a bit hard to follow, and downcasting would make it easier to read. Also, with the templated approach, Fetcher.h will grow quite large as it will contain a specialization for every function for each derived class. I already have a half-dozen functions that would need specialization and a half-dozen derived versions of Fetcher. As I type this out, templating seems like the right way to go, but I've been doing some templatization recently and a bit worried I'm starting to "view every problem as a nail."

Are there any options I'm missing? Or should I forge ahead with templating?

Edit: Regardless of the approach I take, I plan on having non-member functions like GetLicense and GetTrial that would look something like

LicenseData GetLicense()
{
    const RequestLicense params = RequestLicense(/*arbitrary parameters*/);
    std::wstring response = FetchBlob(params);
    std::unique_ptr<LicenseData> data = DeserializeBlob<LicenseData>(response);
    if (ValidateResponse<LicenseData>(data))
    {
        return data;
    }
    return {};
}

CodePudding user response:

I think I would make the whole Fetcher structure a template, with the member functions being abstract virtual functions:

// R is the request type
// D is the data type
template <typename R, typename D>
struct Fetcher
{
    virtual std::wstring FetchBlob(R const& requestParameters) = 0;
    
    virtual std::unique_ptr<D> DeserializeBlob(const std::wstring& serializedBlob) = 0;

    virtual bool ValidateResponse(D const& deserializedResponse) = 0;

    // ...
};

Then when inheriting from it pass the specific types:

struct LicenseFetcher : public Fetcher<RequestLicense, LicenseData>
{
    std::wstring FetchBlob(RequestLicense const& requestParameters) override
    {
        // TODO: Implementation
    }
    
    std::unique_ptr<LicenseData> DeserializeBlob(const std::wstring& serializedBlob) override
    {
        // TODO: Implementation
    }

    bool ValidateResponse(LicenseData const& deserializedResponse) override
    {
        // TODO: Implementation
    }

    // ...
};

Would be much cleaner than having a multitude of specializations.

  • Related