modello di progettazione per classe con dati allegati

1

Ho una classe Tiles simile a questa:

class Tiles {
 public:
  void AddTile(int x) { tiles_.push_back(x); }
  std::vector<int> tiles_;
}

Ora voglio creare una classe Tiles con dati, cioè,

class TilesWithData : public Tiles {
 public:
  void AddTile(int x, double data) { 
    tiles_.push_back(x);
    data_.push_back(data);  
  }
  std::vector<double> data_;
}

In questo scenario voglio che se viene creata una TilesWithData , la AddTile(int x) cancellata, in modo che sia disponibile solo la versione AddTile(int x, double data) . È possibile? Qual è il miglior design pattern per questo caso? Grazie!

    
posta hovo 08.02.2018 - 12:12
fonte

3 risposte

8

Quali comportamenti hanno queste classi? Al momento faresti meglio con

using Tile = int;
using Tiles = std::vector<Tile>;

struct TileWithData { Tile tile; double data; };
using TilesWithData = std::vector<TileWithData>;

La semplice risposta alla tua domanda è preferisci la composizione all'ereditarietà

    
risposta data 08.02.2018 - 12:28
fonte
5

TilesWithData è pubblicamente derivato da Tiles , quindi può essere assegnato a un puntatore (intelligente o stupido) di tipo Tiles (il mio C ++ è un po 'arrugginito, ma in pratica dovrebbe essere valido)

Tiles *tiles = new TilesWithData();

Disabilitare tiles->AddTile(123) violerebbe il Principio di sostituzione di Liskov (vedi qui ), poiché un client che ha un puntatore a un'istanza Tiles (che si tratti di un% attuale di% di co_de o di una% diTiles) non saprebbe di non chiamare TilesWithData .

Se esiste una funzionalità comune, potresti introdurre una classe base astratta per entrambi

class Tiles 
{
    public:
        virtual vector<int> GetTiles() = 0; 
}

da cui puoi ricavare AddTile

class SimpleTiles : public Tiles 
{
    public:
        void AddTile(int x) { tiles_.push_back(x); }
        std::vector<int> GetTiles 
        {
            return tiles_;
        }
    private:
        std::vector<int> tiles_;
}

e SimpleTiles

class TilesWithData : Tiles
{
    public:
        void AddTile(int x, double data) 
        { 
            tiles_.push_back(x); 
            data_.push_back(data);
        }

        std::vector<int> GetTiles 
        {
            return tiles_;
        }

        std::vector<double> GetData()
        {
            return data_;
        }
    private:
        // elided
}

In questo modo, i dati (interni) della tua classe sono incapsulati e sei libero di cambiare l'implementazione interna effettiva senza violare i client del tuo codice. Inoltre, qualsiasi cliente, in possesso di un riferimento a TilesWithData può essere sicuro delle sue capacità: Ottenere l'elenco delle tessere. Tutto ciò che è ulteriore dipende dai derivati.

Note: c'è ancora molto spazio per miglioramenti. Le implementazioni Tiles non sono né pesce né carne, non forniscono una vera logica, ma non sono né semplici raccolte. Un modo migliore potrebbe essere abstrac tiles

class Tile
{
    public:
        virtual void Update() = 0;
}

e una singola raccolta per contenere tutti i tipi derivati da Tiles

class Tiles
{
    public:
        void AddTile(std::shared_ptr<Tile> tileToAdd)
        {
            tiles_.push_back(tileToAdd);
        }

        void UpdateTiles()
        {
            for(std::vector<std::shared_ptr<Tile>>:iterator it = tiles_.begin(); it != tiles_.end(); it++)
            {
                (*it)->Update();
            }
        }
    private:
        std::vector<std::shared_ptr<Tile>> tiles_;
}

Ora puoi implementare diversi tipi di tessere adatti alle tue esigenze

class SimpleTile : public Tile
{
    public:
        SimpleTile(int whatever)
        {
            // ...
        }

        void Update()
        {
            // ...
        }
    // ...
}

class TileWithData : public Tile 
{
    public:
        TileWithData(int whatever, double data)
        {
            // ...
            data_ = data;
        }

        void Update()
        {
            // ...
        }
    // ...
}

Dichiarazione di non responsabilità: Potrebbe darsi che ci siano alcuni errori nella sintassi C ++. Se la mia intenzione è chiara, sentitevi liberi di modificare, otherwith, per favore lasciatemi un commento.

    
risposta data 08.02.2018 - 14:13
fonte
2

Il modo di vita dei modelli

L'unica cosa in comune tra Tiles e TilesWithData sembra essere che fanno cose simili ma con tipi di dati diversi. Quindi l'ereditarietà non è adatta. Preferisci invece un generico Tiles :

template <class DataType>
class Tiles {
 public:
  void AddTile(DataType x) { tiles_.push_back(x); }
  std::vector<DataType> tiles_;
};

Una volta definito, puoi usare qualsiasi tipo di tessera:

Tiles<int> a; 
a.AddTile(10); 

Tiles<std::string> b; 
b.AddTile("hello, world"); 

Tiles<std::tuple<int, double>> c; 
c.AddTile(std::make_tuple(20, 12.5));

Nota l'ultimo esempio: potresti usare il tuo tipo. Ma se vuoi semplicemente usare una combinazione di diversi tipi senza preoccuparti, puoi usare std :: tupla .

Demo online

Nel caso in cui preferisci mantenere l'ereditarietà ...

Problemi di progettazione

Ci sono diversi problemi nella progettazione:

  • per prima cosa, i membri dei dati devono essere private o protected . Qual è il senso di preoccuparsi dell'uso illegittimo di AddTile() , se d'altra parte, qualcuno potrebbe modificare tiles_ direttamente con un push_back() ?
  • secondo, TilesWithData::AddTile() non dovrebbe confondere con tiles_ direttamente, ma piuttosto usa l'interfaccia di classe di base (ad es. sostituisci il pushback esplicito con Tiles::AddTile(x); ). Perché ? Immagina cosa succederebbe se decidessi di passare a std::list tiles_; ...
  • terzo, usi l'ereditarietà in modo errato: se TilesWithData ha un'interfaccia diversa, non è in realtà un tipo di Tiles (e torna alla mia prima proposta ;-)

E funziona già come vuoi!

Con le tue classi, lo scenario funziona già come desiderato! Questo perché il nome si nasconde. Ad esempio, il seguente codice genererà un messaggio di errore:

TilesWithData b; 
b.AddTile(30);   /// OUCH !

Il messaggio di errore è:

prog.cpp:26:14: error: no matching function for call to ‘TilesWithData::AddTile(int)’
  b.AddTile(30);
              ^

Demo online

Se si desidera utilizzare AddTile() della classe base, è necessario "importare" il suo nome inserendo la seguente dichiarazione in TilesWithData affinché il sovraccarico funzioni correttamente:

using Tile::AddTile; 
    
risposta data 08.02.2018 - 21:38
fonte