Code Complete 2 – Steve McConnell – Working Classes
I just love Steve McConnell's classic book Code Complete 2, and I recommend it to everyone in the Software 'world' who's willing to progress and sharpen his skills.
Other blog posts in this series:
Class Foundations: Abstract Data Types (ADTs)
An abstract data type is a collection of data and operations that work on that data. The operations both describe the data to the rest of the program and allow the rest of the program to change the data.
For example, if you are not using ADTs to change a bold property of text you could write something like:
currentFont.bold = true;
but a better way, which keeps the abstraction of the class, is:
It ain't abstract if you have to look at the underlying implementation to understand what's going on. ~ P.J. Plauger
By thinking about ADT, and not about classes, we are programming into the language and not programming in language.
If a stack represents a set of employees, treat the ADT as employees rather than as a stack. Treat yourself to the highest possible level of abstraction.
Try to make the names of classes and access routines independent of how the data is stored.
is better than
One way to think of a class is as an abstract data type plus inheritance and polymorphism.
Good class interface
The first and probably most important step in creating a high-quality class is creating a good interface. This consists of creating a good abstraction for the interface to represent and ensuring the details remain hidden behind the abstraction.
If the class interface doesn't present a consistent abstraction, then the class has poor cohesion.
Class interface consists of public routines (functions, methods)
Each class should implement one ADT!
In some cases, you’ll find that half a class’s routines work with half the class’s data, and half the routines work with the other half of the data. In such a case, you really have two classes masquerading as one. Break them up!
Move unrelated information to another class
Encapsulation is a stronger concept than abstraction.
Abstraction helps to manage complexity by providing models that allow you to ignore implementation details. Encapsulation is the enforcer that prevents you from looking at the details even if you want to.
Don't expose member data in public
You shouldn't expose data like this:
float x; float y;
because client code can monkey around with data. And perfect encapsulation would look like:
float GetX(); float GetY(); void SetX( float x ); void SetY( float y );
Now you have no idea whether the underlying implementation is in terms of floats x, y, whether the class is storing those items as doubles and converting them to floats, or whether the class is storing them on the moon and retrieving them from a satellite in outer space. ?
Avoid friend classes
In a few circumstances such as the State pattern, friend classes can be used in a disciplined way that contributes to managing complexity. But, in general, friend classes violate encapsulation. They expand the amount of code you have to think about at any one time, increasing complexity.
Design and Implementation Issues
Containment ("has a" Relationships)
Containment is a simple idea that a class contains a primitive data element or object. A lot more is written about inheritance than about containment, but that’s because inheritance is more tricky and error prone, not because it’s better.
Inheritance ("is a" Relationships)
Don't inherit a class instead it's a truly "is a" more specific version of the Base Class. ~ Barbara Liskov
Inherited routines come in three basic flavors:
+ An Abstract overridable routine means that the derived class inherits the routine's interface but not its implementation.
+ Overridable routine means that the derived class inherits the routine's interface but not its implementation
Overridable routine -> polymorphism
+ A non-overridable routine means that the derived class inherits the routine's interface and its default implementation and it is not allowed to override the routine's implementation
Don't reuse names of non-overridable base-class routines in derived classes.
Avoid deep inheritance trees
Deep inheritance trees have been found to be significantly associated with increased fault rates. That's because of deep inheritance trees increase complexity, which is exactly the opposite of what inheritance should be used to accomplish.
7±2 subclasses from base class, and maximum of three levels of inheritance
Rules for inheritance
- If multiple classes share common data but not behavior, create a common object that those classes can contain.
- If multiple classes share common behavior but not data, derive them from a common base class that defines the common routines.
- If multiple classes share common data and behavior, inherit from a common base class that defines the common data and routines.
- Inherit when you want the base class to control your interface; contain when you want to control your interface.
Law of Demeter
The rule states that Object A can call any of its own routines. If Object A instantiates an Object B, it can call any of Object B's routines. But it should avoid calling routines on objects provided by Object B.
Initialize all member data in all constructors, if possible. Initializing all data member in all constructors is an inexpensive defensive programming practice.
Enforce the singleton property by using a private constructor. If you want to define a class that allows only one object to be instantiated, you can enforce this by hiding all the constructors of the class and then providing a static GetInstance() routine to access the class's single instance.
Deep and shallow copy
- Deep copies are simpler to code and maintain than shallow copies
- Shallow copies are created typically to improve performance
Prefer deep copies to shallow copies until proven otherwise.
Reasons to Create a Class
Create a class to hide information so that you won't have to think about it and to reduce complexity. Sure, you will need to think about it when you write the class, but after it's written you can forget about the implementation details and use the class without any knowledge of its internal workings.
- Class interface should provide a consistent abstraction. Many problems arise from violating this single principle.
- Classes must have data members and behavior
- Containment is usually preferable to inheritance unless you're modeling an "is a "relationship
- Inheritance is a useful tool, but it adds complexity, which is counter to Software's Primary Technical Imperative of managing complexity
— Nikola Brežnjak (@HitmanHR) August 21, 2017