TO make your code loosely coupled here are a few simple things to remember:
Part 1:
Technically known as "Separation of Concern". Each class has a specific role, it should be handling business logic or application logic. Try and steer clear of class that combine both responsibilities. i.e. A class that manages (broad term) data is application logic while a class that uses data is business logic.
Personally I refer to this (in my own little world) as create it or use it
. A class should create an object or use an object it should never do both.
Part 2:
How to implement separation of concern.
As a starting point there are two simple techniques:
Note: Design patterns are not absolute.
They are supposed to be customized to the situation but have an underlying theme that is similar to all applications. So don't look at the examples below and say that I must follow this rigidly; these are just examples (and slightly contrived at that).
Dependency Injection:
This is where you pass in an object that a class uses. The object that you pass in based on an interface so your class knows what to do with it but does not need to know the actual implementation.
class Tokenizer
{
public:
Tokenizer(std::istream& s)
: stream(s)
{}
std::string nextToken() { std::string token; stream >> token;return token;}
private:
std::istream& stream;
};
Here we inject the stream into a Tokenizer. The tokenizer does not know what type the stream is as long as it implements the interface of std::istream.
Service Locator Pattern:
The service locator pattern is a slight variation on dependency injection. Rather than give an object that it can use, you pass it an object that knows how to locate(create) the object you wants to use.
class Application
{
public:
Application(Persister& p)
: persistor(p)
{}
void save()
{
std::auto_ptr<SaveDialog> saveDialog = persistor.getSaveDialog();
saveDialog.DoSaveAction();
}
void load()
{
std::auto_ptr<LoadDialog> loadDialog = persistor.getLoadDialog();
loadDialog.DoLoadAction();
}
private:
Persister& persistor;
};
Here we pass the application object a persistor object. When you perform a save/load action it uses the persistor to create an object that actually knows how to do the action. Note: Again the persistor is an interface and you can provide different implementations depending on the situation.
This is useful when a potentially
unique object is required each time you instantiate an action.
Personally I find this is particularly useful in writing unit tests.
Note of Patterns:
Design patterns is a huge subject unto itself. This is by no means an exclusive list of patterns you can use to help with loose coupling; this is just a common starting point.
With experience you will realize you are already using these patterns its just that you did not use their formal names. By standardizing their names (and getting everybody to learn them) we find that it is easy and quicker to communicate ideas.