So, I've come back to ask, once more, a patterns-related question. This may be too generic to answer, but my problem is this (I am programming and applying concepts that I learn as I go along):
I have several structures within structures (note, I'm using the word structure in the general sense, not in the strict C struct sense (whoa, what a tongue twister)), and quite a bit of complicated inter-communications going on. Using the example of one of my earlier questions, I have Unit objects, UnitStatistics objects, General objects, Army objects, Soldier objects, Battle objects, and the list goes on, some organized in a tree structure.
After researching a little bit and asking around, I decided to use the mediator pattern because the interdependencies were becoming a trifle too much, and the classes were starting to appear too tightly coupled (yes, another term which I just learned and am too happy about not to use it somewhere). The pattern makes perfect sense and it should straighten some of the chaotic spaghetti that I currently have boiling in my project pot.
But well, I guess I haven't learned yet enough about OO design. My question is this (finally. PS, I hope it makes sense): should I have one central mediator that deals with all communications within the program, and is it even possible? Or should I have, say, an abstract mediator and one subclassed mediator per structure type that deals with communication of a particular set of classes, e.g. a concrete mediator per army which helps out the army, its general, its units, etc.
I'm leaning more towards the second option, but I really am no expert when it comes to OO design. So third question is, what should I read to learn more about this kind of subject (I've looked at Head First's Design Patterns and the GoF book, but they're more of a "learn the vocabulary" kind of book than a "learn how to use your vocabulary" kind of book, which is what I need in this case.
As always, thanks for any and all help (including the witty comments).
I don't think you've provided enough info above to be able to make an informed decision as to which is best.
From looking at your other questions it seems that most of the communication occurs between components within an Army. You don't mention much occurring between one Army and another. In which case it would seem to make sense to have each Mediator instance coordinate communication between the components comprising a single Army - i.e. the Generals, Soldiers etc. So if you have 10 Army's then you will have 10 ArmyMediator's.
If you really want to learn O-O Design you're going to have to try things out and run the risk of getting it wrong from time to time. I think you'll learn just as much, if not more, from having to refactor a design that doesn't quite model the problem correctly into one that does, as you will from getting the design right the first time around.
Often you just won't have enough information up front to be able to choose the right design from the go anyway. Just choose the simplest one that works for now, and improve it later when you have a better idea of the requirements and/or the shortcomings of the current design.
Regarding books, personally I think the GoF book is more useful if you focus less on the specific set of patterns they describe, and focus more on the overall approach of breaking classes down into smaller reusable components, each of which typically encapsulates a single unit of functionality.
I can't answer your question directly, because I have never used that design pattern. However, whenever I have this problem, of message passing between various objects, I use the signal-slot pattern. Usually I use Qt's, but my second option is Boost's. They both solve the problem by having a single, global message passing handler. They are also both type-safe are quite efficient, both in terms of cpu-cycles and in productivity. Because they are so flexible, i.e. any object and emit any kind of signal, and any other object can receive any signal, you'll end up solving, I think, what you describe.
Sorry if I just made things worse by not choosing any of the 2 option, but instead adding a 3rd!
In order to use Mediator you need to determine:
(1) What does the group of objects, which need mediation, consist of?
(2) Among these, which are the ones that have a common interface?
The Mediator design pattern relies on the group of objects that are to be mediated to have a "common interface"; i.e., same base class: the widgets in the GoF book example inherit from same Widget base, etc.
So, for your application:
(1) Which are the structures (Soldier, General, Army, Unit, etc.) that need mediation between each other?
(2) Which ones of those (Soldier, General, Army, Unit, etc.) have a common base?
This should help you determine, as a first step, an outline of the participants in the Mediator design pattern. You may find out that some structures in (1) fall outside of (2). Then, yo may need to force them adhering to a common interface, too, if you can change that or if you can afford to make that change... (may turn out to be too much redesigning work and it violates the Open-Closed principle: your design should be, as much as possible, open to adding new features but closed to modifying existent ones).
If you discover that (1) and (2) above result in a partition of separate groups, each with its own mediator, then the number of these partitions dictate the number of different types of mediators. Now, should these different mediators have a common interface of their own? Maybe, maybe not. Polymorphism is a way of handling complexity by grouping different entities under a common interface such that they can be handled as a group rather then individually. So, would there be any benefit to group all these supposedly different types of mediators under a common interface (like the DialogDirector in the GoF book example)? Possibly, if:
(a) You may have to use a heterogeneous collection of mediators;
or
(b) You envision in the future that these mediators will evolve (and they probably will). Hence providing an abstract interface allows you to derive more evolved versions of mediators without affecting existent ones or their colleagues (the clients of the mediators).
So, without knowing more, I'd have to guess that, yes, it's probably better to use abstract mediators and to subclass them, for each group partition, just to prepare yourself for future changes without having to redesign your mediators (remember the Open-Closed principle).
Hope this helps.
Related
I am doing my own research project, and I am quite struggling regarding the right choice of architectural/design patterns.
In this project, after the "system" start, I need to do something in background (tasks, processing, display data and so on) and at the same be able to interact with the system using, for example, keyboard and send some commands, like "give me status of this particular object" or "what is the data in this object".
So my question is - what software architectural/design patterns can be applied to this particular project? How the interraction between classes/objects should be organized? How should the objects be created?
Can, for example, "event-driven architecture" or "Microkernel" be applied here? Some references to useful resources will be very much appreciated!
Thank you very much in advance!
Careful with design patterns. If you sprinkle them throughout your code hoping that everything will work great, you'll soon have an unreadable, boilerplate full mess. They are recipes, not solutions.
My advice to you is pick a piece of paper and a pencil and start drawing all the entities of your domain, with all their requisites, and see how they relate. If you want to get somewhat serious about it, you can do something like this.
When defining your entities, strive for high cohesion and loose coupling.
High cohesion means that you should keep similar functionalities together. In a very simple example, if you have a class that reads stuff from a file and processes it, the class has low cohesion, since reading and processing are two very distinct functionalities. In this case, you would want a class for each functionality.
As for loose coupling, it means that your entities should be independent of each other. Using the example above, supposed that you are now the proud owner of two highly cohesive classes - one that reads stuff from a file (Reader), and one that processes that stuff (Processor). Now, suppose that the Processor class has an instance of the Reader class, and calls it in order to get its input. In this case, we can say that both classes are tightly coupled, since Processor won't work without Reader. In the OOP world, the solution for this is typically the use of interfaces. You can find a neat example here.
After defining an initial model of your domain and gathering as much knowledge about it as you can, you can now start to think about the implementation's architecture. This is were you can start thinking about the architectural patterns. Event driven architecture, clean architecture, MVP, MVVM... It will all depend on your domain. It is your job to know which pattern will fit best. Spoiler alert: this can be extremely hard to do correctly even for experienced engineers, so don't be afraid to fail.
Finally, leave the design patterns for the implementation stage. Their use completely depends on your implementation problems and decisions. Also, DON'T FORCE THEM. Ideally, you will solve a problem and, IF APPLICABLE, you'll see a pattern emerging. Trust me, the last thing you want is to have a case of design patternitis. Anyway, if you need literature on patterns, I totally recommend this book. It's great no matter your level as an engineer.
Further reading:
SOLID principles
Onion Architecture
Clean architecture
Good luck!
You have a background task, and it can be used for a message pump/event queue indeed. Then your foreground task would send requests to this background thread and asynchronously wait for the result.
Have a look at the book "Patterns for Parallel Programming".
It is much better if you check a book for Design Patterns. I really like this one.
For example, if you need to get some data from a particular object, you may need the Observer Pattern to work for you and as soon as the object has the data, you (or another object) get to know this data and can work with it, with another pattern (strategy might work, it really depends on what you have to do).
If you have to do some things at the same time, check also the Singleton pattern (well, check the most important ones!).
I've got lots of problems with project i am currently working on. The project is more than 10 years old and it was based on one of those commercial C++ frameworks which were very populary in the 90's. The problem is with statecharts. The framework provides quite common implementation of state pattern. Each state is a separate class, with action on entry, action in state etc. There is a switch which sets current state according to received events.
Devil is hidden in details. That project is enormous. It's something about 2000 KLOC. There is definitely too much statecharts (i've seen "for" loops implemented using statecharts). What's more ... framework allows to embed statechart in another statechart so there are many statecherts with seven or even more levels of nesting. Because statecharts run in different threads, and it's possible to send events between statecharts we have lots of synchronization problems (and big mess in interfaces).
I must admit that scale of this problem is overwhelming and I don't know how to touch it. My first idea was to remove as much code as I can from statecharts and put it into separate classes. Then delegate these classes from statechart to do a job. But in result we will have many separate functions, which logically don't have any specific functionality and any change in statechart architecture will need also a change of that classes and functions.
So I asking for help:
Do you know any books/articles/magic artefacts which can help me to fix this ? I would like to at least separate as much code as I can from statechart without introducing any hidden dependencies and keep separated code maintainable, testable and reusable.
If you have any suggestion how to handle this, please let me know.
The statechart pattern is intended to be used specifically to remove switch statements, so this sounds like a horrid abuse. Additionally, states should only change on asynchronous events. If you are processing an event and you change through multiple states (or for loop, etc.), then this is also a horrid abuse of the pattern.
I would start from these two points, as they will solve much of your concurrency issues just fixing them up. What you need to determine is:
What are your external, asynchronous events to the system? These are the only things that should be determining state transitions, not things that happen during event processing. An event may cause 0 or 1 state transitions. Once you have a list of these state transitions, you can reconstruct the actual states of your system. If you are aware of UML State diagrams, this would be a perfect time to sketch one up in a charting program, not just for yourself (though it will help you immensely), but also for everyone in the future that has to return to the project. As you have learned, this happens.
Now that you know what are really states, list what are states in the code that shouldn't be. This usually indicates that something can be "functionally decomposed". Instead of a state object for each of these, likely all that is needed is a separate function. This will cut down on a lot of the overhead of state objects and should clean up the code immensely.
Now it's time to tackle those horrendous switch statements you mentioned. If they were truly based on state, you shouldn't need one at all. Instead, you should be able to call the state machine directly.
Something like:
myStateMachine->myEvent();
and it should work without any switch. But notice, this may be the case even for some of those objects that don't work across asynchronous events. This is also an indication of where you may just use inheritance to get the same effect. If you have:
switch (someTypeIdentifier)
{
case type1:
doSomething();
break;
case type2:
doSomethingElse();
break;
}
usually the correct OOP method to do is to create two actual types Type1, Type2, both derived from an abstract base TypeBase, with a virtual method doSomething() that does what you need. The reason this is useful is because it means you can "close" the handling (in the meaning of the Open/Closed Principle), and still extend the functionality by adding new derived types as needed (leaving it open to extension). This saves bugs like crazy because it gets developers hands out of those switch statements, which can get quite ugly and convoluted, instead encapsulating each separate behavior in separate classes.
4 - Now look to fix up your thread issues. Identify all objects used from multiple threads. Make a list. Now, how are these used? Are some of them always used together? Start making groups. The goal here is to find the level of encapsulation that best works for these objects, separate the objects into individual classes that control their own synchronisation, figure out the atomic level of actual "transactions" for the objects, and make methods of the classes that expose those meaningful transactions, wrapped behind the scenes with the appropriate mutexes, condition variables, etc.
You might be saying "that sounds like a lot of work! Why do all that instead of just writing it all over myself?" Good question! :) The reason is actually straightforward: if you are going to do it all by yourself, those are the steps you should be doing anyway. You should be identifying your states, your dynamic polymorphism, and getting a handle on the multithreaded transactions. But, if you start with the existing code, you also have all of those unspoken business rules that were never documented and may cause all sorts of unexpected bugs down the line. You don't have to bring everything over - if you suspect it's a bug, discuss the logic with the people who have worked with the system in the past (if available), QA, or whoever might identify bugs, and see if it really should be carried over. But you need to actually evaluate what the bugs are either way, or you may not code something that actually needed coding.
In the end, this is a manual process that is a part of software engineering. There are CASE tools that can help draw up the state diagrams and even publish them to code, there are refactoring tools, like those found in many IDEs, that can help move code between functions and classes, and similar tools which can help identify threading needs. However, those things shouldn't be picked up for a single project. They need to be learned throughout your career, picking them up and learning them more deeply over years of work, as they are a part of being a software engineer. They don't do it for you. You still need to know the whys and hows, and they just help get it done more efficiently.
Statecharts (including nested Statecharts) are a powerful way to specify, understand and even simulate/validate complex control flow. But to gain the benefit, you need the statechart model in a suitable tool (I used Statemate way back in the day, not sure if it's still available), plus a reliable mapping from the chart to the code (Statemate used to generate the code) - then you can forget about the state management code (mostly)! In your situation, if you don't have the model, I would try to reverse one from the code - as Ira says, chances are high that the original developers had a model in some form, and you may find the code making a lot of sense as the model emerges. If this works out, you will have a really good spec/model of the code which should make future code edits much easier (even if you don't want to go to automatic code generation, and maintain the code/model mapping manually (but you'll need to be meticulous!!))
Sounds to me like your best bet is (gulp!) likely to start from scratch if it's as horrifically broken as you make out. Is there any documentation? Could you begin to build some saner software based on the docs?
If a complete re-write isn't an option (and they never are in my experience) I'd try some of the following:
If you don't already have it, draw an architectural picture of the whole system. Sketch out how all the bits are supposed to work together and that will help you break the system down into potentially manageable / testable parts.
Do you have any kind of requirements or testing plan in place? If not, can you write one and start to put unit tests in place for the various chunks of code / functionality which exist already? If you can do that, you can start to refactor things without breaking as much of whatever does currently work.
Once you've broken things down a bit, start building your unit tests into integration tests which pull together more of the functionality.
I've not read them myself, but I've heard good things about these books which may have some advice you can use:
Refactoring: Improving the Design of Existing Code (Object Technology Series).
Working Effectively with Legacy Code (Robert C. Martin)
Good luck! :-)
I'm comming from an scientific background and I'm having trouble to think object oriented. I'm always thinking of functions not objects.
For example I've got a Dataset containing multiple 2D arrays and I like to extract Regions of Interest out of these arrays. So my first Design was something like a RoIFinder class to whom I pass a reference to the Dataset object. The RoIFinder object did its magic and returned the RoI's.
But this gives me a bad feeling since it looks more like a function than an object.It's more like a Blackbox. But I have no Idea how it is done properly.
How would you do something like that?
OO is not a silver bullet. Do your work in whichever way it seems to be correct from the different points of view: problem decomposition, efficiency, code simplicity, testing, etc.
Do not make the code look in OO way if you don't need to. The OO is about simplification the life when the problem is too complex, not for making simple problems solved in a complicated way.
Specifically for your problem, I see nothing bad in your approach. It possibly doesn't use some advanced techniques, so what?
To me it sounds like in the specific situation you describe, this could be a fine OO design.
OO in brief is about bundling together
state
behaviour
(identity).
Whenever you have data which represents the state of (a part of) the system, and you have behaviour tied to (typically manipulating) that data, you have a candidate for an object. Optionally, these objects may also have an identity, but this may not be always necessary.
If you potentially have multiple different criteria to pick regions of interests out of the Dataset, you may implement these as distinct *Finder classes, implementing a common base interface. There you have an OO class hierarchy! From then on, the Finders can even be used as interchangeable Strategies in your code.
The alternative would be to put the finder functionality in the Dataset. This might be OK if you are absolutely sure you won't have more different criteria to extract regions. Even then, your Dataset has two distinct responsibilities, which is usually not a good idea. It is better to have each class be responsible for one thing, and do it well.
We don't know what you are supposed to do with the data in the arrays - there may be some possibility to find more abstractions there and build some OO types and objects on these too.
Note though, that these all are just possibilities. Implement them only if they are actually useful (for solving problems, simplifying your code, or - last but not least - helping you gain practical experience with new concepts).
What you've done is probably better than the obvious OO approach of adding a find_roi() to the Dataset class itself. Why? Because it sounds like you've created the RoIFinder functionality based only on the public API of Dataset. Keeping Dataset simpler is also good. The STL (well these days it's just part of the Standard library) has examples of this in the way algorithms such as sort are applicable to multiple containers, rather than each container have a sort member function (although in the case of list optimisation opportunities leads it to implementing its own version). The STL also has std::string which controvertially embeds a lot of functionality that could have been factored out - in my opinion it is well designed, prioritorising convenient and elegant usage which is important in such a ubiquitous class which is so frequently and heavily operated on. So, pick what suits the situation. Anyway, there's no reason to put the RoIFinder into a class if it could equally be just a function, but if you've found some state (i.e. data members) that are convenient to preserve, or it helps usability in some other way, then that's a good enough reason to stick with your object.
Your 2D array can be implemented as a matrix class. One object
A region of interest is another class.
Getting a region of interest out of a matrix is a method.
"Iterators" into your matrices are classes.
Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 4 years ago.
Improve this question
I am a c++ programmer, and I am looking forward to learning and mastering OO design.I have done a lot of search and as we all know there is loads of material, books, tutorials etc on how to achieve a good OO design. Of course, I do understand a good design is something can only come up with loads of experience, individual talent, brilliance or in fact even by mere luck(exaggeration!).
But sure it all starts off with a solid beginning & building some strong basics.Can someone help me out by pointing out the right material on how to start off this quest of learning designing right from the stage of identifying objects, classes etc to the stage of using design patterns.
Having said that I am a programmer but I have not had a experience in designing.Can you please help me take someone help me out in this transition from a programmer to a designer?
Any tips,suggestions,advice will be helpful.
[Edit]Thanks for the links and answers, I need to get myself in to that :) As i mentioned before I am a C++ programmer and I do understand the OO basic concepts as such, like inheritance, abstraction, polymorphism, and having written code in C++ do understand a few of the design patterns as well.what i dont understand is the basic thought process with which one should approach a requirement. The nitty grittys of how to appraoch and decide on what classes should be made, and how to define or conclude on relationships they should have amongst themselves.Knowing the concepts(t some extent) but not knowing how to apply them is the problem i seem to have :( Any suggestions about that?
(very) Simple, but not simplist design (simple enough design if you prefer) : K.I.S.S.
Prefer flat hierarchies, avoid deep hierarchies.
Separation of concerns is essential.
Consider other "paradigms" than OO when it don't seem simple or elegant enough.
More generally : D.R.Y. and Y.A.G.N.I help you achieve 1.
There is no secret. It's sweat, not magic.
It's not about doing one thing right. It's balancing many things that must not go wrong. Sometimes, they work in sync, sometimes, they work against each other. Design is only one group of these aspects. The best design doesn't help if the project fails (e.g. because it never ships).
The first rule I'd put forward is:
1. There are no absolutes
Follows directly from the "many things to balance. D.R.Y., Y.A.G.N.I. etc. are guidelines, strictly following them cannot guarantee good design, if followed by the letter they may make your project fail.
Example: D.R.Y. One of the most fundamental principles, yet studies show that complexity of small code snippets increases by a factor of 3 or more when they get isolated, due to pre/post condition checking, error handling, and generalization to multiple related cases. So the principle needs to be weakened to "D.R.Y. (at least, not to much)" - when to and when not is the hard part.
The second rule is not a very common one:
2. An interface must be simpler than the implementation
Sounds to trivial to be catchy. Yet, there's much to say about it:
The premise of OO was to manage program sizes that could not be managed with structured programming anymore. The primary mechanism is to encapsulate complexity: we can hide complexity behind a simpler interface, and then forget about that complexity.
Interface complexity involves the documentation, error handling specifications, performance guarantees (or their absence), etc. This means that e.g. reducing the interface declaration by introducing special cases isn't a reduction in complexity - just a shuffle.
3-N Here's where I put most of the other mentions, that have been explained already very well.
Separation of Concerns, K.I.S.S, SOLID principle, D.R.Y., roughly in that order.
How to build software according to these guidelines?
Above guidelines help evaluating a piece of code. Unfortunately, there's no recipe how to get there. "Experienced" means that you have a good feel for how to structure your software, and some decisions just feel bad. Maybe all the principles are just rationnalizaitons after the fact.
The general path is to break down a system into responsibilities, until the individual pieces are managable.
There are formal processes for that, but these just work around the fact that what makes a good, isolated component is a subjective decision. But in the end, that's what we get paid for.
If you have a rough idea of the whole system, it isn't wrong to start with one of these pieces as a seed, and grow them into a "core". Top-down and bottom-up aren't antipodes.
Practice, practice, practice. Build a small program, make it run, change requirements, get it to run again. The "changing requirements" part you don't need to train a lot, we have customers for that.
Post-Project reviews - try to get used to them even for your personal projects. After it's sealed, done, evaluate what was good, what was bad. Consider the source code was thrown away - i.e. don't see that sessison as "what should be fixed?"
Conway's Law says that "A system reflects the structure of the organizaiton that built it." That applies to most complex software I've seen, and formal studies seem to confirm that. We can derive a few bits of information from that:
If structure is important, so are the people you work with.
Or Maybe structure isn't that important. There is not one right structure (just many wrong ones to avoid)
I'm going to quote Marcus Baker talking about how to achieve good OO design in a forum post here: http://www.sitepoint.com/forums/showpost.php?p=4671598&postcount=24
1) Take one thread of a use case.
2) Implement it any old how.
3) Take another thread.
4) Implement it any old how.
5) Look for commonality.
6) Factor the code so that commonality is collected into functions. Aim for clarity of code. No globals, pass everything.
7) Any block of code that is unclear, group into a function as well.
8) Implement another thread any old how, but use your existing functions if they are instant drop-ins.
9) Once working, factor again to remove duplication. By now you may find you are passing similar lumps of stuff around from function to function. To remove duplication, move these into objects.
10) Implement another thread once your code is perfect.
11) Refactor to avoid duplication until bored.
Now the OO bit...
12) By now some candidate higher roles should be emerging. Group those functions into roles by class.
13) Refactor again with the aim of clarity. No class bigger than a couple of pages of code, no method longer than 5 lines. No inheritance unless the variation is just a few lines of code.
From this point on you can cycle for a bit...
14) Implement a thread of use case any old how.
15) Refactor as above. Refactoring includes renaming objects and classes as their meanings evolve.
16) Repeat until bored.
Now the patterns stuff!
Once you have a couple of dozen classes and quite a bit of functionality up and running, you may notice some classes have very similar code, but no obvious split (no, don't use inheritance). At this point consult the patterns books for options on removing the duplication. Hint: you probably want "Strategy".
The following repeats...
17) Implement another thread of a use case any old how.
18) Refactor methods down to five lines or less, classes down to 2 pages or less (preferably a lot less), look for patterns to remove higher level duplication if it really makes the code cleaner.
19) Repeat until your top level constructors either have lot's of parameters, or you find yourself using "new" a lot to create objects inside other objects (this is bad).
Now we need to clean up the dependencies. Any class that does work should not use "new" to create things inside itself. Pass those sub objects from out side. Classes which do no mechanical work are allowed to use the "new" operator. They just assemble stuff - we'll call them factories. A factory is a role in itself. A class should have just one role, thus factories should be separate classes.
20) Factor out the factories.
Now we repeat again...
21) Implement another thread of a use case any old how.
22) Refactor methods down to five lines or less, classes down to 2 pages or less (preferably a lot less), look for patterns to remove higher level duplication if it really makes the code cleaner, make sure you use separate classes for factories.
23) Repeat until your top level classes have an excessive number of parameters (say 8+).
You've probably finished by now. If not, look up the dependency injection pattern...
24) Create (only) your top level classes with a dependency injector.
Then you can repeat again...
25) Implement another thread of a use case any old how.
26) Refactor methods down to five lines or less, classes down to 2 pages or less (preferably a lot less), look for patterns to remove higher level duplication if it really makes the code cleaner, make sure you use separate classes for factories, pass the top level dependencies (including the factories) via DI.
27) Repeat.
At any stage in this heuristic you will probably want to take a look at test driven development. At the very least it will stop regressions while you refactor.
Obviously, this is a pretty simple process, and the information contained therein shouldn't be applied to every situation, but I feel like Marcus gets it right, especially with regards to the process one should use to design OO code. After a while, you'll start doing it naturally, it'll just become second nature. But while learning to do so, this is a great set of steps to follow.
As you said, there is nothing like experience. You can read every existing book on the planet about this, you'll still not as good as if you practice.
Understanding the theory is good, but in my humble opinion, there is nothing like experience. I think the best way to learn and understand things completely is to apply them in some project(s).
There you'll face difficulties, you'll learn to solve them, sometimes perhaps with a bad solution : but still you'll learn. And if at any time something bothers you and you can't find how to solve it nicely, we'll be here on SO to help you ! :)
I can advice you the book "Head First Design Patterns" (search Amazon). It is a good starting point, before seriously diving into the gang of fours' bible, and it shows design principles and the most used patterns.
In a nutshell : Code, criticize, look for a well-known solution, implement it, and back to first step till you're (more or less) satisfied.
As often, the answer to this kind of question is : it depends. And in that case, it depends how you learn things. I'll tell you what work for me, for I face the very problem you describe, but it won't work with everybody and I would not say it's "the perfect answer".
I begin coding something, not too simple, not too complex. Then, I look at the code and I think : all right, what is wrong ? For that, you can use the first three "SOLID principles" :
Single responsibility (are all your classes serving a unique purpose ?)
Open/Close principle (if you want to add a service, your classes can be extended with inheritance, but there is no need to alter the basic functions or your current classes).
Liskov Substitution (all right, this one I can't explain simply, and I'd advise reading about it).
Don't try to master those and understand everything about them. Just use them as guideline to criticize your code. Think chiefly about "what if I want to do this now ?". "What if I work for a client, and he wants to add this or that ?". If your code is perfectly adaptable to any situation (which is almost impossible), you might have reached a very good design.
If it's not, consider a solution. What would you do ? Try to come with an idea. Then, read about design patterns and find one that could answer your problem. See if it matches your idea - often, it's the idea you had, but better expressed and developped. Now, try to implement it. It's going to take time, you'll often fail, it's frustrating, and that's normal.
Design is about experience, but experience is acquired by criticizing your own code. That's how you'll understand design, not as a cool thing to know, but as the basis for a solid code. It's not enough to know "all right, a good code has that and that". It's much better to have experienced why, to have failed and see what whas wrong. The trouble with design pattern is that they are very abstract. My method is a way (probably not the only one nor the best) to make them less abstract to you.
No-solo-work. Good designs are seldom created by a single person only. Talk to your colleagues. Discuss your design with others. And learn.
Don't be too smart. A complex hierarchy with 10 levels of inheritance is seldom a good design. Make sure that you can clearly explain how your design works. If you can't explain the basic principles in 5 minutes, your design is probably too complex.
Learn tricks from the masters: Alexandrescu, Meyers, Sutter, GoF.
Prefer extensibility over perfection. Source code written today will be insufficient in 3 years time. If you write your code to be perfect now, but inextensible, you will have a problem later. If you write your code to be extensible (but not perfect), you will still be able to adapt it later.
The core concepts in my mind are:
Encapsulation - Keep as much of you object hidden from both the prying eyes and sticky fingers of the outside world.
Abstraction - Hide as much of the inner workings of you object from the simple minds of the code that needs to use your objects
All the other concepts such as inheritance, polymorphism and design patters are about incorporating the two concepts above and still have objects that can solve real world problems.
I would like to start my question by stating that this is a C++ design question, more then anything, limiting the scope of the discussion to what is accomplishable in that language.
Let us pretend that I am working on a vehicle simulator that is intended to model modern highway systems. As part of this simulation, entities will be interacting with each other to avoid accidents, stop at stop lights and perhaps eventually even model traffic enforcement with radar guns and subsequent exciting high speed chases.
Being a spatial simulation written in C++, it seems like it would be ideal to start with some kind of Vehicle hierarchy, with cars and trucks deriving from some common base class. However, a common problem I have run in to is that such a hierarchy is usually very rigidly defined, and introducing unexpected changes - modeling a boat for instance - tends to introduce unexpected complexity that tends to grow over time into something quite unwieldy.
This simple aproach seems to suffer from a combinatoric explosion of classes. Imagine if I created a MoveOnWater interface and a MoveOnGround interface, and used them to define Car and Boat. Then lets say I add RadarEquipment. Now I have to do something like add the classes RadarBoat and RadarCar. Adding more capabilities using this approach and the whole thing rapidly becomes quite unreasonable.
One approach I have been investigating to address this inflexibility issue is to do away with the inheritance hierarchy all together. Instead of trying to come up with a type safe way to define everything that could ever be in this simulation, I defined one class - I will call it 'Entity' - and the capabilities that make up an entity - can it drive, can it fly, can it use radar - are all created as interfaces and added to a kind of capability list that the Entity class contains. At runtime, the proper capabilities are created and attached to the entity and functions that want to use these interfaced must first query the entity object and check for there existence. This approach seems to be the most obvious alternative, and is working well for the time being. I, however, worry about the maintenance issues that this approach will have. Effectively any arbitrary thing can be added, and there is no single location in which all possible capabilities are defined. Its not a problem currently, when the total number of things is quite small, but I worry that it might be a problem when someone else starts trying to use and modify the code.
As one potential alternative, I pondered using the template system to achieve type safe while keeping the same kind of flexibility. I imagine I could create entities that inherited whatever combination of interfaces I wanted. Using these objects would entail creating a template class or function that used any combination of the interfaces. One example might be the simple move on road using just the MoveOnRoad interface, whereas more complex logic, like a "high speed freeway chase", could use methods from both MoveOnRoad and Radar interfaces.
Of course making this approach usable mandates the use of boost concept check just to make debugging feasible. Also, this approach has the unfortunate side effect of making "optional" interfaces all but impossible. It is not simple to write a function that can have logic to do one thing if the entity has a RadarEquipment interface, and do something else if it doesn't. In this regard, type safety is somewhat of a curse. I think some trickery with boost any may be able to pull it off, but I haven't figured out how to make that work and it seems like way to much complexity for what I am trying to achieve.
Thus, we are left with the dynamic "list of capabilities" and achieving the goal of having decision logic that drives behavior based on what the entity is capable of becomes trivial.
Now, with that background in mind, I am open to any design gurus telling me where I err'd in my reasoning. I am eager to learn of a design pattern or idiom that is commonly used to address this issue, and the sort of tradeoffs I will have to make.
I also want to mention that I have been contemplating perhaps an even more random design. Even though I my gut tells me that this should be designed as a high performance C++ simulation, a part of me wants to do away with the Entity class and object-orientated foo all together and uses a relational model to define all of these entity states. My initial thought is to treat entities as an in memory database and use procedural query logic to read and write the various state information, with the necessary behavior logic that drives these queries written in C++. I am somewhat concerned about performance, although it would not surprise me if that was a non-issue. I am perhaps more concerned about what maintenance issues and additional complexity this would introduce, as opposed to the relatively simple list-of-capabilities approach.
Encapsulate what varies and Prefer object composition to inheritance, are the two OOAD principles at work here.
Check out the Bridge Design pattern. I visualize Vehicle abstraction as one thing that varies, and the other aspect that varies is the "Medium". Boat/Bus/Car are all Vehicle abstractions, while Water/Road/Rail are all Mediums.
I believe that in such a mechanism, there may be no need to maintain any capability. For example, if a Bus cannot move on Water, such a behavior can be modelled by a NOP behavior in the Vehicle Abstraction.
Use the Bridge pattern when
you want to avoid a permanent binding
between an abstraction and its
implementation. This might be the
case, for example, when the
implementation must be selected or
switched at run-time.
both the abstractions and their
implementations should be extensible
by subclassing. In this case, the
Bridge pattern lets you combine the
different abstractions and
implementations and extend them
independently.
changes in the implementation of an
abstraction should have no impact on
clients; that is, their code should
not have to be recompiled.
Now, with that background in mind, I am open to any design gurus telling me where I err'd in my reasoning.
You may be erring in using C++ to define a system for which you as yet have no need/no requirements:
This approach seems to be the most
obvious alternative, and is working
well for the time being. I, however,
worry about the maintenance issues
that this approach will have.
Effectively any arbitrary thing can be
added, and there is no single location
in which all possible capabilities are
defined. Its not a problem currently,
when the total number of things is
quite small, but I worry that it might
be a problem when someone else starts
trying to use and modify the code.
Maybe you should be considering principles like YAGNI as opposed to BDUF.
Some of my personal favourites are from Systemantics:
"15. A complex system that works is invariably found to have evolved from a simple system that works"
"16. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over, beginning with a working simple system."
You're also worring about performance, when you have no defined performance requirements, and no problems with performance:
I am somewhat concerned about
performance, although it would not
surprise me if that was a non-issue.
Also, I hope you know about double-dispatch, which might be useful for implementing anything-to-anything interactions (it's described in some detail in More Effective C++ by Scott Meyers).