Skip to content
Author: Tianle Yuan

Software desgin principles⚓︎

Before we start to learn the design pattern, we need to know what is a good software design.

Good SW desgin?⚓︎

How to define a good SW desgin?

A good software design should show code reuse and extensibility.

Basic design principles⚓︎

Here we show the basic principles to make SW flexible, robust, and understandable.

1.1 Encapsulate varies⚓︎

There are two types of encapsulation: Method encapsulation and Class encapsulation.

Method encapsulation

When some operation in a method needs to be used or edited frequently, we choose to encapsulate the operations into submethods to increase the readability and flexibility.

See the highlight lines as an example. After the method encapsulation, the frequent-changed tax algorithms become a manageable unit.

method.cpp
//Calculate total money need to pay
float getTotal(Order order){ 
    int sum = 0, total = 0;

    //1. Calculate sum of all books' price
    for (auto item: order.m_items) sum += item.price * item.quantity; 

    //2. Calculate consumption tax
    if (order.m_country == "CN") 
        total += total * 0.05;
    else
        total += total * 0.07;

    return total;
}
method_encap.cpp
void addTax(Order & order, float & total){
    if (order.m_country == "CN") 
        total += total * 0.05;
    else
        total += total * 0.07;
}
//Calculate total money need to pay
float getTotal(Order order){
    int sum = 0, total = 0;

    //1. Calculate sum of all books' price
    for (auto item: order.m_items) sum += item.price * item.quantity;

    //2. Calculate consumption tax
    addTax(order, total);

    return total;
}
Class encapsulation

If the operation in a function contains too many assistant variables and methods, we abstract it into a new class to increase the readability and logic.

classDiagram
    class Order{
        - m_items : item
        - m_country : string
        - m_other_info1
        - m_other_info2
        - ...
        - getOrderTotal()
        - addTax(m_country, m_other_info1, m_other_info2, ...)
    }
classDiagram
    direction LR
    Order ..> TaxCalculator : Depends on
    note for Order "for (auto item: m_items) {\n.   total = item.price*item.quantity;\n.   total += total * addTax(m_country, m_other_info1, m_other_info2, ...);\n}"
    class Order{
        - m_items : item
        - m_country : string
        - m_other_info1
        - m_other_info2
        - ...
        - getOrderTotal()
    }
    class TaxCalculator{
        + addTax(country, info1, info2, ...)
        - getCountryTax(country)
        - getInfo1Tax(info1)
        - getInfo2Tax(info2)
    }

1.2 Program to an interface⚓︎

Program to an interface, not an implementation. It means making the dependency more flexible (decoupled)

  • Figure out the method, which demands the dependent class.
  • Using a new Interface or Abstract class to abstract the method.
  • Let the dependent class implement the abstraction.
  • Dependency injection (now the class in demand depends on the abstraction).
Example

Dog loves eating bones. Now let's make the code more extensible (although it temporally looks more complicated):

classDiagram
    direction LR
    Dog ..> Bone : Depends on
    note for Dog "this.m_energy += b.getNutrition();"
    class Dog{
        - m_energy : float
        + Eat(Bone b)
    }
    class Bone{
        - ...
        + getNutrition()
        + getFlavor()
        + getSize()
    }
classDiagram
    direction LR
    Dog ..> Food : Depends on
    Bone ..|> Food : Realize
    note for Dog "this.m_energy += b.getNutrition();"
    class Dog{
        - m_energy : float
        + Eat(Food b)
    }
    class Food{
        <<interface>>
        + getNutrition()
    }
    class Bone{
        - ...
        + getNutrition()
        + getFlavor()
        + getSize()
    }

1.3 Favor Composition over Inheritance⚓︎

To put Reuse Mechanism to work, we normally do NOT do inheritance, which will cause some problems:

  • The subclass can't abandon any interfaces of the superclass. All the abstract methods (pure virtual functions) in the superclass have to be implemented in the subclass, even though some of them are useless.

  • When overriding, should check compatibility between the new behavior and the base one. In case some code needs to use the subclass object to pass in superclass as the parameter.

  • Inheritance breaks superclasses' encapsulation. Subclasses have protect authority to visit their superclass. Vise, parents can also visit children.

  • Subclasses are tightly coupled to superclasses. Editing superclasses will destroy the functions of subclasses.

  • Reusing code through inheritance can lead to parallel inheritance hierarchies. The combination of classes will explode.

Let's use Composition. The technique we are using is called Delegation

InheritanceComposition!

Check the picture in UML class diagram again:

picture 3

With the same depth, we can change from using Inheritance to Composition or looser Aggregation.

E.g. Let's take vehicles as an example:

You can see lots of repeat codes since subclass cannot inherit two classes in the same type for ambiguity (diamond problem).

classDiagram
    direction BT
    Sedan --|> vehicles
    SUV --|> vehicles
    `Electronic Sedan` --|> Sedan
    `Fuel Sedan` --|> Sedan
    `Autopilot Electronic Sedan` --|> `Electronic Sedan`
    `Manual Electronic Sedan` --|> `Electronic Sedan`
    `Manual Fuel Sedan` --|> `Fuel Sedan`
    `Electronic SUV` --|> SUV
    `Fuel SUV` --|> SUV
    `Autopilot Electronic SUV` --|> `Electronic SUV`
    `Autopilot Fuel SUV` --|> `Fuel SUV`
    `Manual Fuel SUV` --|> `Fuel SUV`

You can solve this problem with composition. Instead of car objects implementing a behavior on their own, they can delegate it to other objects.

classDiagram 
    direction BT
    Engine *-- vehicles
    Driver o-- vehicles
    `Fuel Engine` --|> Engine
    `Electronic Engine` --|> Engine
    Robot --|> Driver
    Human --|> Driver

    class vehicles{
        - engine
        - tire
        + deliver(src,dst,cargo) 
    }
    class Engine{
        <<interface>>
        + move()
    }
    class Driver{
        <<interface>>
        + navigate()
    }

SOLID principles⚓︎

Besides the basic design principles we talked above, SOLID principles makes software design easier to understand, more flexible, and more maintainable. Here we focus more on using C++ examples for better understanding.

2.1 Single Responsibility Principle⚓︎

One class should be responsible for behaviors as simple as possible.

classDiagram 
    direction TB
    class Student{
        - ID : int
        + getID()
        + printTranscript()
    }
classDiagram 
    direction LR
    Transcript ..> Student
    class Transcript{
        - ...
        + print(studentID : int)
    }
    class Student{
        - ID : int
        + getID()
    }

2.2 Open/Closed Principle⚓︎

For any extension, the class should be "open-minded"; For any edition, the class should be "enclosed".

  • "Open-minded" (developing status): add new methods and attributes, and rewrite superclass.
  • "Enclosed" (releasing status): interfaces have been defined, and behaviors will not be edited anymore.
  • *Note: when the class itself has some bugs, directly edit it instead of create new subclasses.

There is a finished Order class.

classDiagram 
    direction LR
    note for Order "int getShippingCost(){\n . if(m_ship_method == 'air') return 80;\n . else return 20;\n}"
    class Order{
        - m_items
        - m_ship_method
        + getTotalCost()
        + getShippingCost()
        + setShippingType(string)
        + getShippingType()
    }

To add a new shipping method, you have to edit the original class. We can use the interface to extend the uneditable Order class.

classDiagram 
    direction LR
    note for Order "int getShippingCost(){\n . return Shipping.getCost(this);\n}"
    Order --o Shipping
    Ground ..|> Shipping
    Air ..|> Shipping
    class Order{
        - m_items
        - m_ship_method: Shipping
        + getTotalCost()
        + getShippingCost()
        + setShippingType(m_ship_method)
        + getShippingType()
    }
    class Shipping{
        <<Interface>>
        + getCost(order)
    }
    class Ground{
        + getCost(order)
    }
    class Air{
        + getCost(order)
    }

2.3 Liskov Substitution Principle⚓︎

2.4 Interface Segregation Principle⚓︎

2.5 Dependency Inversion Principle⚓︎

Programming techniques⚓︎

Reference⚓︎

Comments