Sign in to follow this  
Amnesiac5

Decoupling classes

Recommended Posts

Amnesiac5    157
I'm writing my own obj file loader in C++ and things are going swimmingly after a few hiccups here and there. However, I'm trying to do everything the OO way and my current "prototype" isn't (surprise, surprise... I wouldn't be here cap in hand otherwise [smile]). Currently, I have a single class that encapsulates both obj file reader and mesh behaviours and I would like to abstract them into two distinct classes. Both the obj file reader and mesh know about things like points, 3d vectors, normals and so forth, and there are things that only one class or the other will know about such as files and Gouraud normals. However, even though both mesh and object file reader know about vectors, for instance, there's no guarantee that they are implemented in the same way in each class, furthermore neither class will know how the other accesses its data. So, how to properly decouple them? My first idea is to pass function pointers for the mesh object methods to the obj file reader which it would call to create a 3d vector, assign ordinate value and store vector, face and index data. Does this seem reasonable? My suspicion is that it is a/ inefficient, b/ wrong. Suggestions welcome.

Share this post


Link to post
Share on other sites
deathkrush    350
The function pointer approach is a very good idea and I have seen it in many open-source compression libraries. However, there is a performance penalty for calling the function pointer in a tight loop. If you can make the compiler inline the function call, there is no such penalty. For example:


struct Functor
{
inline void operator()(...);
};

template <class T>
void LoadOBJ(T& functor, ...)
{
while(...)
{
functor(...);
}
}


Share this post


Link to post
Share on other sites
Verg    450
On a more abstract level:

Each class should have a single responsibility. That said, no class is an island.

If a class' single responsibility can be broken up into smaller steps, it should then delegate those steps to classes that are more atomic. You can set that up in many ways, like aggregation and composition, or with policy classes.

The only thing left to cover is... what are the inputs for your class object, and where does it get them from?

Either an object gets the data it needs when it is born (constructor), through dependency injection, or through dependency lookup.

If, for example, you need to set the bitmap that an object is working on... you may set it in the constructor. But, if your program needs to keep your BitmapWorker around, and use it with other bitmaps, you could pass it in the main method your BitmapWorker calls.

If, however, your BitmapWorker's main method is called many times in a loop... for example... sending the same bitmap pointer each time would be wasteful. In this case, it would be all right to put a special function on your class to accept a bitmap before you call the class' main method.

...

As to the particulars of your situation... just make sure each object has what it needs, and isn't constantly pestering other objects for what it needs. Usually thats a sign that the data should exist where it is needed, and not where it is held.

HTH


Chad

[Edited by - Verg on August 5, 2007 7:59:31 PM]

Share this post


Link to post
Share on other sites
steven katic    275
well, it sounds to me, like you are almost answering you own questions..and want
some second opinions on the idea:

"...I would like to abstract them into two distinct classes."

good idea,do exactly that. And take advantage of the purpose behind OO.

How far into using OO ideas depends on you aim:

e.g.
(1) Learn more about how to OO?
(2) Learn more about refactoring the "prototype" you now have into some sort of good 00 design?
(3) Find out what other people would do?
(4) Reuse the mesh object to load model formats other than obj(you do mean maya .obj format do you?)

Verg's post contains good generalizations to get you thinking.

Sometimes, the difficulty with giving advice on these boards is acertaining
the level of knowledge of the target audience (i.e. you):

I'll just give some more specific info with regards to what you are doing and what I have done:

(1) make the 2 classes you mention.

e.g. class CObjReader, class CMesh

(2) encapsulate all the mesh properties, types, data etc into the CMesh class and do not make any of it visible
to the CObjReader that it doesn't need to know about.
Do the same with the CObjReader: e.g have methods that read the data file.

The idea here is minimum coupling:

CMesh can and should know nothing at all about CObjReader.
The least the ObjReader really needs to see is a Cmesh (better still an interface to a CMesh ).

