Decoupling classes

Started by
13 comments, last by Gage64 16 years, 8 months ago
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.
Constipation is the thief of time.Diaorrhea waits for no man.
Advertisement
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(...);	}}

deathkrushPS3/Xbox360 Graphics Programmer, Mass Media.Completed Projects: Stuntman Ignition (PS3), Saints Row 2 (PS3), Darksiders(PS3, 360)
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]
my_life:          nop          jmp my_life
[ Keep track of your TDD cycle using "The Death Star" ] [ Verge Video Editor Support Forums ] [ Principles of Verg-o-nomics ] [ "t00t-orials" ]
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]
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]
Constipation is the thief of time.Diaorrhea waits for no man.
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++.
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]
Constipation is the thief of time.Diaorrhea waits for no man.
Quote:Original post by Amnesiac5
No, not at all. I was referring to function pointer and functor syntax! Help!!! [wow]


Pointers to member functions, and functors. (Except that functors are referred to there as 'functionoids' instead.
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).
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....

This topic is closed to new replies.

Advertisement