public data member

Started by
6 comments, last by HappyCoder 12 years, 5 months ago
I read everywhere not to use public data members, but reading about game development there is some places where public data members are used..
I'm reading david eberlys 3d game engine architecture atm where he says he uses public in his scenegraph node as there is no side effects to changing the data..

I see his point, but whats your opinion on this? Do you use it or not? I already use public data for my math lib, but not sure if I want to use it in my scenegraph..
Advertisement
I don't expose member variables in classes. All of my classes' member variables are private or protected. The reason is that there are times when I wish to change the internal implementation of the class. Maybe I still want to be able to add a child to a class (for example) through the add(ChildType child) method in the class. But maybe I was storing the children in the class in a std::vector<ChildType> object. Maybe I want to change it to a std::hash_set instead of an std::vector. If I've publicly exposed and used the std::vector object, now I have to go through my code and find everywhere I used it and update all those places.

On the other hand, if I just make sure the thing is private and use public methods to manipulate the data, I don't have to go all throughout my code base updating things. I just need to update the one class, and everything still works fine.
[size=2][ I was ninja'd 71 times before I stopped counting a long time ago ] [ f.k.a. MikeTacular ] [ My Blog ] [ SWFer: Gaplessly looped MP3s in your Flash games ]
I would probably never expose any container either, but eg. like in the case of my Light class I have many members that there is no side effects to changing them.. its obvious what they do, they probably wont change and I would have to provide setters/getters for them which give full access to them anyway..

There's more too it than this, but it shows what I mean..


class Light
{
public:

enum
{
LIGHT_AMBIENT,
LIGHT_DIRECTIONAL,
LIGHT_POINT,
LIGHT_SPOT,
LIGHT_COUNT
};

Light(int lightType = LIGHT_AMBIENT) : m_lightType(lightType)
{

}



int m_lightType;
ColorRGBA m_ambient;
ColorRGBA m_diffuse;
ColorRGBA m_specular;
Real m_intensity;
Real m_constant;
Real m_linear;
Real m_quadric;
bool m_attenuate;
bool m_on;

Math::Vector3D<Real> m_position;
Math::Vector3D<Real> m_direction;
Math::Vector3D<Real> m_up;
Math::Vector3D<Real> m_right;

//Spotlight parameters
Real m_exponent;
Real m_angle;
};
You should think in terms of invariants.

Suppose you have some related data that should be considered as a unit. Let's take a simplified car as an example. The car has a handful of attributes we care about: color, year of manufacture, automatic or manual transmission, and presence (or lack) of an air conditioner.

struct Car
{
ColorEnum Color;
unsigned ManufactureYear;
bool AutomaticTransmission;
bool AirConditioner;
};


Take some arbitrary instance of Car. Change its color. Does this affect its year of manufacture? Transmission type? A/C system? No! We can change the color independently of all other descriptions of the car. Ditto for every stat in the list. All of the data elements are orthogonal and there is no invariant maintained here.


Contrast this with a UnitVector, which is a 3-component vector that is expected to always be normalized:

struct UnitVector
{
float X;
float Y;
float Z;
};


Suppose we have an instance of UnitVector that has X, Y, Z values of (1.0, 0, 0) respectively. Now some code decides that the vector should point at a 45 degree angle, so it changes the components to (1.0, 1.0, 0.0).

Why is this bad? Because we have, through public data, violated the invariant of the object. It is no longer normalized, and any code that expects a UnitVector to have a length of 1.0 will be in for a nasty shock.


This principle can be extended to all code design. Either your data involves an invariant, or it does not. If there are invariants to deal with, you should generally not expose data directly. Always provide an interface that can maintain those invariants. If there are no invariants, you should generally not write accessors or mutators to read/write that data; those just subject your code to overhead that is not necessary.

The argument about changing code in the future falls on its face here. If you have an object which suddenly needs to change in its implementation details, there are two possibilities: either the object has invariants, or it doesn't. If there are invariants involved, you should have hidden them behind an interface to begin with, and changing the code's implementation detail will be properly contained. If there are not, then changing the implementation detail should affect precisely nothing else. (Hint: the latter case is extremely rare, and chances are, if you're changing implementation details, you're also dealing with invariants, whether you are aware of them or not.)

So modifying implementation detail in the presence of public data members is a red herring. If you're doing your job, this will basically never happen. Your design should take into account from day one whether or not the objects in question have invariants to maintain, and if they do, you should be hiding them behind an interface that upholds that contract. If not, you should never have cause to worry about implementation detail, because there isn't any! You just have public data that is purely orthogonal to everything else. Consider again the car example from earlier: what implementation detail could you possibly change? Maybe a data type at the most, but if you have to change one of the data types mid-stream, you screwed up your design (in the process of requirements gathering) to begin with. In any case, if you change the data type of a component of an object, wrapping it in accessors won't save you from having to update all the code that touches that data.


Observant readers will note that much of the time you either have POD objects or contractual objects, i.e. objects that must uphold invariants. A pure POD object has no need for trivial accessors, and a contractual object should never have public members that are related to the class invariants. There are cases where a single object might have some orthogonal members and some contractually obligated members, but those are a minority, and in many cases that situation indicates that you have a single class trying to do too much.


My convention in C++ has long been that POD types should be structs and everything else should be a class, and a class with a public data member is a serious red flag, simply because the presence of nontrivial member functions suggests the existence of a contract, and public data members are the very antithesis of encapsulation in the presence of invariants.

(Obviously in other languages like C#, struct-vs-class means something very different and should therefore be treated accordingly.)

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Thanks for a great answer!

One thing I didn't get clearly.. if there is invariants in a class I should also hide the non invariants behind accessors? I did get the point that generaly I would have either invariant data or non-invariant data in a class/struct though, but I'm sure theres times where I will have some kind of a mix too..

I'm programming in C++.. should have said that in the first post :)
Like I said:

There are cases where a single object might have some orthogonal members and some contractually obligated members, but those are a minority, and in many cases that situation indicates that you have a single class trying to do too much.

Wielder of the Sacred Wands
[Work - ArenaNet] [Epoch Language] [Scribblings]

Ah right.. sorry.. thanks again.
I don't think a scene node justifies public data members. I actually did that on a javascript project I did recently. At first things worked fine, then I made a subclass of a node that was controlled using a physics engine. Now every time I moved the position of the object I also had to move the position of the physics body attached to it. So at this point I changed everything to use getters and setters. It would have been easier to just do getters and setters to begin with so I would recommend not making the data members public.
My current game project Platform RPG

This topic is closed to new replies.

Advertisement