SOLID and game (engine) design

Started by
22 comments, last by Juliean 11 years ago

Hello,

so as suggested to me in a former thread where I already asked something about writing a game engine solid, I've actually just carried on writing some basic graphics capabilities. Now I've got things running and actually came back to refactor these and hopefully finally learn those principles. I've read a lot of articles multiple times about the topic of S.O.L.I.D and about each idiom, but then again I best learn by doing, so I've tried. I still got some more questions and things I want to make sure before I overcomplicate things, especially things

- Single responsibility: I actually have a hard time destinquishing what a single responsibility is, that I should have each class perform. From what I've got its nothing as wide as "handling textures" and nothing as close as "acessing a texture", while I've even seen people suggest you should basically make a lot of classes that, due to their completely isolated task, do often not have more than one or two methods. On the other hand, I'd rather prefer classes that at least are something more than a namespace function and some data, so I'd rather go with that single responsibility is when "a class performs on the same set of data". Is this a correct case of this pattern? What would you suggest, and why? And is there some help for people who have a hard time trying to (conceptually) seperate concerns?

- Dependency inversion principly. For this, I've started actually implementing "interfaces" (say, classes with (almost -> we'll get to that later) only pure virtual functions), that I use to implement the actual buisness logic. I'm still unsure whether I'm doing it right, since most sample impementation where eigther targeted towards java or especially not game programming. So let's look at what I did with my graphics libary. Here how effect and texture worked before:


#pragma once
#include "D3DEffectPool.h"
#include "D3DEffect.h"
#include "Device.h"
#include "Texture.h"
#include <string>

class Effect
{
public:
    Effect(Device& device, D3DEffectPool& d3DEffectPool, const std::wstring& sFileName);
    virtual ~Effect(void);

    void SetTexture(LPCSTR lpName, const Texture* pTexture);
    void SetMatrix(LPCSTR lpName, const D3DXMATRIX& matrix);
    void SetFloat(LPCSTR lpName, float value);
    void SetFloat3(LPCSTR lpName, float first, float second, float third);
    void SetFloat4(LPCSTR lpName, float first, float second, float third, float fourth);
    void SetTechnique(LPCSTR lpName);

    void Begin(void) const;
    void BeginPass(void) const;
    void ApplyChanges(void) const;
    void End(void) const;
    void EndPass(void) const;

private:

    D3DEffect* m_pD3DEffect;
};
 

class Texture
{
public:
	Texture(D3DTexture* pTexture);
	virtual ~Texture(void);

	int GetWidth(void) const;
	int GetHeight(void) const;
	int GetRef(void) const;

	void AddRef(void);
	void Release(void);

	D3DTexture* GetD3DTexture(void) const;

private:

	D3DTexture* m_pTexture;

	int m_Ref;
};

So obviously both those classes would both provide the "interface" as well as the implementation. They would be used directly at any part of the respective graphics pipeline, therefore e.g. effect would depend on texture, and so on. Now to resolve that, I tried this:


´#pragma once
#include <d3dx9.h> //todo: remove, implement custom matrices!!!
#include "ITexture.h"
#include "IResource.h"

class IEffect : public IResource
{
public:

	virtual void SetTexture(LPCSTR lpName, const ITexture* pTexture) = 0;
	virtual void SetMatrix(LPCSTR lpName, const D3DXMATRIX& matrix) = 0;
	virtual void SetFloat(LPCSTR lpName, float value) = 0;
	virtual void SetFloat3(LPCSTR lpName, float first, float second, float third) = 0;
	virtual void SetFloat4(LPCSTR lpName, float first, float second, float third, float fourth) = 0;
	virtual void SetTechnique(LPCSTR lpName) = 0;

	virtual void Begin(void) const = 0;
	virtual void ApplyChanges(void) const = 0;
	virtual void End(void) const = 0;
};


#pragma once
#include "IResource.h"

class ITexture : public IResource
{
public:
    virtual int GetWidth(void) const = 0;
    virtual int GetHeight(void) const = 0;
};

#pragma once

class IResource
{
public:
	int GetRef(void) const { return m_numRef; }

	void AddRef(void) { m_numRef += 1; }
	virtual void Release(void) = 0;

protected:

	int m_numRef;
};

Now I have three interfaces, one implements the reference counting, the other two are those from whom I inherit my actual texture and effect classes from. Now my questions: Now obviously everywhere where I used to have a Texture or Effect object, I'd instead have a ITexture or IEffect pointer.

