不同于设计模式,在面向对象软件设计当中,还有 SOLID 代码设计原则,本文将对其进行介绍和举例。
1、什么是设计模式
设计模式是软件设计当中通用的、可复用的解决方案,它是关于如何在不同情况下如何解决问题的描述或模版,通常是最佳实践。
设计模式使得代码变得可维护的、可扩展的、解耦的,开发者为每个解决某种类型问题的方案起名,整理成各种通用的设计模式。
注意,软件架构和设计模式是不同的概念,软件架构描述的是要实现什么样的功能和在什么地方来实施,而设计模式是描述怎样来实现。
设计模式不是要编写解决方案,它更像是描述解决方案应该长什么样。在设计模式当中,问题及其解决方案是密不可分的。
我们需要设计模式,这样代码会更加清晰、直观、占用更少的内存空间、性能高效,也更加方便我们后续进行代码的修改。
设计模式主要包含 6 条规则:
-
经过验证和实践的(Proven Solutions)
-
可以简单地进行复用(Easily Reusable)
-
直观的(Expressive)
-
易于交流的(Ease Communication)
-
预防重构的需要(Prevent the Need for Refactoring Code)
-
最小化代码(Lower the Size of the Codebase)
当设计一个完整的软件应用时,我们需要考虑得很多:
-
创建(Creational Design Patterns):如何创建或者实例化对象?(Factory、Builder、Prototype、Singleton)
-
结构化(Structural Design Patterns):各个对象如何结合成一个大的实体,如何兼容未来的需要?(Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxy)
-
行为(Behavioural Design Patterns):对象之间如何联系,如何避免后续改动的影响以及减少副作用?(Chain of responsibility、Command、Interpreter、Iterator、Mediator、Memento、Observer、State、Strategy、Template Method、Visitor)
2、SOLID 设计原则
SOLID 是面向对象软件设计当中较为出名的一系列设计原则,包含有以下 5 个原则:
-
SRP – Single Responsibility Principle
-
OCP – Open/Closed Principle
-
LSP – Liskov Substitution Principle
-
ISP – Interface Segregation Principle
-
DIP – Dependency Inversion Principle
注:设计模式(Pattern)和设计原则(Principle) 是两个不同的概念。
2.1、Single Responsibility Principle (SRP)
SRP (单一功能原则),主要思想是一个类只能有一个被修改的原因(A class should have only one reason to change)。
也就是说每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class Journal {
string m_title;
vector<string> m_entries;
public:
explicit Journal(const string &title) : m_title{title} {}
void add_entries(const string &entry) {
static uint32_t count = 1;
m_entries.push_back(to_string(count++) + ": " + entry);
}
auto get_entries() const { return m_entries; }
//void save(const string &filename)
//{
// ofstream ofs(filename);
// for (auto &s : m_entries) ofs << s << endl;
//}
};
struct SavingManager {
static void save(const Journal &j, const string &filename) {
ofstream ofs(filename);
for (auto &s : j.get_entries())
ofs << s << endl;
}
};
SavingManager::save(journal, "diary.txt");
|
在上面的代码案例中,Journal
类如果同时存在两个功能(two reason to change),关联事件(add_entries)和保存(save),那么会存在以下问题:
所以根据 SRP 设计原则,把保存函数抽离,封装成另一个类,这样 Journal
类只需要关联事件,而 SavingManager
类只负责保存,这样就可以应付后面的改动,提高可维护性、更直观、解耦、可复用。
但也会存在缺点,各个类之间可能会存在联系,导致上百个类关联到一起,实际应该整合成一个类。
SRP 原则是为了减少改动的影响,所以整合功能以相同的原因(same reason)做修改,分离功能是因为以不同的原因(different reason)做修改。在做代码重构时,非常有帮助。
2.2、Open Closed Principle (OCP)
OCP (开闭原则),主要思想是软件中的对象应该对于扩展是开放的,但是对于修改是封闭的(classes should be open for extension, closed for modification)。
也就是说对一个类只能做扩展,而不能被修改。
2.2.1、反例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
enum class COLOR { RED, GREEN, BLUE };
enum class SIZE { SMALL, MEDIUM, LARGE };
struct Product {
string m_name;
COLOR m_color;
SIZE m_size;
};
using Items = vector<Product*>;
#define ALL(C) begin(C), end(C)
struct ProductFilter {
static Items by_color(Items items, const COLOR e_color) {
Items result;
for (auto &i : items)
if (i->m_color == e_color)
result.push_back(i);
return result;
}
static Items by_size(Items items, const SIZE e_size) {
Items result;
for (auto &i : items)
if (i->m_size == e_size)
result.push_back(i);
return result;
}
static Items by_size_and_color(Items items, const SIZE e_size, const COLOR e_color) {
Items result;
for (auto &i : items)
if (i->m_size == e_size && i->m_color == e_color)
result.push_back(i);
return result;
}
};
int main() {
const Items all{
new Product{"Apple", COLOR::GREEN, SIZE::SMALL},
new Product{"Tree", COLOR::GREEN, SIZE::LARGE},
new Product{"House", COLOR::BLUE, SIZE::LARGE},
};
for (auto &p : ProductFilter::by_color(all, COLOR::GREEN))
cout << p->m_name << " is green\n";
for (auto &p : ProductFilter::by_size_and_color(all, SIZE::LARGE, COLOR::GREEN))
cout << p->m_name << " is green & large\n";
return EXIT_SUCCESS;
}
|
一个产品(Product
)可能有多个属性,然后我们需要过滤出特定属性的产品。
但按照上面代码的实现,存在下面几个问题:
2.2.2、使用模版实现扩展
要实现 OCP 原则的方式,可以采用多态和模版。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
template <typename T>
struct Specification {
virtual ~Specification() = default;
virtual bool is_satisfied(T *item) const = 0;
};
struct ColorSpecification : Specification<Product> {
COLOR e_color;
ColorSpecification(COLOR e_color) : e_color(e_color) {}
bool is_satisfied(Product *item) const { return item->m_color == e_color; }
};
struct SizeSpecification : Specification<Product> {
SIZE e_size;
SizeSpecification(SIZE e_size) : e_size(e_size) {}
bool is_satisfied(Product *item) const { return item->m_size == e_size; }
};
template <typename T>
struct Filter {
virtual vector<T *> filter(vector<T *> items, const Specification<T> &spec) = 0;
};
struct BetterFilter : Filter<Product> {
vector<Product *> filter(vector<Product *> items, const Specification<Product> &spec) {
vector<Product *> result;
for (auto &p : items)
if (spec.is_satisfied(p))
result.push_back(p);
return result;
}
};
// ------------------------------------------------------------------------------------------------
BetterFilter bf;
for (auto &x : bf.filter(all, ColorSpecification(COLOR::GREEN)))
cout << x->m_name << " is green\n";
|
通过模版对外暴露接口,从而使得类可扩展,而不需要修改类的实现。
2.2.3、总结
使用 OCP 原则的方式,可以增强可扩展性、可维护性、灵活性。但实际上,一个类很难保持完全的封闭性,总会存在一些不可预见的、需要修改类实现的情况。对于可预见的,那么 OCP 原则是很不错的修改方式。
2.3、Liskov’s Substitution Principle (LSP)
LSP (里氏替换原则),主要思想是派生类对象可以在程序中代替其基类对象(Subtypes must be substitutable for their base types without altering the correctness of the program)。
以 C++ 举例来说明,就是指向基类对象的指针或引用可以被替换为派生类对象。
2.3.1、反例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
struct Rectangle {
Rectangle(const uint32_t width, const uint32_t height) : m_width{width}, m_height{height} {}
uint32_t get_width() const { return m_width; }
uint32_t get_height() const { return m_height; }
virtual void set_width(const uint32_t width) { this->m_width = width; }
virtual void set_height(const uint32_t height) { this->m_height = height; }
uint32_t area() const { return m_width * m_height; }
protected:
uint32_t m_width, m_height;
};
struct Square : Rectangle {
Square(uint32_t size) : Rectangle(size, size) {}
void set_width(const uint32_t width) override { this->m_width = m_height = width; }
void set_height(const uint32_t height) override { this->m_height = m_width = height; }
};
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
assert((w * 10) == r.area()); // Fails for Square <--------------------
}
|
在上面的代码案例中,正方形(Square
)继承于矩形(Rectangle
),在执行面积判断 process
函数时,就会出现替换错误。看似正确,实则矩形面积是以宽和高来定义的,而正方形是以长度来定义的,所以这样继承不是好的方式。
2.3.2、判断兼容
1
2
3
4
5
6
7
8
9
|
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
if (dynamic_cast<Square *>(&r) != nullptr)
assert((r.get_width() * r.get_width()) == r.area());
else
assert((w * 10) == r.area());
}
|
多加一层类型判断来进行兼容,也不是什么好的方式,实际上这并不是真正的可替换。
2.3.3、包含关系
1
2
3
4
5
6
7
8
9
|
void process(Rectangle &r) {
uint32_t w = r.get_width();
r.set_height(10);
if (r.is_square())
assert((r.get_width() * r.get_width()) == r.area());
else
assert((w * 10) == r.area());
}
|
因为矩形是包含正方形的,所以不需要创建正方形这个类,在有需要的地方做判断即可,但这也不是推荐的方式。
2.3.4、改进继承关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
struct Shape {
virtual uint32_t area() const = 0;
};
struct Rectangle : Shape {
Rectangle(const uint32_t width, const uint32_t height) : m_width{width}, m_height{height} {}
uint32_t get_width() const { return m_width; }
uint32_t get_height() const { return m_height; }
virtual void set_width(const uint32_t width) { this->m_width = width; }
virtual void set_height(const uint32_t height) { this->m_height = height; }
uint32_t area() const override { return m_width * m_height; }
private:
uint32_t m_width, m_height;
};
struct Square : Shape {
Square(uint32_t size) : m_size(size) {}
void set_size(const uint32_t size) { this->m_size = size; }
uint32_t area() const override { return m_size * m_size; }
private:
uint32_t m_size;
};
void process(Shape &s) {
// Use polymorphic behaviour only i.e. area()
}
|
通过创建一个更加包容的基类,从而使得正方形类和矩形类都纳入其中,这样就能满足可替换性。
2.3.5、总结
LSP 原则可以达到很好的兼容性、类型安全、可维护性。在面向对象设计当中,仅仅描述“IS-A”(是什么)关系是不够的,更精确的应该是描述“IS-SUBSTITUTABLE-FOR”(可被替换为)关系。
2.4、Interface Segregation Principle (ISP)
ISP (接口隔离原则),主要思想是客户不应被迫使用对其而言无用的方法或功能(Clients should not be forced to depend on interfaces that they do not use)。
也就是说,设计抽象接口应该只完成用户需要的功能,把非常庞大臃肿的接口划分成更小的和更具体的,这样客户只需要知道他们感兴趣的方法,也更方便解耦和重构。
2.4.1、反例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
struct Document;
struct IMachine {
virtual void print(Document &doc) = 0;
virtual void fax(Document &doc) = 0;
virtual void scan(Document &doc) = 0;
};
struct MultiFunctionPrinter : IMachine { // OK
void print(Document &doc) override { }
void fax(Document &doc) override { }
void scan(Document &doc) override { }
};
struct Scanner : IMachine { // Not OK
void print(Document &doc) override { /* Blank */ }
void fax(Document &doc) override { /* Blank */ }
void scan(Document &doc) override {
// Do scanning ...
}
};
|
在基类机器(IMachine
)要对文档进行操作当中,暴露了三个接口,打印(print
)、传真(fax
)、扫描(scan
),对于打印机而言是正常的,但对于扫描仪而言,它并没有打印和传真的功能,被迫把这两个接口的实现为空。
2.4.2、接口分离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/* -------------------------------- Interfaces ----------------------------- */
struct IPrinter {
virtual void print(Document &doc) = 0;
};
struct IScanner {
virtual void scan(Document &doc) = 0;
};
/* ------------------------------------------------------------------------ */
struct Printer : IPrinter {
void print(Document &doc) override;
};
struct Scanner : IScanner {
void scan(Document &doc) override;
};
struct IMachine : IPrinter, IScanner { };
struct Machine : IMachine {
IPrinter& m_printer;
IScanner& m_scanner;
Machine(IPrinter &p, IScanner &s) : printer{p}, scanner{s} { }
void print(Document &doc) override { printer.print(doc); }
void scan(Document &doc) override { scanner.scan(doc); }
};
|
和单一功能原则(SRP)比较类似,通过分离接口,提供客户感兴趣的接口。
2.4.3、总结
ISP 原则可以使得编译更快(因为如果接口签名改变,所有子类都要重编)、可复用的、可维护的。所以在设计接口类时,要设想这个类中的所有方法是否真的需要用到。
2.5、Dependency Inversion Principle (DIP)
DIP (依赖反转原则),主要思想是:
-
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口(High-level modules should not depend on low-level modules. Both should depend on abstractions)
-
抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口(Abstractions should not depend on details. Details should depend on abstractions)
高层次模块是更加抽象以及包含更多的复杂逻辑,低层次模块描述更具体的实现以及是独立的实现细节的部分。
2.5.1、反例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
enum class Relationship { parent, child, sibling };
struct Person {
string m_name;
};
struct Relationships { // Low-level <<<<<<<<<<<<-------------------------
vector<tuple<Person, Relationship, Person>> m_relations;
void add_parent_and_child(const Person &parent, const Person &child) {
m_relations.push_back({parent, Relationship::parent, child});
m_relations.push_back({child, Relationship::child, parent});
}
};
struct Research { // High-level <<<<<<<<<<<<------------------------
Research(const Relationships &relationships) {
for (auto &&[first, rel, second] : relationships.m_relations) {// Need C++17 here
if (first.m_name == "John" && rel == Relationship::parent)
cout << "John has a child called " << second.m_name << endl;
}
}
};
|
在上面的代码案例中,关系链调查,如果 Relationships
类当中的变量改变类型或者名字的话,将会导致高层次模块 Research
类出问题,而这两个类之间还存在很多的关联。
2.5.2、依赖反转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
struct RelationshipBrowser {
virtual vector<Person> find_all_children_of(const string &name) = 0;
};
struct Relationships : RelationshipBrowser { // Low-level <<<<<<<<<<<<<<<------------------------
vector<tuple<Person, Relationship, Person>> m_relations;
void add_parent_and_child(const Person &parent, const Person &child) {
m_relations.push_back({parent, Relationship::parent, child});
m_relations.push_back({child, Relationship::child, parent});
}
vector<Person> find_all_children_of(const string &name) {
vector<Person> result;
for (auto &&[first, rel, second] : m_relations) {
if (first.name == name && rel == Relationship::parent) {
result.push_back(second);
}
}
return result;
}
};
struct Research { // High-level <<<<<<<<<<<<<<<----------------------
Research(RelationshipBrowser &browser) {
for (auto &child : browser.find_all_children_of("John")) {
cout << "John has a child called " << child.name << endl;
}
};
|
我们可以创建一个抽象接口类,来把高层次模块和低层次模块连接到一起。这样其中的一些修改就不会导致另一个类出问题。
2.5.3、总结
如果转换成 DIP 原则比较困难的话,就应该先设计抽象接口类,然后实现基于抽象的高层次模块,而不能提前感知到低层次模块的实现。
DIP 原则可以带来复用性、可维护性。不要尝试混用各个类对象,应该首先使用抽象。
参考
What Is Design Pattern?:http://www.vishalchovatiya.com/what-is-design-pattern/
Single Responsibility Principle in C++:http://www.vishalchovatiya.com/single-responsibility-principle-in-cpp-solid-as-a-rock/
单一功能原则:https://zh.wikipedia.org/zh-cn/%E5%8D%95%E4%B8%80%E5%8A%9F%E8%83%BD%E5%8E%9F%E5%88%99
Open Closed Principle in C++:http://www.vishalchovatiya.com/open-closed-principle-in-cpp-solid-as-a-rock/
开闭原则:https://zh.wikipedia.org/zh-cn/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99
Liskov’s Substitution Principle in C++:http://www.vishalchovatiya.com/liskovs-substitution-principle-in-cpp-solid-as-a-rock/
里氏替换原则:https://zh.wikipedia.org/zh-cn/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99
Interface Segregation Principle in C++:http://www.vishalchovatiya.com/interface-segregation-principle-in-cpp-solid-as-a-rock/
接口隔离原则:https://zh.wikipedia.org/zh-cn/%E6%8E%A5%E5%8F%A3%E9%9A%94%E7%A6%BB%E5%8E%9F%E5%88%99
Dependency Inversion Principle in C++:http://www.vishalchovatiya.com/dependency-inversion-principle-in-cpp-solid-as-a-rock/
依赖反转原则:https://zh.wikipedia.org/zh-cn/%E4%BE%9D%E8%B5%96%E5%8F%8D%E8%BD%AC%E5%8E%9F%E5%88%99