(3) Equip Cmesh with methods into which you can feed the the raw data that the CObjReader will obtain from the obj file.
raw data here refers to the integral types you obtain from the file (practically only ints and floats ...and strings maybe).

At the risk of sounding obvious, 3D geometry data (i.e. your CMesh) will always contain generic 3D data types:
e.g. Vertices, Triangles, Materials, plus more specific info like normals (vertex or triangle?), texture u,v (in a Material)
depending on you needs/implementaion.

So...how to couple the objects:

e.g. pseudocode-> at obj file load time create the CObjectReader, create a CMesh and pass a CMesh reference/pointer to the CObjectReader:

String^ filePath;
CMesh^ newMesh = gcnew CMesh();
CObjectReader objReader = gcnew CObjectReader();
objReader->DoImport(filePath, newMesh);
....
// then add the object to the scene
m_scene->Add(newMesh );

What happens in DoImport()?

(a) CObjectReader opens the file and reads it
(b) as soon as pertinent data is read load into the newMesh;

e.g. Lets say you have:

CMesh^ m_MeshRef = newMesh;//as data member in the CObjectReader class

and to read a vertex:

switch(ch)
{
case 'v':

ReadVertexInfo();

SetReadingState(READING_VERTEX_BLOCK);
m_VertexCnt++;
break;
// other cases...
.......
.......
default: break;
}

and:

ReadVertexInfo()
{
float x,y,z;
read3floats("%f %f %f", &x, &y, &z);
m_MeshRef->AddVertex(x, y, z);
}

and thus all sorts of carefully thought out methods to load other 3D data/into:
like: AddTriangle(..), AddNormal(..) etc.

I hope this has given you some ideas. You may well find many additions/other options along the way.

We go a little further in an implemenation we have which is a Plugin system where various model formats
can be loaded via an IModel Interface that any particular format reader class created, can use. And the
ideas shown above was a basis for it. I am very happy with its performance, its quick, clean and readable/easily
maintainable.

[Edited by - steven katic on August 6, 2007 12:36:00 AM]

Share this post


Link to post
Share on other sites
Amnesiac5    157
Thanks all, for the very interesting feedback. The responses are pretty much exactly what I was hoping for, although I still have to get my head around the syntax [smile].

deathkrush - performance is something that usually bothers me so thanks for the inlining pointer (no pun intended).

Verg - interesting links. What you say is pretty much my understanding of OO and the tack I'm trying to take.
Quote:
Original post by steven katic
well, it sounds to me, like you are almost answering you own questions..and want some second opinions on the idea:
Well, yes. I'm learning (aren't we all) and want to make sure that I'm not heading down a cul-de-sac.
Quote:
Original post by steven katic
How far into using OO ideas depends on your aim: e.g.
(1) Learn more about how to OO?
(2) Learn more about refactoring the "prototype" you now have into some sort of good 00 design?
(3) Find out what other people would do?
(4) Reuse the mesh object to load model formats other than obj(you do mean maya .obj format do you?)
All of the above.

I'm learning/playing about with Opengl and one of the first limitations I encountered was the lack of model definition loading. Rather than learn another API or adopt someone else's idea of what good data structures are, I've undertaken to write my own routines. Partly as a learning exercise in its own right and partly as a practical application of OO principals.
Quote:
Original post by steven katic
The least the ObjReader really needs to see is a CMesh (better still an interface to a CMesh).
I don't want ObjFileReader to even know about Mesh, only some of Mesh's methods e.g. AddTriangle.
Quote:
Original post by steven katic
At the risk of sounding obvious, 3D geometry data (i.e. your CMesh) will always contain generic 3D data types:
e.g. Vertices, Triangles, Materials, plus more specific info like normals (vertex or triangle?), texture u,v (in a Material) depending on you needs/implementation.
Hmm. Both ObjFileReader and Mesh know about these generic types. Am I right in thinking that it's necessary to code against the possibility that each class implements them differently, and hence abstract out methods which assign values? Or are they so generic that is this overkill?
Quote:
Original post by steven katic
I hope this has given you some ideas.

