4.5 Principles for OO design

In this section we list some principles for object-oriented software engineering. As kind of an intellectual game, we've made an effort to label each principle with an OOA, OOD, or OOP, according to at which stage of the software engineering process the principle is most likely to come into play. The idea is that principles marked OOA are things you can work out when you're doing the high-level design, the OOD are things that you'll get into when you work out the detailed design for the classes, while the principles marked OOP are design details that you're more likely to think of after you start coding. Don't take these labels too seriously!

  • OOA: An object is an organism. Make class objects responsible for all their behavior. A class should own every method it needs to do things.

  • OOA: Let your classes multiply. Freely derive classes from your base classes to implement variations on behavior. Remember, you want to start thinking of defining a class as something easy. Move as much code as possible up into helper functions that live in the base class; this makes deriving the children easier.

  • OOA: Use utility classes in place of primitives. Most of your class members should be other classes, or pointers to other classes. Admittedly, somewhere you will need to have data that is of a primitive type such as int, float and char. But, so far as possible, you should wrap these primitives up inside of classes that are closer to the way you think. Thus we use the MFC CString objects instead of char arrays, and we use our cVector objects instead of pairs of float x, y. With practice, you can learn to think of creating a new class as something easy and helpful, rather than as something arcane and risky! Figure 4.13 illustrates the kind of class nesting that you might expect to see in an OOD.

    Figure 4.13. The dots are primitives, the shapes are classes


  • OOA: Keep your classes light. Don't make one single class do too much. You wouldn't want to have only one class called Main, with all of your program's data and methods in it! It makes the code easier to develop and maintain if each class has only a limited number of related responsibilities. Delegating a given functionality off into a separate class gives you the option of implementing it in some standard way in a base class with child classes for alternate behaviors.

  • OOA: Reuse classes. When appropriate, inherit from or compose with other people's classes, or classes that you've used in other programs. Be aware of what classes are available for you in the MFC framework, for instance.

  • OOA: Prefer object composition to class inheritance. If you use composition it makes it easier to have each class be focused on one kind of task. Composition also prevents a combinatorial explosion of classes.

  • OOD: Think like an object. In trying to determine a class's methods, try and read through your code taking the viewpoint of one of your class objects.

  • OOD: Use pointer members rather than instance members. When you give a ClassB a member object of ClassA, you can either declare it as ClassA _mA or as ClassA *_pA. The former is an instance member, the latter is a pointer member. Using pointer members is a bit more work because you need to remember to construct and destroy the object it points to. The virtue of a pointer member is that you can put child class variables into it without having to upcast them to the base class as you would with an instance member. This makes polymorphism possible; that is, if a pointer member is of type ChildA* instead of type ClassA*, then it will use the overridden methods of ChildA.

  • OOD: Program to an interface, not to an implementation. When thoroughly carried out, this principle means that you have abstract, implementation-free classes at the top of your class hierarchies. This gives all of the derived classes identical interfaces. When less thoroughly done, this simply means that you try and think always in terms of what your classes have in common. An example of this in action would be to have your object variables be given the highest base class type possible rather than a specific child class type. Thus, it would be better to have, say, a cCritter * variable than to have a cCritterSpaceWarGameAsteroid * variable. The reason is that if your code only mentions the base class cCritter, then the code is more reusable.

  • OOP: No forgery. Avoid storing the same data in two different places. Any copy of a data object is a 'forgery' which may be corrupt. Thus, it would be a mistake to try and maintain an int _crittercount member in the cGame class, because the same information is already present in the CArray<cCritter*> _pbiota cGame member, and can be accessed as _pbiota->GetSize(). If we kept a separate _crittercount variable, we'd repeatedly have to worry about keeping the 'forged' _crittercount in synch with the 'genuine' _pbiota->GetSize().

  • OOP: Don't write the same code twice. Avoid writing the same code in two different places. If you have more than three or four lines of code that you use twice, put this code inside a method that you call in the different places. The reason for this is that if you have the same code in two places, then over time (bug-fixes, development) the two versions may drift apart and become different. Sometimes you will want to encapsulate the code inside a class that you can compose with.

  • OOP: Encapsulate methods. If you use some piece of data as an explicit or implicit argument to function calls more than two lines in a row, think about giving the class that owns the data a method that accomplishes these lines with a single call. Thus, if we have a cVector v object, instead of writing Real mag = sqrt(v.x()*v.x() +v.y()*v.y() +v.z()*v.z()), we give cVector a magnitude method that does the calculation and allows us to just write Real mag = v.magnitude().

  • OOP: Don't ask objects their class type. If you're doing a switch on the type of a class, you should replace the switch with a polymorphic function call so you don't need to find out the class's type. This said, there are times when we will use the MFC GetRuntimeClass method to condition an action on the type of an argument object. But always think twice to see if it's really necessary.

  • OOP: Don't break encapsulation. Avoid friend statements like the plague. Make most data private or at least protected. Conceal the actual implementation structure of your class, and reveal only the few basic public methods that others need to call. It takes only seconds to write in-line accessor and mutator methods like Real age(){return _age;}; void setAge(Real age){age = _age;}. If a method is in-line and non-virtual, there is no computational cost whatsoever in using it, because the preprocessor replaces each occurrence of, e.g., 'age()' by '_age'. So why bother? Because, with reference to the age example, at some point you may decide to associate another age-related variable with the _age variable, and then you may want to change the accessor, the mutator, or both.

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference