Come evitare il static_cast / dynamic_cast nel pattern di progettazione 'Abstract Factory'?

1

Stiamo utilizzando il modello di design Abstract Factory nel nostro progetto, poiché il progetto è diventato complesso, il più delle volte le funzionalità della classe concreta devono separarsi in più classi.

Come il seguente frammento di codice, il rendering è supportato da Renderer e Canvas , quindi è necessario static_cast nell'implementazione concreta come il seguente snippet di codice.

Quindi è un cattivo odore di design per il typecasting in Abstract Factory pattern di progettazione? Se sì, come migliorare il design nel seguente frammento?

#include <iostream>

class Renderer
{
public:
    virtual ~Renderer() {};
    virtual void RenderIt(Canvas* canvas) = 0;
};

class OpenGLRenderer : public Renderer 
{
    void RenderIt(Canvas* canvas) {
        // <----How can we avoid to cast here.
        OpenGLCanvas* canvas = static_cast<OpenGLRenderer>(canvas);
        // Do something with opengl.
    }
};

class DirectXRenderer : public Renderer 
{
    void RenderIt(Canvas* canvas) {
        // <---How can we avoid to cast here.
        DirectXCanvas* canvas = static_cast<DirectXCanvas>(canvas);
        // do something with directx
    }
};

#include <string>

class RendererFactory
{
public:
    Canvas * createCanvas(const std::string& type) {
        if(type == "opengl") 
            return new OpenGLCanvas();
        else if(type == "directx") 
            return new DirectXCanvas();
        else return NULL;
    }

    Renderer *createRenderer(const std::string& type) 
    {
        if(type == "opengl") 
            return new OpenGLRenderer();
        else if(type == "directx") 
            return new DirectXRenderer();
        else return NULL;
    }
};
    
posta ZijingWu 25.09.2017 - 10:39
fonte

1 risposta

4

(In primo luogo una nota generale: non è una fabbrica astratta, il modello prende il nome dall'avere una classe astratta o un'interfaccia per la fabbrica. Quindi si suppone che abbia una classe factory diversa per ogni famiglia di tipi, di scegliere il tipo dalla ricerca di una stringa, tuttavia è possibile scegliere una classe factory concreta in base a una stringa. C'è un esempio alla fine di questa risposta.)

Questo problema è in effetti discusso nel libro Design Patterns:

But even when no coercion is needed, an inherent problem remains: All products are returned to the client with the same abstract interface as given by the return type. The client will not be able to differentiate or make safe assumptions about the class of a product. If clients need to perform subclass-specific operations, they won't be accessible through the abstract interface. Although the client could perform a downcast (e.g., with dynamic_cast in C++), that's not always feasible or safe, because the downcast can fail. This is the classic trade-off for a highly flexible and extensible interface.

 — Gamma, Helm, Johnson, Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. 1994. See p. 91 in the chapter Abstract Factory.

Quindi, se la decisione tra OpenGL e DirectX deve essere una decisione runtime, allora C ++ può darti solo quella con una perdita di sicurezza del tipo. Molti modelli di progettazione si sentono molto più a loro agio in linguaggi più dinamici, ma in primo luogo non garantiscono una sicurezza di tipo comparabile. Per esempio. probabilmente non penseresti affatto a questo problema in Python, ma il cast necessario è molto visibile in C ++. Quindi il dynamic_cast non è poi così male.

(Si noti che dovresti usare un dynamic_cast qui per affermare che il tipo di runtime dell'oggetto corrisponde realmente. Un static_cast eseguirà un downcast non controllato e non è sicuro per il tipo.)

Quello che puoi fare è usare i modelli invece dell'ereditarietà. Tutto il codice che utilizza i renderer / canvass sarebbe quindi basato sul tipo di API e verrebbe compilato in modo efficace due volte. Ad esempio:

struct DirectXCanvas { ... };
struct DirextXRenderer {
  void render(DirectXCanvas& canvas);
};

struct DirectXAPI {
  using Canvas = DirectXCanvas;
  using Renderer = DirectXRenderer;
};

struct OpenGLCanvas { ... };
struct OpenGLRenderer {
  void render(OpenGLCanvas& canvas);
};

struct OpenGPLAPI {
  using Canvas = OpenGLCanvas;
  using Renderer = OpenGLRenderer;
};

template<typename API>
void client_code() {
  API::Renderer r;
  API::Canvas c1;
  API::Canvas c2;
  r.render(c1);
  r.render(c2);
}

int main() {
  string type = ...;
  if (type == "directx") {
     client_code<DirectXAPI>();
  } else if (type == "opengl") {
     client_code<OpenGLAPI>();
  } else {
     return 1;
  }
  return 0;
}

Nell'esempio precedente, la struttura API assume il ruolo della tua fabbrica. Qui usare gli alias di tipo è sufficiente, in un esempio più complesso questi tipi di API potrebbero anche fornire metodi.

A seconda di come è strutturato il codice cliente, a volte è possibile introdurre un'interfaccia di livello superiore che riassume tutti i tipi concreti. Le sottoclassi concrete dell'interfaccia ora internamente ora sui tipi specifici Canvas e Renderer. Questo è noto come tipo di cancellazione .

Nota che nelle lingue con generici (al contrario dei modelli), è possibile scrivere questo tipo di codice senza calchi. La differenza fondamentale è che gli argomenti del template sono segnaposto che devono essere compilati con tipi concreti, mentre i generici hanno variabili di tipo che sono sufficienti per controllare la sicurezza del tipo, ma non specificare un concreto tipo di runtime. Avremmo quindi (usando Java):

interface Renderer<Canvas> {
  void render(Canvas c);
}

interface AbstractFactory<Canvas> {
  Canvas createCanvas();
  Renderer<Canvas> createRenderer();
}

class OpenGLCanvas { ... }
class OpenGLRenderer implements Renderer<OpenGLCanvas> {
  @Override void render(OpenGLCanvas c) { ... }
}

class OpenGLFactory implements AbstractFactory<OpenGLCanvas> {
  @Override OpenGLCanvas createCanvas() {
    return new OpenGLCanvas();
  }
  @Override OpenGLRenderer createRenderer() {
    return new OpenGLRenderer();
  }
}

class DirectXCanvas { ... }
class DirectXRenderer implements Renderer<DirectXCanvas> {
  @Override void render(DirectXCanvas c) { ... }
}

class DirectXFactory implements AbstractFactory<DirectXCanvas> {
  @Override DirectXCanvas createCanvas() {
    return new DirectXCanvas();
  }
  @Override DirectXRenderer createRenderer() {
    return new DirectXRenderer();
  }
}

void <Canvas> clientCode(AbstractFactory<Canvas> factory) {
  Renderer<Canvas> r = factory.createRenderer();
  Canvas c1 = factory.createCanvas();
  Canvas c2 = factory.createCanvas();
  r.render(c1);
  r.render(c2);
}

static void main(String... argv) {
  String type = ...;
  if ("directx".equals(type)) {
    clientCode(new DirectXFactory());
  } else if ("opengl".equals(type)) {
    clientCode(new OpenGLFactory());
  } else {
    System.exit(1);
  }
}

La differenza con C ++ qui è che in effetti utilizziamo le interfacce Renderer e Factory e che il codice del client deve essere compilato una sola volta, non di nuovo per ogni API. Tuttavia, dobbiamo ancora trasportare tutte le variabili di tipo necessarie (qui: la tela). In Java è particolarmente ingombrante dal momento che la lingua non ha alias di tipo.

    
risposta data 25.09.2017 - 12:38
fonte