Oh yes, indeedy! [grin]

Share this post


Link to post
Share on other sites
steven katic    275
Hi Amnesiac5,

Quote:

I don't want ObjFileReader to even know about Mesh, ...


Why?
That was blunt wasn't it ;)
Give me a good reason why not and I'll give you some reasons why ObjFileReader does know about CMesh in the example.

Quote:

Hmm. Both ObjFileReader and Mesh know about these generic types. Am I right in thinking that it's necessary to code against the possibility that each class implements them differently, and hence abstract out methods which assign values?
or are they so generic that is this overkill?


well sort of..almost there...again?

Bear with me->

Coupling and Cohesion:

"Systems that do not exhibit low coupling might experience the following developmental difficulties:

Change in one module force a ripple of changes in other module.
Modules are difficult to understand in isolation.
Modules are difficult to reuse or test because dependent modules must be included."

I'm being lazy: cutting and pasting the quote.
But I can see vague similarities between your statement and the quote. Can you?
The quote just seems a little more succinct and lucid.

In short: by using the integral types in the ObjFileReader, it can avoid needlessly interacting with other objects
(i.e. a vertex, a triangle).[I'm assuming the vertex, triangle etc are classes or structs).
Hence, you will reduce the chances of experiencing first hand those things in the quote.

My point of view is: All ObjFileReader is doing is reading bytes from a file according to the file format spec.
Give it that task and give it everything else on a "need to know basis".

I can also give you concrete examples from experience as to how using more objects in ObjFileReader,
might (and has) led to the problems in the quote, if you press me. But hopefully you'll google
"Coupling and Cohesion" for a more conceptual view that expands on the quote above well enough for you.


oh and:

Quote:

I still have to get my head around the syntax


Do you mean the psuedocode I wrote? I hope not.

Whenever I provide psuedocode: The syntax is usually a just a mixed bag of stuff that comes to mind
(probably based around C, C++ variations). My idea is to provide meaningful method names for what is
happening in the code quickly, and for the reader to ignore the real meaning in the syntax. If it's not working
I will have to make the code even more "pseudo" i.e. closer to english than to any language like C/C++.

Share this post


Link to post
Share on other sites
Amnesiac5    157
Quote:
Quote:
I don't want ObjFileReader to even know about Mesh, ...
Why?
That was blunt wasn't it ;)
Give me a good reason why not and I'll give you some reasons why ObjFileReader does know about CMesh in the example.
Quote:
Hmm. Both ObjFileReader and Mesh know about these generic types. Am I right in thinking that it's necessary to code against the possibility that each class implements them differently, and hence abstract out methods which assign values?
or are they so generic that is this overkill?
Well, my latter quote was an attempt to explain the former (unintentional bluntness and all [wink]).

The ObjFileReader implementations that I've seen all include some "arbitrary" structs to cope with vectors and texture co-ordinates. If I were to use one of these I would have adopt those same structs, write code to convert to my own data structs or change the "foreign" source.

Likewise, if someone else were to use my implementation they would face the same problem. Am I missing something? Is this overkill? Practically speaking, I don't need this degree of genericity for my own code, but I would like to be able to identify when it's needed and practice its implementation.
Quote:
well sort of..almost there...again?

Bear with me->

Coupling and Cohesion:

... snippity snip...
Fair comment, but what if ObjFileReader doesn't know about vertices, text co-ordinates, etc., but does know how to pass appropriate values to methods it is has been passed? The ObjFileReader may well still use it's own structs, but the user isn't burdened with using or even having to know about them. Does that make any sense?

This may be one of those things I'll have to play about with to get the hang of... which I will then go and promptly forget... [dead].
Quote:
oh and:
Quote:
I still have to get my head around the syntax
Do you mean the psuedocode I wrote? I hope not.
No, not at all. I was referring to function pointer and functor syntax! Help!!! [wow]

Share this post


Link to post
Share on other sites
steven katic    275
I think I see my misunderstanding now:

I'm talking about:

Quote:
(4) Reuse the mesh object to load model formats other than obj(you do mean maya .obj format do you?)


whereas it is now clear you are talking about reusing the ObjFileReader(s).

Share this post


Link to post
Share on other sites
RobTheBloke    2553
I tend to have a large (erm, very large and very complex) data structure at the core of my data transfer. It basically knows how to save itself and load itself into it's own format. (ie your CMesh, CTexCoord etc - I'll just call this CModel for all data). I then provide an interface such as :

