Saturday, July 16, 2022

C++ API Design Best Practices - CLASS DESIGN

CLASS DESIGN

Inheritance

  • Is it appropriate to add the class to an existing inheritance hierarchy?
  • Should you use public or private inheritance? 
  • Should you support multiple inheritances? 
  • This affects which member functions should be virtual.

Composition

  • Is it more appropriate to hold a related object as a data member rather than inheriting from it directly?

Abstract interfaces. 

  • Is the class meant to be an abstract base class, where subclasses must
  • override various pure virtual member functions?

Standard design patterns. 

  • Can you employ a known design pattern to the class design?
  • Doing so lets you benefit from well-thought-out and refined design methodologies and makes your design easier to use by other engineers

Initialization and destruction model. 

  • Will clients use new and delete or will you use a factory method? 
  • Will you override new and delete for your class to customize the memory allocation behavior? 
  • Will you use smart pointers?

Defining a copy constructor and assignment operator

  • If the class allocates dynamic memory, you need both of these (as well as a destructor of course). 
  • This will impact how your objects will be copied and passed by value.

Templates

  • Does your class define a family of types rather than a single type? If so, then you may consider the use of templates to generalize your design.

Const and explicit. 

  • Define arguments, return results, and methods as const wherever you can. 
  • Use the explicit keyword to avoid unexpected type conversions for single-parameter constructors.

Defining operators define 

  • Define any operators that you need for your class, such as +, *=, ==, <<

Defining type coercions

  • Consider whether you want your class to be automatically coercible to different types and declare the appropriate conversion operators.

Use of friends. 

  • Friends breach the encapsulation of your class and are generally an indication of bad design. Use them as a last resort.

Non-functional constraints. 

  • Issues such as performance and memory usage can place constraints on the design of your classes.

Using Inheritance

Design for inheritance or prohibit it. 
  • The most important decision you can make is to decide whether a class should support subclasses.  If it should, then you must think deeply about which methods should be declared as virtual and document their behavior. 
  • If the class should not support inheritance, a good way to convey this is to declare a non-virtual destructor.
Only use inheritance where appropriate. 
  • Deciding whether a class should inherit from another class is a difficult design task. In fact, this is perhaps the most difficult part of software design. 
Avoid deep inheritance trees
  • Deep inheritance hierarchies increase complexity and invariably result in designs that are difficult to understand and software that is more prone to failure. 
  • The absolute limit of hierarchy depth is obviously subjective, but any more than two or three levels is already getting too complex.
Use pure virtual member functions to force subclasses to provide an implementation
  • A virtual member function can be used to define an interface that includes an optional implementation, whereas a pure virtual member function is used to define only an interface, with no implementation (although it is actually possible to provide a fallback implementation for a pure virtual method). Of course, a non-virtual method is used to provide behavior that cannot be changed by subclasses.
Don’t add new pure virtual functions to an existing interface
  • You should certainly design appropriate abstract interfaces with pure virtual member functions.
  • However, be aware that after you release this interface to users if you then add a new pure virtual method to the interface then you will break all of your clients’ code. That’s because clients’ classes that inherit from the abstract interface will not be concrete until an implementation for the new pure virtual function is defined.
Don’t overdesign
  • A good API should be minimally complete. In other words, you should resist the temptation to add extra levels of abstraction that are currently unnecessary.
  • For example, if you have a base class that is inherited by only a single class in your entire API, this is an indication that you have overdesigned the solution for the current needs of the system.


Private Inheritance

  • Private inheritance lets you inherit the functionality, but not the public interface, of another class. In essence, all public members of the base class become private members of the derived class.
  • I refer to this as a “was-a” relationship in contrast to the “is-a” relationship of public inheritance.

 

class Ellipse 
                    {
                        public:
                            E
llipse();
                            Ellipse(float major, float minor);
                            void SetMajorRadius(float major);
                        private:
                            float mMajor;
                            float mMinor;
                    };

                    class Circle: private Ellipse
                    {
                        public:
                            Circle();
                            explicit Circle(float r);
                            void SetRadius(float r);
                            float GetRadius() const;
                    };

  • In this case, Circle does not expose any of the member functions of Ellipse, that is, there is no public Circle::SetMajorRadius() method
  • If you do want to expose a public or protected method of Ellipse in Circle then you can do this as follows.
                     class Circle: private Ellipse
                    {
                        public:
                            Circle();
                            explicit Circle(float r);
                            void SetRadius(float r);
                            float GetRadius() const;

                            // expose public methods of Ellipse
                            using Ellipse::GetMajorRadius;
                            using Ellipse::GetMinorRadius;
                    };

Composition

  • This simply means that instead of class Circle inheriting from EllipseCircle  declares Ellipse as a private data member (“has-a”) 
            or 
  • class  Circle declares a pointer or reference to Ellipse as a member variable (“holds-a”).  

                     class Circle
                    {
                        public:
                            Circle();
                            explicit Circle(float r);
                            void SetRadius(float r);
                            float GetRadius() const;
                        private 
                            Ellipse mEllipse;

                    };

  • In this case, the interface for Ellipse is not exposed in the interface for Circle. However, Circle still builds upon the functionality of Ellipse by creating a private instance of Ellipse.
  • Composition, therefore, provides the functional equivalent of private inheritance. However, there is wide agreement among object-oriented design experts that you should prefer composition over inheritance
  • With composition, the class is only coupled with the public members of the other class. Furthermore, if you only hold a pointer to the other object, then your interface can use a forward declaration of the class rather than #include its full definition. This results in greater compile-time insulation and improves the time it takes to compile your code.

The Open/Closed Principle

  • This means that the behavior of a class can be modified without changing its source code. This is a particularly relevant principle for API design because it focuses on the creation of stable interfaces that can last for the long term. 
  • The principal idea behind the OCP is that once a class has been completed and released to users it should only be modified to fix bugs. However, new features or changed functionality should be implemented by creating a new class. This is often achieved by extending the original class, either through inheritance or composition, and also by providing a plugin system to allow users of your API to extend its basic functionality.

The Law of Demeter

  • The Law of Demeter (LoD) states that you should only call functions in your own class or on immediately related objects.

Class Naming

  • Simple class names should be powerful, descriptive, and self-explanatory
  • Good names drive good designs. Therefore, If a class is difficult to name, that’s usually a sign that your design is lacking
  • Sometimes it is necessary to use a compound name to convey greater specificity and precision, such as TextStyle, SelectionManager, or LevelEditor
  • If you are using any more than two or three words then this can indicate that your design is too confusing or complex.
  • Interfaces (abstract base classes) tend to represent adjectives in your object model. They
  • can therefore be named in this way, for example, Renderable, Clonable, or Observable.
  • Alternatively, it’s common to prefix interface classes with the uppercase letter “I,” for example, IRenderer and IObserver.
  • Include some form of the namespace for your top-level symbols, such as classes and free functions, so that your names do not clash with those in other APIs that your clients may be using.

References 

  • API Design for C++ Book by Martin Reddy







No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...