• Advertisement
Sign in to follow this  
  • entries
    17
  • comments
    4
  • views
    32394

Practical Example of needing decoupling...

Sign in to follow this  

699 views

Some people are probably thinking "Matty, your crazy. Coupling
is how programs do things; there is no way around it!" Well...
all I have to say is its time to put on your big girl panties
and grasp the larger picture.

All software projects grow until they are abandoned. The more
flexible the project--the better the chances it can adapt
to new requirements. The better the coupling [eg, as minimal
as possible, and when it exists; use a balance of signature
and type coupling. Avoid logical coupling whenever possible.] the
more flexible the software.

Lets say that you have a killer game. It started off as a small project, but ended
up growing very quickly. Lets imagine the worst case scenario: the project
is in visual studio 2003. There is no seperation between the renderer and the MFC UI:
they are on seperate threads [hopefully], but in the same monolythic app. GL is the renderer,
so there are glXXX calls all over the renderer. There is no shader support, its
all GL FFP.

Lets say the first task you have to do is to add DX9 support. What could we do?
Lets follow the rule, that only simple classes should have external coupling.

Rule 1: NEVER MAKE DIRECT RENDERING API CALLS!
Rule 2: When in doubt, refer to rule 1!

GL is C; however, don't treat it as such. If you are stuck in C then
make your own vtable... but always make a virtual base class.

Make a file called RenderAPIInterface.h:

typedef enum
{
DEPTH_TEST = 0
// etc... all potential renderstates
} RenderAPIEnum;

typedef enum
{
DX9 = 0,
GL_FPP = 1
// etc... all supported renderers [GL+GLSL, GL+CG, DX+HLSL, DX11, etc.]
} RenderersEnum;

class CRenderAPIInterface
{
public:
virtual bool SetState(RenderAPIEnum RS, DWORD Value) {return false;}
// ...
};

class CRendererBuilder
{
static CRenderAPIInterface* CreateRenderer(RenderersEnum DesiredRenderer);
}


Now, this system is supposed to target both DX and GL.
We will need implementations for that.

Make a file DX9APIInterface.h:

#include "RenderAPIInterface.h"

class CDX9APIInterface : CRenderAPIInterface
{
public:
CDX9APIInterface();
~CDX9APIInterface();
virtual bool SetState(RenderAPIEnum RS, DWORD value);
// ...
};


In the code for the DX9 cpp
DX9APIInterface.cpp:

#include "DX9APIInterface.h"
#include "D3D9.h"

LPDIRECT3DDEVICE9 gDevice = NULL; // assuming a single global device, I'll get into who to
// encapsulate this later.


CDX9APIInterface::CDX9APIInterface()
{
// init gDevice
}

CDX9APIInterface::~CDX9APIInterface()
{
// teardown gDevice
}

bool CDX9APIInterface::SetState(RenderAPIEnum Value, DWORD value)
{
switch (Value)
{
case DEPTH_TEST:
{
gDevice->SetRenderState(D3DRS_ZENABLE, value);
break;
}

default:
{
return false;
}
}

return true;
}


Now we need to wrap fixed function GL.
Make a file GLFFPAPIInterface.h:


#include "RenderAPIInterface.h"

class CGLFFPAPIInterface : CRenderAPIInterface
{
public:
CGLFFPAPIInterface();
~CGLFFPAPIInterface();
virtual bool SetState(RenderAPIEnum RS, DWORD value);
// ...
};


Now we need GLFFPAPIInterface.cpp:

#include "GLFFPAPIInterface.h"
#include "g/GL.h"
#include "gl/GLU.h"

CGLFFPAPIInterface::CDX9RenderState()
{
// init GL
}

CGLFFPAPIInterface::~CDX9RenderState()
{
// teardown GL
}

bool CGLFFPAPIInterface::SetState(RenderAPIEnum Value, DWORD value)
{
switch (Value)
{
case DEPTH_TEST:
{
::glEnable(GL_DEPTH_TEST);
break;
}

default:
{
return false;
}
}

return true;
}


The final file we need at this point is CRendererBuilder.cpp

#include "GLFFPAPIInterface.h"
#include "DX9APIInterface.h"

CRenderAPIInterface* CRendererBuilder::CreateRenderer(RenderersEnum DesiredRenderer)
{
switch (DesiredRenderer)
{
case DX9: return (CRenderAPIInterface*)new CDX9RenderState();
case GL_FPP: return (CRenderAPIInterface*)new CGLFFPAPIInterface();
}

return NULL;
}


Okay, there is a lot of typing there... But conceptually it is really simple.
You want to have a renderer that can run in a few modes. In this case DX9 and GL.

Yes, this is a heavy handed method... you will have to copy a lot of
both sets of headers into your own system. Its not the most glamourous work, but is also well worth it.

We are going to use ambigious type coupling to isolate the rendering API.

Stub out all of the set/get/create features in the base class. In the derived classes
map the calls into their api specific calls.

The only places where your application should talk directly DX9/GL are in these 3 CPP's.
If the rest of the system talks to the renderer via its base class -- then you can
drop renderer's in and out to your hearts content. -- you have completely uncoupled
the core of the system from the rendering API!

[NOTE: you WILL need to also have texture and potentially mesh cpp's are bound to the API]

Lets say you want to add new "middleware" but keep the base GL or DX9 support.
Rendering middleware... What do I mean by that? Well, graphics APIs are essentially bunk
today. DX and OpenGL talk to the same physical graphics card hardware. Before hardware T&L
and shaders you could really argue which was "better." Now, all the do is hand up values
across the pcie bus.

Straight DX and GL use fixed function middleware. You upload the values for per-vertex lighting.
The card then does its magic [even though this is simulated by shaders on newer cards...]

Historically, the way I've encapsulated the change to shaders has been to add more
APIInterface's like the ones above. You could add another 2:


class CDX9HLSLAPIInterface : CDX9APIInterface

and

class CGLGLSLAPIInterface : CGLFFPAPIInterface


Now, in each of these new base classes... simply extend your
functionality to call the shaders.

After that, extend your builder to:


CRenderAPIInterface* CRendererBuilder::CreateRenderer(RenderersEnum DesiredRenderer)
{
switch (DesiredRenderer)
{
case DX9: return (CRenderAPIInterface*)new CDX9RenderState();
case GL_FPP: return (CRenderAPIInterface*)new CGLFFPAPIInterface();
case DX9_HLSL:return (CRenderAPIInterface*)new CDX9HLSLAPIInterface();
case DX9_GLSL:return (CRenderAPIInterface*)new CGLGLSLAPIInterface();
}

return NULL;
}


Here are some thought exercises:
* How could we make this better/safer with smart pointers?

* How do smart pointers effect coupling?

This decoupling example uses ambiguous type coupling:
* What would be needed to do signature coupling?
* What what signature coupling get us? What would it cost us?

* How would we recast this in the other modern languages?
[Particularly C# -- what is the immediate danger?]

* Say later we go cross platform. How else does this type of
coupling/encapsulation help?

The answers might surprise you...

More later,
Matty
Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Advertisement