什么是设计模式?
设计模式是对一类问题的具有特定效果的解决方案模板。设计模式有四个关键:名称,对应问题,具体效果,解决方案。
设计原则是在编程过程中为了保持项目代码可维护性而提出的抽象性较高的原则。
设计原则有:
- 单一职责原则(SRP:Single responsibility principle):一个类应该只有一个发生变化的原因,即一个类只负责一项职责。核心是解耦,增强内聚性
- 开闭原则(OCP :Open Closed Principle):代码的设计应该对扩展开发,对修改封闭,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
- 里氏替换原则(LSP:Liskov Substitution Principle):基类适用的,子类一定适用,子类可以扩展基类的功能,但是不能改变基类的原有功能
- 依赖倒置原则(DIP :Dependence Inversion Principle):依赖抽象,而不是依赖细节,即面向接口编程,与具体的实现无关。
- 迪米特原则:一个对象应该对其他类尽可能少的了解
- 接口隔离原则:使用多个专门的接口,而不是一个单一的大的接口
- 组合/聚合复用原则:尽量使用组合/聚合,而不是继承
具体每个原则的例子如下:
单一职责原则
// 一个员工既负责采购和销售,又负责仓库管理,这样设计就不好,要变动很麻烦
class Worker {
public:
void buy() {
std::cout << "Worker buy" << std::endl;
money -= 100;
goods += 1;
}
void sell() {
std::cout << "Worker sell" << std::endl;
money += 100;
goods -= 1;
}
void manage() {
std::cout << "Worker manage" << std::endl;
}
private:
int money;
int goods;
};
// 把仓库单独写成一个类,worker通过调用仓库的方法来实现货物管理
class warehouse {
public:
void goods_in() {
std::cout << "warehouse goods_in" << std::endl;
goods += 1;
}
void goods_out() {
std::cout << "warehouse goods_out" << std::endl;
goods -= 1;
}
private:
int goods;
};
// 如果再进一步,还可以把money也独立出去,搞个财务部,worker只负责具体行为
class Worker2 {
public:
Worker2(warehouse* w) : w(w) {}
void buy() {
std::cout << "Worker buy" << std::endl;
w->goods_out();
money -= 100;
}
void sell() {
std::cout << "Worker sell" << std::endl;
w->goods_in();
money += 100;
}
void manage() {
std::cout << "Worker manage" << std::endl;
}
private:
int money;
warehouse* w;
};
开闭原则
// 根据单一职责原则,将打折单独做成一个类,但是如果要切换打折方案,就要修改源代码
class Discount {
public:
int discount(int price) {
return price * 0.8;
}
};
class Worker{
public:
void buy();
void sell(){
std::cout << "worker sell" << std::endl;
money += discount->discount(100);
};
void manage();
private:
int money;
Discount* discount;
};
可以选择把打折类做成一个基类,不同的打折方案,实现不同的子类:
class Discount {
public:
virtual int discount(int price) = 0;
};
class DiscountA : public Discount {
public:
int discount(int price) {
return price * discount_rate;
}
private:
int discount_rate;
};
class DiscountB : public Discount {
public:
int discount(int price) {
return price > threshold ? price - discount_num : price;
}
private:
int threshold;
int discount_num;
};
里氏替换原则
里氏原则是实现开闭原则的重要方式之一。
class Worker {
public:
virtual void buy() = 0;
virtual void sell() = 0;
int manage() {
std::cout << "Worker manage" << std::endl;
return worker_id;
}
private:
int worker_id;
};
// 重写了基类的manage方法,且由原来的返回工号变成了返回学校id,这就不好,很可能会出大乱子
// 如果实习生没有工号,那解决办法应该是给分配工号后再继承,或者不要继承于worker并将返回学校id的函数改个名字
class Trainee : public Worker {
public:
int manage() {
std::cout << "Trainee manage" << std::endl;
return school_id;
}
private:
int school_id;
};
依赖倒置原则
其实上面已经体现了,将discount类做成基类,然后将每个具体的折扣方案类继承于基类,销售方法依赖的是的discount->discount(price)
,而不是具体的某一个discount类。
即,依赖倒置可以通过多态来实现
下面给出依赖的另一种情况,即抽象不应依赖细节,
// 赠送样品,下面这种做法就不好,依赖于sell的实现,如果sell并不是卖100呢?
// 最佳办法应该是直接从仓库out一个商品,
class WorkerA : Worker{
public:
void gift() {
std::cout << "Worker gift" << std::endl;
sell();
money -= 100;
}
private:
int money;
};
迪米特原则
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
- 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限
- 在类的设计上,只要有可能,一个类型应当设计成不变类
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)
- 谨慎使用序列化(Serializable)功能:当通过序列化进行对象传输的时候,如果对象修改了属性的访问权限,而传输的另一方没有进行同步修改,则会报序列化失败
例如顾客只需要跟商店提出购买请求即可,不需要知道是具体谁卖给自己东西的。:
class Worker {
public:
virtual void buy() = 0;
virtual void manage() = 0;
private:
int worker_id;
};
class Sellable {
public:
virtual void sell() = 0;
};
class SellWorker : Worker,Sellable{
public:
void sell() {
std::cout << "Worker sell" << std::endl;
money += 100;
}
private:
int money;
};
class Shop : Sellable {
public:
void sell() {
std::cout << "Shop sell" << std::endl;
for(int i = 0; i < 100; i++) {
if(sell_worker[i] != nullptr) {
sell_worker[i]->sell();
break;
}
}
}
private:
SellWorker* sell_worker[100];
};
class customer {
public:
void buy(Sellable* s) {
s->sell();
}
};
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
接口隔离原则
接口的职责也应该单一,接口不要太过于臃肿,依赖者应该只依赖自己需要的接口,例如顾客只需要依赖sell接口即可,而不需要依赖整个worker所有接口,那么可以尝试把sell单独拿出来做成一个接口基类,使得具体的worker继承sell:
class Sellable {
public:
virtual void sell() = 0;
};
class Worker {
public:
virtual void buy() = 0;
virtual void manage() = 0;
private:
int worker_id;
};
class WorkerA : Worker,Sellable{
public:
void sell() {
std::cout << "Worker sell" << std::endl;
money += 100;
}
private:
int money;
};
class customer {
public:
void buy(Sellable* s) {
s->sell();
}
};
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
组合/聚合原则
尽量使用组合,而非继承,Rust中对这点做的就非常极致,压根儿就没有继承,连方法和数据都是组合形成的。
class Warehouse {
public:
void goods_in() {
std::cout << "warehouse goods_in" << std::endl;
goods += 1;
}
void goods_out() {
std::cout << "warehouse goods_out" << std::endl;
goods -= 1;
}
private:
int goods;
};
class Finance{
public:
void cost(int cost){
money -= cost;
}
void income(int income){
money += income;
}
private:
int money;
};
class SellRole {
public:
void sell(){
std::cout << "worker sell" << std::endl;
finance->income(100);
};
private:
Finance* finance;
Warehouse* warehouse;
};
class BuyRole {
public:
void buy(){
std::cout << "worker buy" << std::endl;
finance->cost(100);
}
private:
Finance* finance;
Warehouse* warehouse;
};
class ManageRole {
public:
void manage(){
std::cout << "worker manage" << std::endl;
}
private:
Finance* finance;
Warehouse* warehouse;
};
// 这样就可以实现一个员工可以拥有多个身份
// 这跟单一职责并不冲突,单一职责体现在每个任务的具体实现还是由各自类实现的。
class Worker {
public:
void buy(){
buy_role->buy();
}
void sell(){
sell_role->sell();
}
void manage(){
manage_role->manage();
}
private:
BuyRole* buy_role;
SellRole* sell_role;
ManageRole* manage_role;
};
这个例子不太好,因为这个其实用多重继承接口类会更好一些。总之,组合适用场景是has-a的关系,继承适用场景是is-a的关系。这个例子中,两种关系都说的过去,所以用那种都可以,但是两者的优缺点是不同的:
- 组合这种复用是黑箱复用,依赖更少,实现起来更自由
- 组合会多出来很多对象需要管理
- 继承时,新的实现比较容易,因为基类的大部分功能都可以通过继承自动的进入子类
- 继承时,如果基类类的实现发生改变,那么子类的实现也不得不发生改变