struct FileTranslator
{
// override to say if the file translator can import/export
virtual bool CanImport() = 0;
virtual bool CanExport() = 0;

// override to impliment data conversion into & outof the CModel class
virtual bool Import(const char* filename,CModel& model) = 0;
virtual bool Export(const char* filename,CModel& model) = 0;

// override to see if the file translator handles the specified
// extension - e.g. "obj", "3ds" etc
virtual bool SupportsExtension(const char* ext) = 0;

// returns some info about the file translator - e.g. "Obj Loader"
virtual const char* GetInfo() = 0;
};


I impliment *ALL* file loaders as stand alone units, i.e. I'll have an ObjLoad class such as :

class ObjLoad {
public:

bool load(const char* filename);
bool save(const char* filename);
};


I do this primarily for code re-use. The Obj loader is a self contained entity that only loads and saves the obj data. It has no other dependencies on any other library of code - which means you can easily pull it into any other project you need in future. It doesn't contain rendering code, it doesn't contain any maths operators - It just loads and saves and nothing else....

I then provide an ObjFileTranslator that derives from FileTranslator, and holds a member variable of type ObjLoad. The file translator class therefore handles the conversion of your obj data into your own data structures. If your core data structures extend over time, then usually it wont affect your obj loading/saving code - but you will require small changes to be made to the translators (which is usually a trivial excercise.

The other advantage is that you can have a conv tool that converts all assets into your own (CModel) data format (via a load of plug-in file translator classes). The game can simply use the CModel class in isolation to load all of your game data....

Share this post


Link to post
Share on other sites
Amnesiac5    157
Quote:
Original post by steven katic
I think I see my misunderstanding now:

I'm talking about:
Quote:
(4) Reuse the mesh object to load model formats other than obj(you do mean maya .obj format do you?)
whereas it is now clear you are talking about reusing the ObjFileReader(s).

Whoops, my bad, probably didn't explain myself clearly enough, soz. I am still learning, you know.[smile]

Share this post


Link to post
Share on other sites
Amnesiac5    157
Quote:
Original post by RobTheBloke
I tend to have a large (erm, very large and very complex) data structure at the core of my data transfer. It basically knows how to save itself and load itself into it's own format. (ie your CMesh, CTexCoord etc - I'll just call this CModel for all data). I then provide an interface such as :
struct FileTranslator
{
*** Source Snippet Removed ***
};
Ooh, informative, thanks for that.
Quote:
Original post by RobTheBloke
I impliment *ALL* file loaders as stand alone units, i.e. I'll have an ObjLoad class such as :

*** Source Snippet Removed ***

I do this primarily for code re-use. The Obj loader is a self contained entity that only loads and saves the obj data. It has no other dependencies on any other library of code - which means you can easily pull it into any other project you need in future. It doesn't contain rendering code, it doesn't contain any maths operators - It just loads and saves and nothing else...
I think this is what I'm trying to get at.

Well, I think it's now time for me to stop talking and do some coding. Probably be back soon.

Share this post


Link to post
Share on other sites
steven katic    275
Hi RobTheBloke,

That doesn't look too dissimilar to my example, except you have the additional
FileTranslator object in which people can safely mess about with getting
the data into their own data structures.

[Although...the words we use to express our implementations are like chalk and
cheese! amazing..well it was for a 10th of a second there!]

So, instead of using other ObjFileReaders and their data structures where Amnesiac5 would have to :

[quote]
adopt those same structs, write code to convert to my own data structs or change the "foreign" source.
[\quote ]