1. is this a correct approach at dependency inversion? I just want to be sure, so in case this is OK, a simple "yes, is how dependency inversion works", or other wise "no, it would rather go that way..." would be nice. Remember, we are talking about applying one/more programming idioms, its not about getting things working (for the few people who'd say "do whatever works").

2. In case this is a correct approach, when and where should I apply this? Should I really create an "interface" for each and every dependency I might have, if not, how do I destinquish between cases where it makes sense and where not?

3. What about shared code and member variables? I didn't find any common "policies" for c++, take a look for example at my IResource class, should I rather have interfaces shared data member like the "m_numRef" and implement some common logic (like the setter and getter here) or should I really depend on the actual implemented class(es) to handle everything?

4. Even though I only depend on the interfaces, I might need to determine the actual type of an class at some point. I've thought about it, but since everywhere I'd generally pass an interface class, I can't think of any solution that goes without having to cast the actual type out of something. Not to say that casting is bad, but I wonder if this is "good practice" too, or if there is an better option. Like here:



void D3DEffect::SetTexture(LPCSTR lpName, const ITexture* pTexture)
{
    //todo -> -> is evil, maybe overthink low level abstraction layer?
    if(!pTexture)
        m_pD3DEffect->SetTexture(lpName, nullptr);
    else
        m_pD3DEffect->SetTexture(lpName, static_cast<const Texture*>(pTexture)->GetD3DTexture());
}

This is my D3DEffect - implementation of the IEffect interface. As the pure virtual function takes an ITexture object, but the LPD3DXEFFECT needs a LPDIRECT3DTEXTURE9, I must cast, since I need to extract this texture which is part of the "class D3DTexture : public ITexture" implemenatation. Is this ok, or is there some better/cleaner way to achieve this? One potentialy issue is that one can pass an OpenGLTexture or XNATexture-implemenatition to the D3DEffect, which would throw an error - is this something I just have to deal with? Granted this is probably a very special case since you won't need this type destinction in most other applications, but still, how should I handle this?

5. As you see, IEffect still depends on ITexture. I've never found one word written about the dependency between interfaces. I think this is somewhat obvious but I still want to ask: this is normal and that kind of dependency we tolerate/want to achieve, right?

So this is it with my questions for now. An answer to any of those would be greatly apprechiated, I'm sure a lot of you guys have experience with those patterns. Like mentioned one time, please don't tell me "do whatever works for you", since I already have anything working, and this is my personal "lesson" for learing solid, so I really want to get it right this time, before faulty, half-a**ed over-complicated applying some not well-understood pattern to my engine that won't increase my code anyhow.

Advertisement

I'm sure others will have lots to say, but I do have one question: why can't ITexture have a GetTexture method? Do you really need runtime support for multiple 3D API libraries? It can't just be a compile-time switches?


Multi-API support is a requirement for me, yes. As for compile time switches... as in compile-time switches using compiler defines? I'm not so sure, I think it would be kind of cool if I could switch between different APIs on runtime instead of having different binaries, but compile time switch should do, if there is no better option. So I would just have a GetTexture method in ITexture, or also the respective texture object?

On the other hand, wouldn't that break a part of the purpose of depenceny inversion? Since I'm now depending in changing the interface to achieve different business logic. Also someone would need to change the ITexture interface in order to implement some renderer not forseen by me. You suggest that this is ok in this case, right?

I guess I'm saying that you haven't provided enough info to know if making things interfaces is the right design decision. What is your end goal? If you really do need runtime switching over different APIs, and things like the possibility for someone to develop a new pluggable implementation of ITexture, then sure - what you have is a good start.

I wouldn't cast the ITexture to a particular class type though. Instead, I would make ITexture have some method like GetNativeTexture(some-identifier-of-the-type-requested) so it has a chance to fail gracefully if the it is not of the right type. Or you could use RTTI and dynamic_cast the ITexture to an ID3DTexture which has a GetD3DTexture method. Both these would be better than the static_cast (again, if you really expect different ITexture implementations - which is the whole point of dependency inversion).

If you aren't sure if you need all this plug-and-play kind of stuff, and this is just for yourself, then I would just keep things simple and use classes like you were originally doing. It's easy enough to make the necessary abstractions when you actually need them.

I guess I'm saying that you haven't provided enough info to know if making things interfaces is the right design decision. What is your end goal? If you really do need runtime switching over different APIs, and things like the possibility for someone to develop a new pluggable implementation of ITexture, then sure - what you have is a good start.

Well I got to say, maybe I asked too specifically - though I brought that example, this wasn't just all about the graphics stuff - since I've read and heard a lot about it, I see SOLID as some very handy principles, that I want to learn - even if it means making things a little more complicated than they need to. In fact things like making code const correct, pass by reference instead of value where needed, not putting using namespace std etc... have provided a good source of motivation for me - simply putting out code that works in a way isn't really satisfying me anymore. Then again, I see where you are coming from. Obviously applying the right pattern at the wrong time isn't going to help much. But then again, if I have the choice of keeping things simply and less modular but probably never needing those modularities, and having a well written codebase which is probably more expandable and modular then needed while having spent some time more on developing it - I'd choose the latter. Just to clarify my motivation for all this.

As to if I am going to need it - I'm certainly sure I'm going to want to "release" my code some time when I've refactored it enough to make it useable for someone else than me. I think sticking to the SOLID principles is definately going to help me - and prevent eventually reaching a point where my code is so messed up that even I can't work with it any more. Like I said, there is relatively little concerning games and SOLID, in fact I haven't seen even a line of pseudo-code for any of game developement sub areas. That what I had in mind when asking those questions.

I wouldn't cast the ITexture to a particular class type though. Instead, I would make ITexture have some method like GetNativeTexture(some-identifier-of-the-type-requested) so it has a chance to fail gracefully if the it is not of the right type. Or you could use RTTI and dynamic_cast the ITexture to an ID3DTexture which has a GetD3DTexture method. Both these would be better than the static_cast (again, if you really expect different ITexture implementations - which is the whole point of dependency inversion).

Isn't RTTI and dynamic_cast comperably slow? Since those are performance sensitive areas, this would be important to know - I'm just assuming, I'm not sure if those actually cost much? As for your first method, this would definately be a good option - in fact this seems to be what I was looking for in this particular issue, thanks.

Still I'd be glad to hear some opinions on the other questions I've posted ;)

