asked "Using C++, how can maximum flexibility be maintained?"
This page has been set up expand the question and ask "what guidelines should be used when taking a C++ project extreme?"
- Strong Typing
- Types represent the contract between client objects and their service providers. When the contract changes all the clients and all of the service providers must be recompiled and maybe even edited.
- Nested #includes increase compile times and make definitions hard to find.
- You shouldn't have to go searching for those definitions by hand in the first place, but you generally have to anyway. The tools for browsing, source control, and building C++ projects are generally abysmal. The tools that are good tend to lock you into a particular vendor's toolset and platform.
- Multiple vendors
- Which brings up the fact that you often don't have everything that you need right in front of you. It is often advantageous to purchase a third party library that already implements some of the functionality that you need. Doing so may mean introducing incompatible abstractions into your program.
- Lack of Encapsulation
- The fact that the language starts at a very low level means that there are many things that aren't well encapsulated. Strings are a prime example of this: there are three flavors of character representation (the C++ standard library only supports two), the compiler only understands the rawest format ("this represents an array of 42 characters" -- the last one is the null terminator) and different libraries provide different, incompatible, ways to encapsulate them. This lack of encapsulation can lead to very inflexible code.
- Primitive resources
- The primitive nature of C++ also means that many abstractions that you take for granted or would like to take for granted need to be built from the ground up. This adds a maintenance burden to the project which may make it more inflexible.
(please add any others you can think of)
Your biggest enemy here is dependencies: dependencies that cause long compiles, dependencies that cause excessive editing, conflicting dependencies (e.g. one method needs two different string representations). Your next biggest enemy is detail. In C++ you really do have to implement a lot of your own support. After that is the fact that you have to fight with your tools to get them to work. Finally, you have to deal with the fact that C++ source files can be hard to navigate. These problems aren't daunting so much as they are irritating. They can slow you down or even kill your project if you let them. Fortunately there are a lot of people who have a lot of experience dealing with these problems and you can learn from them:
- Plan ahead
- Just get revision control, release management, and your build engine set up early. These areas are often given short shrift and cause problems down the line.
- Coding and design conventions
- These are vital because C++ gives you endless tradeoffs. A team should have a set of "forms" and policies that they use. A consistent policy towards memory management is essential. A good starting point for conventions is the Ellemtel standard, now simplified and morphed into IndustrialStrengthCeePlusPlus. Taking this and StlStyle as a basis won't steer you wrong.
- Use an automated build environment
- Makeit (http://www.dscpl.com.au) is a good example. Many C++ developers have been spoiled rotten by IDEs, especially VC6, which make automatic builds very hard. When the IDEs don't do what you need, they still resist anything that isn't built-in. So make an auto-build environment and help the developers configure their IDEs to invoke it.
- Imitate Java's file management scheme
- One public class per source/header pair, one header per source file, one package/project per directory. Files and directories named after the classes and packages that they contain. This will make it much easier to find things. The exception here is decorators; don't put these in files of their own if they only make sense in the context of their primary class.
- Keep your package hierarchy flat
- This makes it easy to move classes around to make your libs smaller. Doing template closures on large libs consumes much more time than you'd expect.
- Avoid nested #includes
- Nested #includes lead to propagating dependencies. Avoid this by using forward declarations whenever possible. JohnLakos' book on large scale programming in C++ goes into depth about other ways to break the #include chain of dependencies. The important thing is not so much to break every link, but to introduce breaks often. Packages can help with that.
- Use interfaces
- Abstract base classes in C++ are like interfaces in Java. You can reduce coupling by depending on them instead of depending directly on the implementing classes themselves. They are expensive because they have to be defined before they can be used. As a result they aren't used enough. Add this to your discipline.
- Use templates
- A templated method can take parameters of any type so long as those types implement the methods that the templated method actually uses. When you can do this it will save you from having to use abstract base classes.
- Design interfaces before implementing
- There is a small problem with using interfaces to reduce coupling: you are still coupled to the interface. If it changes then all of its implementers must also change and everything that depends on it, clients and implementers alike, must be recompiled. YouArentGonnaNeedIt is as likely to hurt you as help you in this circumstance because interfaces are so expensive to change. This is where being good at analysis and abstractions really helps. OnceAndOnlyOnce is the tool to use here, make sure that your interface says all it needs to say (because you don't want to change it) and that it says it as simply as possible (because you are going to have to implement that interface in its entirety several times).
- Start with all methods class-inline
- Only take 'em out of line when they get larger than 5 lines or when they obtain more than one exit point. Always leave accessors and two-liners class-inline. That way they self-document. Also DontUseGetAndSet.
- Build infrastructure
- In C++ I often find that I have to create a way to say what I want to say before I can actually say it. XP will lead in the right direction on this but you'll end up with an organizational problem because of all the classes (and attendant dependencies) you'll create. The next two pieces of advice are directed at this problem so don't be afraid of it. C++ programmers (and more often C programmers) are apt to build lower level infrastructure right into their higher level methods because it is easier to do in the moment. XP should help with this but it will probably be a little bit of a hurdle.
- Design the infrastructure
- The lower the level of infrastructure the better the chances that it will be needed in more than one place. Infrastructure that isn't easily reused will likely be duplicated. Infrastructure that is heavily reused is difficult to refactor because of all the dependent clients. It's important to remember that C++ projects have deeper dependency graphs than Smalltalk or Java projects because that means there is a higher potential for a small change at the lowest level of the project to have impact at the highest levels. Here is the one other place in C++ programming where YouArentGonnaNeedIt isn't always useful. The answer here is the same as for interfaces: carefully design the abstractions that the lower levels of infrastructure present. The more stable the abstractions are the more stable your project will be. The key is to pay attention to the cost curve -- when it's flat assume YouArentGonnaNeedIt when it starts to curve you should start to DesignUpFront.
- Partition using packages
- This is a dependency management technique that is particularly effective with lower level infrastructure. A package should have a very small number of well designed classes or interfaces that are public to the rest of the project. It should be a full sub-project that can be distributed as a library and header file(s). They should be versioned, tested and released internally independent of the rest of the project. The clients of a package should use the whole package. If a package needs to be changed the changes should be local to the package but it should be okay if everything inside the package has to change. Cyclic dependencies within packages have little impact on the project but cyclic dependencies between packages will kill you. RobertMartin and JohnLakos have written a bunch on this subject. The gist of it is that on a large project you can manage the dependencies between a relatively small number of packages instead of a large number of classes. I think that the ability to have a large number of classes is valuable enough that packages should be used on all but the smallest of projects, but that's just me.
- Stop building infrastructure
- If you're doing XP you don't need this. Otherwise you probably do. Build the infrastructure that you need, build it right, then stop messing with it and move on. It's very easy to get bogged down in creating the semantics for what you want to say. It's also easy to create cool stuff that no one else in your project can understand how to use. Keep it simple.
- Use MixIns
- The "other" use for multiple inheritance. C++ uses inheritance instead of Java's "implements" key word for interfaces so we use that and it leads to a fairly benign form of multiple inheritance. A lot of the rest of the time we want to leave multiple inheritance alone. The exception occurs when we have a particular ability that we would like to "mix in" to existing classes. Mixins are particularly useful for implementing resource management and notification schemes. They can be made even more effective by using them in conjunction with templates. This is a feature that Smalltalk and Java don't have, I didn't want you to miss out on it.
- Do not use MixIns
- Mixins are just about the nastiest dependencies there are, and since your biggest enemy here is dependencies, mixins should be particularly shunned.
- Go slow
- There are some things that are harder to do in C++ than they should be. Do them anyway. Adding a new method to an interface means having to write its signature at least three times (in the interface, in the implementing class's header and in the implementing class's source file), but if that's the right thing to do then do it. XP shines in this department. Take advantage of that. The biggest trouble that I've seen C++ projects get into has always stemmed from doing the easy thing instead of the right thing.
- Consider AlternateHardAndSoftLayers
- Use the most appropriate of Python, Haskell, Perl or <shudder> VB for the soft layer. Better to write most of your system in a soft language that's easier to maintain, then profile and rewrite the hard bits in C++. But you may find strong management resistance to this notion - management sees extra languages as extra risks, not risk reducers.
- Scrupulously apply the AcyclicDependenciesPrinciple when refactoring.
- Don't let C atavisms creep into your code
- keep everything strictly within StlStyle.
- Use a really good cross-platform generic library
- ObjectSpace is my favorite. And, if in MS-land, use ATL and never ever use MFC.
- Use as a rule in nested includes.
- you can get away without designing interfaces up-front if you work in a microcosm and transition towards the full project. C++ XP has to bend either in this direction to avoid designing them up-front when compile time is considered.
- Use ReferenceCounting
- Heap allocated objects should almost always be ref-counted and a smart pointer template should be used to automate that ref-counting.
- The Debugger Is Your Interpreter
- this is the place to try quick inner-loop repetitions. A reasonable debugger will reload only the class libraries that have changed since the last run. But most debuggers break on distributed and multi-threaded work, so remember the rest of the DebuggingPatternLanguage.
- Try RefactoringCppToReduceDependencies
- Start out by developing in a way that is very flexible and easy to change. Perhaps Java style -- writing all the code inline in the header. As dependency problems start creeping up try these refactorings to reduce them.
The key thing for keeping a C++ project flexible enough to do XP on it is to identify the areas where you really can't be that flexible and isolate them. That would be at the lowest levels of infrastructure and at the most frequently implemented interfaces. I think that these areas lend themselves better to more traditional analysis and design techniques. If you do that then it seems possible to me that you can get the best of both the XP and traditional approaches.
- Write refactoring tools
- A general problem with C++ is the ease of refactoring. This distorts the design of the application, as programmers will avoid the hard-to-do thing, and do something easier that stores up problems for the future. But often a perl script (or similar) can help get around this. One instance: moving code from one package to another (and fixing up relative include paths etc). Another instance: generating project files for multiple builds. Another instance: analyzing cross - #include hotspots. Obviously the ideal would be a refactoring browser (sigh).
I've added my first principles crack at dealing with XP in C++ at the bottom of the discussion page. I hope you all take a look at it. It contains a few ideas that I wish I'd run past you all before this page became so large.
I have recently written a small position paper on this topic. You can find it in the 'XP' link of http://objectmentor.com/
. It is called Can XP be used with C++?
The conclusion of the paper is:
XP can be practiced in C++, but only if the code is kept as flexible as possible. Without that flexibility, one of the core principles of XP, namely refactor ruthlessly, will not be cost effective. We have found that applying the principles of dependency management [see: PrinciplesOfObjectOrientedDesign
]is a very effective way to keep C++ code flexible and refactorable thereby enabling the use of XP with C++. -- RobertCecilMartin
I am skeptical. The contributors to this and other Wiki pages on the subject have yet to convince me how doing Xp in Cpp is any harder than doing it anywhere else. Would some folks like to try again at WhyCppNeedsSpecializedXp
? -- MichaelHill
It's harder because a large C++ project compiles much more slowly than a large project in Smalltalk or other dynamic languages. Slowness reduces agility.
Yes, and lots of interdependencies mean that there is a high chance of a significant portion of your project needing recompiling if you make even minor edits (e.g. RenameClass
I see a lot of personal bias expressed on this page, but little that may actually apply to using Extreme Programming with C++. Aside from the fact that some people just out and out do not like C++, I also don't see what the big deal is in applying XP to C++. If XP is so fragile that switching programming languages causes it problems, then it cannot qualify as a programming methodology. I don't believe this and echo the request from above. I ask for some logical arguments in WhyCppNeedsSpecializedXp
Some of the above lifted from ExtremeGuidelinesForCeePlusPlusDiscussion
See also: ExtremeFormsForCppCode