Domanda di progettazione su come nascondere l'implementazione dagli utenti di una libreria dinamica

5

Sto costruendo una DLL e nelle mie intestazioni pubbliche ho questo: (le definizioni sono in .cpp ma per chiarezza le mostro in .hpp qui)

ObjectTag.hpp:

class API_DLL ObjectTag {
public:
    ObjectTag() : mUUID(UIDGenerator::Generate()) {
    }

    uuid getUUID() {
        return mUUID;
    }

private:
    uuid mUUID;
};

Texture.hpp:

class API_DLL Texture : public ObjectTag {
public:
    Texture() : ObjectTag(){
    }

};

Ho un editor di Scene che crea oggetti Texture e li salva in un file con il loro uuid. Ora voglio caricare quegli oggetti Texture letti dal file all'interno della libreria e impostare il loro UUID precedentemente generato e salvato.

Il mio primo tentativo è stato quello di rendere questa semplice funzione void setUUID (id idu):

class API_DLL ObjectTag {
public:
    ObjectTag() : mUUID(UIDGenerator::Generate()) {
    }

    uuid getUUID() const {
        return mUUID;
    }
    void setUUID(uuid id) {
        mUUID=id;
    }

private:
    uuid mUUID;
};

Ma questo consentirà agli utenti della libreria di modificare la variabile UUID. Come lo progetteresti in un modo che impedisce di farlo dagli utenti della biblioteca?

    
posta Daniel Guzman 16.04.2016 - 15:46
fonte

2 risposte

3

How would you design this in a way that prevents doing that from the users of the library?

Interfaccia Texture separata dall'implementazione:

class API_DLL ObjectTag
{
    uuid mUUID;

public:
    uuid getUUID()
    {
        return mUUID;
    }
};

class API_DLL Texture: public ObjectTag // Texture interface visible in client code
{
public:
    virtual ~Texture() = 0;
    virtual void DoTextureThings() = 0;
};

template<typename T>
class EditableObjectTag: public T // EditableObjectTag not exposed to client code
                                  // defines save functionality
{
public:
    void setUUID(uuid id) { mUUID = id; }
};

class YourTexture: public EditableObjectTag<Texture> // YourTexture not exposed
                                                     // to client code

{
    void DoTextureThings() override { ... }
};

La tua funzione di caricamento:

API_DLL std::vector<std::unique_ptr<Texture>> LoadTextures()
{
    std::vector<std::unique_ptr<Texture>> result;

    result.emplace_back(new YourTexture{ bla, bla, bla });

    return result;
}

Il codice cliente ora può lavorare con le trame, senza preoccuparsi che una Texture sia una classe astratta e l'interfaccia Texture non menzioni nulla sull'impostazione dei valori nell'oggetto:

void ClientCode()
{
    auto textures = LoadTextures();
    textures[0]->DoTextureThings();

    // textures[0]->setUUID(someUUID); -> fails to compile
}

Nota : la classe EditableObjectTag è un design basato sull'aspetto: aggiunge un'interfaccia modificabile sul suo argomento modello.

    
risposta data 18.04.2016 - 10:07
fonte
5

Se vuoi serializzare e deserializzare oggetti Texture, allora dovresti avere costruttori, metodi o interfacce progettate esplicitamente per questo scopo. La serializzazione è uno dei pochi casi d'uso che è abbastanza speciale per meritarlo.

Poiché questo è C ++, hai molte opzioni. In cima alla mia testa:

  1. Fornisci i metodi Texture class serialize() e deserialize() . La semplicità è una virtù.
  2. Crea una classe TextureSerializer separata che sia un friend della classe Texture in modo che tu possa rendere setUUID() privato. Questo approccio è spesso una buona idea se ti aspetti che la serializzazione sia complicata o abbia dipendenze non banali o che la classe Texture abbia già un sacco di cose.
  3. Crea una classe Serializable con metodi virtuali serialize() e deserialize() e nessuno stato, a.k.a. un'interfaccia e fai Texture a implementare tale interfaccia. Se ti aspetti che altri programmatori possano scrivere le proprie classi serializzabili che il tuo programma deve gestire, questo potrebbe essere l'approccio più sicuro e generico.
  4. Crea una classe base SerializableTaggedObject con metodi virtuali serialize() e deserialize() e il membro uuid e il costruttore generatore uuid che hai attualmente in ObjectTag . Se hai diverse classi simili a Texture , che devono essere entrambe serializzabili e avere uuid, questa potrebbe essere la scelta migliore.

Tutte queste opzioni ti proteggeranno da utenti non malintenzionati che semplicemente non hanno letto i commenti che hai scritto sopra setUUID() . Come puoi vedere dalle mie osservazioni aggiuntive, quale sceglierai in realtà dovrebbe dipendere da molti altri fattori, ma ognuno di essi sarebbe un enorme miglioramento rispetto all'esposizione di un setter pubblico per un campo che dovrebbe essere immutabile.

Sto deliberatamente saltando alcune domande di implementazione, ad esempio se uno di questi metodi dovrebbe essere statico. Suppongo che tu non stia cercando di proteggere dagli utenti malintenzionati che chiamerebbero volentieri Texture.deserialize("{ \"uuid\": \"bwahahahaha\" }") invece di setUUID() , ma semplicemente impediscono agli utenti di attivare erroneamente comportamenti non definiti nel tuo codice.

P.S. Dato che hai menzionato in modo specifico le DLL, sento che dovrei menzionare anche l'idioma pimpl , dal momento che nasconde i dettagli di implementazione in un modo che spesso migliora la compatibilità binaria , che tende ad essere una preoccupazione particolarmente strong per le DLL. Ovviamente devi ancora mettere un meccanismo di serializzazione da qualche parte sulla reale implementazione, quindi questo sarebbe in aggiunta a una delle opzioni che ho descritto sopra.

    
risposta data 16.04.2016 - 17:42
fonte

Leggi altre domande sui tag