He could safely do the same thing but within the percieved safety of a class
or FileTranslator object derivative (like yours), and maybe do it a little more easily or in a more controlled manner ( as 00 implies, if done well).

Ultimately, there's no getting away from the [edit:lowest was the wrong word] finest granularity we usually stop at: of getting those 3 floats for a vertex and those integers for indexes from the file and into our Mesh.

Ah, the good old days of C where we didn't care to hide our data in contrived
Object Oriented data structures! {Ooops... did I say that, I hope I haven't upset the 00 zealots out there!)

But, seriously folks... I get the impression Amnesiac5 wants to use function pointers

[Edited by - steven katic on August 9, 2007 2:32:48 PM]

Share this post


Link to post
Share on other sites
Amnesiac5    157
If anyone is still following this thread, I now have a fully working program with ObjFileReader and Mesh completely decoupled. The code looks something like the following minimalist snippets...

#include <vector>
#include <map>
#include <string>

using namespace std;
// ObjFileReader.h
typedef void (*PtrToFun)(const vector<string> vertices);

class ObjFileReader {
public:
static void (*pfAddVector00)(const vector<string> vertices);

static void Load(string sFileName, map<string, PtrToFun> pfFuncs );
};
// ObjFileReader.cpp
PtrToFun ObjFileReader::pfAddVector00 = NULL;

void ObjFileReader::Load(string sFileName, map<string, PtrToFun> pfFuncs ) {
pfAddVector00 = pfFuncs["AddVector"];

vector<string> vec;
(*pfAddVector00)(vec);
}
// Mesh.h
class Mesh {
public:
static void addTextureIndices(const vector<string> vertices);
};

// Mesh.cpp
void Mesh::addTextureIndices(const vector<string> vertices) { };
typedef void (*PtrToFun)(const vector<string> vertices);
// main.cpp
int main() {
map<string, PtrToFun> mapLoadFunctionPtrs;
mapLoadFunctionPtrs["AddVector"] = Mesh::addTextureIndices;

string filename = "name";
ObjFileReader::Load("name", mapLoadFunctionPtrs);

return 0;
}


Some points came up.

I had to use static member functions to avoid ObjFileReader knowing about Mesh, which lead to some contortions to get it to work (i.e. the Mesh class has a static pointer which points to the mesh to load the data into).

All the add functions pass a vector of strings; which makes sense since the data file contains strings, and allows Mesh to convert the data to the appropriate type. However, the addVertexIndices function adds a new face before adding the indices to it (which is reasonable), but the addTextureIndices and addNormalIndices add the indices to the last Face, which makes assumptions about how the code works (not a good thing).

The way the code sets up the function pointers is messy and relies on the methods being public... but then, I suppose they should be.

Setting up the pointers would be best delegated to a third class. While coming up with the aforesaid code, my investigations threw up the Builder and Intermediary design patterns as likely candidates.

As I've said, this is a learning exercise and it's been rather fun to push the limits to see what can be done and how. Frequently, I find C++ quite frustrating when some particular feature is not readily implemented and it feels as though horrific contortions must be endured to get the code to do what it should.

I suppose the next step will be to implement this all more elegantly.

Share this post


Link to post
Share on other sites
Gage64    1235
I've only skimmed through the other replies, so maybe this has already been said:

You have a Mesh class, and a Reader class. You don't want Mesh to know about Reader because you might want to create derived Readers (for other file formats) and you don't want to change Mesh every time you do. You may also want to derive different Mesh implementations (for different APIs, different rendering strategies, etc.), or you might want to change the Mesh's internal representation, and you don't want to update Reader every time you make such a change.

One solution I can think of is to introduce an intermediate class that stores 3D data in a format you'll decide on. Reader (or any derived class) will know how to translate it's data to this format, and Mesh (or any derived class) will know how to convert this format to it's internal representation. This way, when new readers are added, the Mesh class isn't modified and visa versa. The two classes know nothing about each other and are completely decoupled.

Share this post


Link to post
Share on other sites

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

Sign in to follow this