C++ Programming Guidelines – Class Design

01

Class Design

A class is the foundation of object-oriented design. A good class should have a single responsibility, a clear and concise interface, low coupling between classes, high cohesion within the class, and effectively demonstrate encapsulation, inheritance, polymorphism, and modularity.02

Single Responsibility of Classes

Explanation: A class should have a single responsibility. If a class has too many responsibilities, it is often difficult to design, implement, use, and maintain. As functionality expands, the scope of a class’s responsibilities naturally broadens, but responsibilities should not diverge. Use small classes instead of large classes.Small classes are easier to write, test, use, and maintain. Small classes embody the concept of simple design; large classes weaken encapsulation, often taking on too many responsibilities in an attempt to provide a “complete” solution, which is often difficult to achieve successfully. If a class has more than 10 data members, it may have too many responsibilities.

03

Information Hiding

Explanation: Encapsulation is one of the core concepts of object-oriented design and programming. It hides the internal data of the implementation, reducing the dependency between the caller’s code and the specific implementation code.⚫ Minimize global and shared data;⚫ Prohibit member functions from returning writable references or pointers to members;⚫ Set data members as private (except for structs) and provide related access functions;⚫ Avoid providing access functions for every class data member;⚫ Use runtime polymorphism to separate internal implementation (provided by derived classes) from external interfaces (provided by base classes).04

Make Class Interfaces Orthogonal, Minimal, and Complete

Explanation: Interfaces should be defined around a core concept, providing services and collaborating with other classes, making them easy to implement, understand, use, test, and maintain. Interface functions should be orthogonal, avoiding one interface function overlapping another. Too many interface functions can be difficult to understand, use, and maintain. If a class contains more than 20 non-private member functions, its interface may not be concise enough.05

Do Not Expose Private and Protected Members in External Interface Classes

Explanation: Exposing protected or private members in external interface classes breaks encapsulation. Once the design of a class changes (adding, deleting, or modifying internal members), it can lead to recompilation of associated components or systems, increasing system compilation time and causing binary compatibility issues, leading to associated upgrades and patches. Therefore, unless necessary, do not expose private and protected members in interface classes.There are several approaches:⚫ Use pure virtual classes as interface classes, with implementation classes completing the implementation, so that users only see the interface class. The drawbacks of this approach are:◼ The code structure is relatively complex.◼ New interfaces must be added after existing ones, and the order of existing interfaces cannot be changed. Otherwise, due to the virtual function table, it will cause client code to recompile.⚫ Use the PIMPL pattern in interface classes (only a private data member pointing to the implementation class), with all private members encapsulated in the implementation class (the implementation class can be kept out of the header file and placed directly in the implementation file).◼ The code structure is simple and easy to understand.◼ It can save virtual function overhead, but there is an indirect access overhead.◼ Modifying the implementation will not cause client code to recompile.

class Interface {   public:     void function();   private:     Implementation* impl_; }; class Implementation {   public:     int i;     int j; }; void Interface:: function () {   ++impl_->i; }

06

Avoid Returning Writable References or Pointers to Members from Member Functions

Explanation: This breaks the encapsulation of the class, as the members of the object can be modified without the object itself knowing.Example: Bad Example

class Alarm { public:   string& getname(){return name;} // Breaks encapsulation, member name is exposedprivate:   string name; };

07 Exceptions: In certain cases, it is indeed necessary to return writable references or pointers, such as in one implementation of the singleton pattern:

Prohibit Circular Dependencies Between Classes

Explanation: Circular dependencies greatly increase the coupling of the system, so circular dependencies between classes are prohibited. Class A depends on class B, and class B depends on class A. In such cases, adjustments to class design are needed, introducing class C:⚫ Upgrade: Move the associated business to class C, making class C depend on class A and class B to eliminate circular dependencies.⚫ Downgrade: Move the associated business to class C, making both class A and class B depend on class C to eliminate circular dependencies.Example: Class Rectangle and Class Window depend on each other

class Rectangle { public:   Rectangle(int x1, int y1, int x2, int y2);   Rectangle(const Window& w); }; class Window { public:   Window(int xCenter, int yCenter, int width, int height);   Window(const Rectangle& r);};

We can add class BoxUtil as a converter to avoid mutual dependency

class BoxUtil { public:   static Rectangle toRectangle(const Window& w);   static Window toWindow(const Rectangle& r); };

08

Set Data Members as Private (Except for Structs) and Provide Related Access Functions

Explanation: Information hiding is key to good design. All data members should be set as private to precisely control the read and write of member variables, shielding the internal implementation. Otherwise, it means that part of the class’s state may be uncontrolled and unpredictable, because:⚫ Non-private members break the encapsulation of the class, leading to the class itself not knowing when its data members are modified;⚫ Any modification to the class will extend its impact on the code that uses the class. By privatizing data members, provide related access functions when necessary, such as defining variable foo_ and access function foo(), assignment operator set_foo(). Access functions are generally defined as inline functions in the header file. If there is no external demand, private data members may not provide access functions to achieve hiding and protection. Do not access the addresses of private data members through access functions (see rule 4.2).09

Use the PIMPL Pattern to Ensure Private Members Are Truly Invisible

Explanation: C++ designates private members as inaccessible, but they are still visible. The PIMPL idiom can make private members invisible within the current class scope. PIMPL mainly achieves separation of interface and implementation through forward declarations, reducing compilation time and coupling.Example:

class Map { private:   struct Impl;   shared_ptr<Impl> pimpl_; };

So I am here

Click the card below to follow me

↓↓↓

C++ Programming Guidelines - Class Design

If you find it interesting, please click

Save

Like

Look at it

+1

❤❤❤C++ Programming Guidelines - Class Design

Leave a Comment