que he estado haciendo algo similar en mi motor de renderizado. Tengo una clase con plantilla IResource interfaz desde la que una variedad de recursos inherit (despojado por razones de brevedad):
template <typename TResource, typename TParams, typename TKey>
class IResource
{
public:
virtual TKey GetKey() const = 0;
protected:
static shared_ptr<TResource> Create(const TParams& params)
{
return ResourceManager::GetInstance().Load(params);
}
virtual Status Initialize(const TParams& params, const TKey key, shared_ptr<Viewer> pViewer) = 0;
};
La función estática Create
una llamada a una clase ResourceManager con plantilla que es responsable de la carga, descarga, y las instancias de almacenamiento del tipo de recurso que administra con claves únicas, asegurando que las llamadas duplicadas se recuperen simplemente de la tienda, en lugar de volver a cargarlas como recursos separados.
template <typename TResource, typename TParams, typename TKey>
class TResourceManager
{
sptr<TResource> Load(const TParams& params) { ... }
};
Las clases de recursos de hormigón heredan de IResource utilizando el CRTP. Los ResourceManagers especializados para cada tipo de recurso se declaran como amigos de esas clases, de modo que la función Load
de ResourceManager puede llamar a la función Initialize
del recurso concreto. Uno de estos recursos es una clase de textura, que utiliza además un modismo pImpl para ocultar sus partes íntimas:
class Texture2D : public IResource<Texture2D , Params::Texture2D , Key::Texture2D >
{
typedef TResourceManager<Texture2D , Params::Texture2D , Key::Texture2D > ResourceManager;
friend class ResourceManager;
public:
virtual Key::Texture2D GetKey() const override final;
void GetWidth() const;
private:
virtual Status Initialize(const Params::Texture2D & params, const Key::Texture2D key, shared_ptr<Texture2D > pTexture) override final;
struct Impl;
unique_ptr<Impl> m;
};
Gran parte de la implementación de nuestra clase de textura es independiente de la plataforma (como la función GetWidth
si sólo devuelve un int almacenado en el Impl). Sin embargo, dependiendo de la API de gráficos a la que nos dirigimos (por ejemplo, Direct3D11 vs. OpenGL 4.3), algunos de los detalles de implementación pueden diferir. Una solución podría ser heredar de IResource una clase intermedia Texture2D que define la interfaz pública extendida para todas las texturas, y luego heredar una clase D3DTexture2D y OGLTexture2D de eso. El primer problema con esta solución es que requiere que los usuarios de su API estén constantemente atentos a la API de gráficos a la que se dirigen (pueden llamar al Create
en ambas clases secundarias). Esto podría resolverse restringiendo el Create
a la clase intermediaria Texture2D, que usa quizás un conmutador #ifdef
para crear un objeto secundario D3D u OGL. Pero aún existe el segundo problema con esta solución, que es que el código independiente de la plataforma se duplicaría en ambos niños, lo que ocasionaría esfuerzos adicionales de mantenimiento.Podría intentar resolver este problema moviendo el código independiente de la plataforma a la clase intermedia, pero ¿qué ocurre si algunos de los datos de los miembros son utilizados tanto por el código específico de la plataforma como por el independiente de la plataforma? Los niños D3D/OGL no podrán acceder a los miembros de datos en Impl del intermediario, por lo que tendrían que moverlos de la Impl y al encabezado, junto con las dependencias que llevan, exponiendo a cualquier persona que incluya su encabezado a toda esa basura que no necesitan saber.
Las API deben ser fáciles de usar, correctas y difíciles de usar. Parte de ser fácil de usar es restringir la exposición del usuario a solo las partes de la API que debería usar. Esta solución lo abre para que se use fácilmente de forma incorrecta y agrega gastos generales de mantenimiento. Los usuarios solo deberían preocuparse por la API de gráficos a la que apuntan en un solo lugar, no en todos los sitios donde usan su API, y no deberían estar expuestos a sus dependencias internas. Esta situación demanda clases parciales, pero no están disponibles en C++. Por lo tanto, en su lugar, simplemente puede definir la estructura Impl en archivos de encabezado separados, uno para D3D y otro para OGL, y ponga un interruptor #ifdef
en la parte superior del archivo Texture2D.cpp y defina el resto de la interfaz pública de forma universal. De esta forma, la interfaz pública tiene acceso a los datos privados que necesita, el único código duplicado son las declaraciones de los miembros de datos (la construcción aún se puede hacer en el constructor Texture2D que crea el Impl), sus dependencias privadas se mantienen privadas y los usuarios no tiene que preocuparse por nada, excepto utilizando el conjunto limitado de llamadas en la superficie expuesta de la API:
// D3DTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert D3D-specific stuff here */
};
// OGLTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert OGL-specific stuff here */
};
// Texture2D.cpp
#include "Texture2D.h"
#ifdef USING_D3D
#include "D3DTexture2DImpl.h"
#else
#include "OGLTexture2DImpl.h"
#endif
Key::Texture2D Texture2D::GetKey() const
{
return m->key;
}
// etc...
en primer lugar, las funciones no virtuales no incurren en "costos" y en segundo lugar, si el código no indica qué versión de la interfaz que está utilizando, trabajando en una versión romperá la otra versión, por ejemplo cuando pruebas-> win32Method(). Las clases específicas de plataforma no son lo mismo y merecen diferentes nombres. – Jamie
Y puede usar un método de fábrica para minimizar #ifdefs. Y puede usar plantillas (http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern) para reducir aún más. – Jamie
C++ es un lenguaje tan viejo y pobre (( –