Object Oriented Design

Published May 12, 2005
Advertisement
Previously I discussed why your design was rotting. The primary participant in this rot is a lack of dependency management. In other words, modules become much more dependent upon each other due to the various code changes. This introduces rigidity into the system, preventing changes. It also makes the system much more fragile, as any change typically results in a cascade of erroneous behavior. Object Oriented Design has many principles and patterns for helping to manage and eliminate dependencies in software design and development.

The Open Closed Principle


'A class should be open for extension, but closed for modification.' - Bertrand Meyer

The idea is simple, instead of changing a class to add a new feature, you simply extend the class (via inheritance or parameterization). You can then change the behavior in the new subclass to introduce the behavior you desire without altering existing, working code.

Take the following code, for example:
struct Protocol {	enum ProtocolType { IP, IPX, UDP } type;};struct IpProtocol {	Protocol::ProtocolType type;	//IP Protocol stuff};struct IpxProtocol {	Protocol::ProtocolType type;	//IPX Protocol stuff};struct UdpProtocol {	Protocol::ProtocolType type;	//UDP Protocol stuff};void Connect(Protocol& p, std::string connectionString) {	switch(p.type) {		case Protocol::IP:			IpConnect((IpProtocol&)p, connectionString);			break;		case Protocol::IPX:			IpxConnect((IpxProtocol&)p, connectionString);			break;		case Protocol::UDP:			UdpConnect((UdpProtocol&)p, connectionString);			break;	}}

Now, looking over this code should, hopefully, give you a very bad taste in your mouth. Of course, this wouldn't be the only switch/if-else if-else combination in the application. Most likely it would be littered with many such examples as this one. Every time a new protocol wishes to be supported, these conditional statements would have to be updated. This code clearly violates OCP, as any new feature must alter many different pieces of code to add support for it.

OCP can be achieved through abstraction. Applying the appropriate abstractions to your code will enable it to easily be extended to add new functionality (or alter existing functionality) without requiring exiting code to change. Below you can see a simple diagram demonstrating the use of abstraction to make the code conform to the Open Closed Principle.

Now we can add new protocols by simply implementing the Protocol interface, or sub-classing one of the existing protocols. The Connect function, and any other functions dependent upon the Protocol interface, will not need to change to accommodate the new protocols as the interface abstracts the concrete implementation from the design.

Implementing this in code we end up with:
class Protocol {public:	virtual void Connect(std::string const& connectionString) = 0;	virtual void Disconnect() = 0;	virtual void Send(std::vector<char> const& data) = 0;	virtual void Receive(std::vector<char>& data) = 0;	virtual ~Protocol() = 0 {}};class IpProtocol : public Protocol {public:	virtual void Connect(std::string const&);	virtual void Disconnect();	virtual void Send(std::vector<char> const&);	virtual void Receive(std::vector<char>&);	~IpProtocol();};class IpxProtocol : public Protocol {public:	virtual void Connect(std::string const&);	virtual void Disconnect();	virtual void Send(std::vector<char> const&);	virtual void Receive(std::vector<char>&);	~IpxProtocol();};class UdpProtocol : public Protocol {public:	virtual void Connect(std::string const&);	virtual void Disconnect();	virtual void Send(std::vector<char> const&);	virtual void Receive(std::vector<char>&);	~UdpProtocol();};void Connect(Protocol& p, std::string const& connectionString) {	p.Connect(connectionString);}

Looking over this, you are probably thinking to yourself: 'Damn, this is more code that the other way though.' However, perhaps you should implement the other code so that it provides the Disconnect, Send, and Receive functions first. At which point you will see that not only is it more code than we have currently, but it also requires a great deal more changes just to implement a single extra protocol (take SPX for instance).
Next Entry Sorry
0 likes 6 comments

Comments

choffstein
Very well said.
May 12, 2005 08:07 PM
Rebooted
Indeed. I like your journal [grin].

By the way, in the situations where a singleton pattern is appropriate (i'm thinking about using one for my global logger class and global settings class), does a singleton provide any advantage over a class with static public member functions? Member data is still encapsulated within and only accessable from the class, but some of the overhead and complexity of the singleton in removed.

What do you think?
May 13, 2005 12:20 PM
Saruman
The singleton is actually what removes the complexity and overhead compared to throwing around statics. Mistakes/bugs with singletons are usually caught compile-time rather than statics which are normally run-time errors.

Although like Washu has said you need to analyze whether the problem space fits the pattern, as the singleton is based on:

Global access
A single instance

If both of those aren't applicable than don't use a singleton.
May 17, 2005 10:46 AM
HopeDagger
Quite a clean and attractive design. I'm looking forward to what contraption of code you'll hand out next! :)
May 20, 2005 11:14 AM
Zahlman
"The singleton is actually what removes the complexity and overhead compared to throwing around statics. Mistakes/bugs with singletons are usually caught compile-time rather than statics which are normally run-time errors."

I'm from Missouri(TM).

Of course you should not throw static *data* around, but static *functions* should be just fine - and anyway, they don't get "thrown around"; you just write Namespace::doStaticThing(), and it somehow interacts with staticData which is not directly available to the rest of the code (you don't need the 'private' keyword provided by a class to do this; you can hide things by careful use of the C++ linker, too.)

If anything, I would say the singleton approach is more prone to runtime problems (consider: someone caches a reference to the instance, then someone else destroys and recreates it), and yes it will catch things at compile-time, but it's also creating extra opportunities to mess up at compile-time ("oh, damn, I need to get the instance here, too...")
May 22, 2005 05:57 PM
paulecoyote
cool read, may be this kind of thing could be made in to a gdnet article to educate the masses.

I'm current reading "Agile Software Development" by Robert C Martin, it covers this stuff and more. Don't agree with everything it says, but the whole open closed principle is covered in depth.
May 27, 2005 03:49 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement