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.
//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;
}
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
orAbstract 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
Inheritance
→ Composition
!
Check the picture in UML class diagram again:
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)
}