Dependency Inversion Principle

2. In case this is a correct approach, when and where should I apply this? Should I really create an "interface" for each and every dependency I might have, if not, how do I destinquish between cases where it makes sense and where not?

'interface' means the portion of a class that is publicly visible. The interface does not have to be a separate pure virtual class (interface in Java / c#).

The key idea behind the Dependency Inversion Principle is that the clients impose the interface on the servers, not the other way around. The interface is owned by the clients and is designed to meet their needs.

In an attempt to adhere to this principle I started introducing pure virtual classes everywhere, but I realised that they were adding unnecessary complexity. As long as the server's interface is imposed by the client it doesn't matter if the interface is defined separately by a pure virtual class or not.

I generally treat pure virtual classes exactly as I would treat any super class. The top level class of an inheritance hierarchy should contain as much common functionality as possible (Don't Repeat Yourself). The application of the DRY principle usually dictates the type of class that will sit at the top of my inheritance hierarchies.

In saying that, sometimes a pure virtual class makes sense, as a pure virtual class describes what an object does, and not what an object is.

These principles are tools to help you manage the complexity of your design. They are a guide only and applying them effectively comes with experience.

Matt

Single Responsibility Principle

I'd rather go with that single responsibility is when "a class performs on the same set of data".

I find SRP difficult. I do use "a class performs on the same set of data" as an indication of the number of responsibilities a class has.

You can't always look at a class in isolation and determine how many responsibilities it has, because the number of responsibilities is often dependent on how the class is used by its clients.

It's a good idea to look at the clients that are using your class and to see how much of the class each client is using, and how they are using it. This may give you an idea of the responsibilities that the clients see the class as having.

Remember also that a class can have multiple responsibilities (particularly if they are all realised using the same data members), but the responsibilities can be separated by creating a pure virtual class for each responsibility which the class inherits. Clients can then use the pure virtual class in place of the original class so that they are only aware of the parts of the class that they use.

Matt



The key idea behind the Dependency Inversion Principle is that the clients impose the interface on the servers, not the other way around. The interface is owned by the clients and is designed to meet their needs.

In an attempt to adhere to this principle I started introducing pure virtual classes everywhere, but I realised that they were adding unnecessary complexity. As long as the server's interface is imposed by the client it doesn't matter if the interface is defined separately by a pure virtual class or not.

Can you give an example of how this would work without having a pure virtual interface of the client? Since once of he main principles of dependency inversion is that a high level components won't depend on low level components (like in my case, the gfx-classes not depending on the low-level graphics wrapper like I did before), I currently can't think of any other way to do so - without making the low level components depend on the high level that is, which would be an even bader idea. Hell, I'm kind of stuck in that thinking - now that I somewhat fully gasped the meaning and applyance of SOLID/DI, I can't stop thinking in interfaces and class seperations anymore. Surely I want to avoid unneccessary complexety. But as you said, you made that mistake once and learned from it, so heck, maybe that wouldn't be so bad for me after all, too.

I generally treat pure virtual classes exactly as I would treat any super class. The top level class of an inheritance hierarchy should contain as much common functionality as possible (Don't Repeat Yourself). The application of the DRY principle usually dictates the type of class that will sit at the top of my inheritance hierarchies.

So given that, and if I were to really need a pure virtual class as an interface in a place where it makes sense, but also have quite a lot of classes that inherit from it - I would inherit one base-class from this interface, write the common functionality in that base class and then derive the other classes from this? Like in this example (this time not counting if it actually makes sense to have the interface, lets pretend it does): I'd have an IRenderable interface, from this I'd derive a BaseRenderable which implements common functionality, and from this I'd further derive my actual renderables like Mesh, Terrain, ... , right?

These principles are tools to help you manage the complexity of your design. They are a guide only and applying them effectively comes with experience.

Rightfully so, I think there is no way to learn those without probably misusing them a few times, like it is most time. Gladely there are forums like gamedevs where one can asked people who already know better how to use such things.

I find SRP difficult. I do use "a class performs on the same set of data" as an indication of the number of responsibilities a class has.



You can't always look at a class in isolation and determine how many responsibilities it has, because the number of responsibilities is often dependent on how the class is used by its clients.



It's a good idea to look at the clients that are using your class and to see how much of the class each client is using, and how they are using it. This may give you an idea of the responsibilities that the clients see the class as having.

Ok, seems like I'm not off the track entirely. Then again, I've always heard that most "Manager" classes are bad or misused, like I did in the past. While they all performed on the same set of data, they implied unnecessary restrictions. But I think with more practice I should be able to determine that better.

Remember also that a class can have multiple responsibilities (particularly if they are all realised using the same data members), but the responsibilities can be separated by creating a pure virtual class for each responsibility which the class inherits. Clients can then use the pure virtual class in place of the original class so that they are only aware of the parts of the class that they use.

Ah, so you are suggestion multiple inheritance here, right? I didn't think of that before, I never even used it before, but from what I read (did a quick google search) this seems to be a good case for multiple inheritance, from interfaces that is. Just to make sure I got it right, I would have the clients that need just a portion of the functionality of the class pass a pointer to one of its multiple inherited interfaces, right? Again one example, so I would inherit my Gfx3D-class from ITextureModule, IMeshModule, IEffectModule, and have it implement all their pure virtual functions for loading / storing stuff etc. Then whenever one class needs to e.g. create some textures, I'd pass my Gfx3D-object to it, as ITextureModule-pointer, right? What would I do if a class needs multiple functionality, but still not all? Say mesh and effect, I'd pass the same Gfx3D-object twice, but once via the IMeshModule, and one time via the IEffectModule, right?

Thanks a lot so far, I'd be glad if you could further eleborate on the questions I had in response to you...

I personally find that the SRP is not something you plan in advance, but something you watch for as you develop. Say you have a simple class, and you add more methods and properties to the class as you go along. Each time you add something, consider whether it makes sense for some of the functionality to be split out into a new class - and if so, do it. If the functionality can stand alone, it probably should stand alone. And even if it is only used inside another class, if it is a self-contained package of functionality, it should be encapsulated as such.

On the other hand, I'd rather prefer classes that at least are something more than a namespace function and some data, so I'd rather go with that single responsibility is when "a class performs on the same set of data". Is this a correct case of this pattern?

Why do you care how many methods a class has?

And if it were possible to say that acting on the same set of data is enough to call it a single responsibility, then you could put your whole program into one class. After all, it all acts on one set of data - namely, all the data that the program uses.


#pragma once

class IResource
{
public:
int GetRef(void) const { return m_numRef; }

void AddRef(void) { m_numRef += 1; }
virtual void Release(void) = 0;

protected:

int m_numRef;
};

IResource has absolutely nothing to do with resources. It's a reference counter. Call it IReferenceCounter. Also, never expose protected data, it's as much a no-no as public data. Your reference counting implementation as it stands is going to be extremely tightly coupled to absolutely everything that touches it.

Far better to remove it entirely and use RAII and smart pointers as appropriate to automate that stuff. Remember 'has a' vs. 'is a' relationships. TextureResources and EffectResources might HAVE ref counted components, but they don't necessarily need to BE ref counted themselves, so long as what they contain is managed properly. Do this and you can dispose of that coupling entirely.

This topic is closed to new replies.

Advertisement