1.书本链接:前言 · 设计模式之禅(第2版) · 看云
本文引用博客:设计模式六大原则(1):单一职责原则_割韭韭的博客-CSDN博客_单一职责原则
2.第一部分:六大设计原则
- 单一职责原则,SRP
定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。// 命令模式:顾客,服务员,厨师:https://www.cnblogs.com/chenqionghe/p/4749915.htm
动物呼吸:https://blog.csdn.net/yabay2208/article/details/7380047// 创建对象 control = new cookControl; // 顾客找服务员 cook = new cook; // 服务员准备好,厨师 // 确定对象直接的关系,聚合,组合,依赖 mealcommand = new MealCommand($cook); // 菜单准备 drinkcommand = new DrinkCommand($cook); // 菜单准备 control->addCommand($mealcommand,$drinkcommand); // 加入菜单 // 执行动作 control->callmeal(); // 顾客点餐 control->calldrink();
// iphone是包含了协议管理(dial, handup)和数据传输(chat) // 两个变化都会触发变化 class IPhone { public: virtual void dial(string number) = 0; virtual void chat(object o) = 0; virtual void handup() = 0; }; class IPhone { public: IManger m; IDataTransfer d; };
2.里氏替换原则
定义:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。2.所有引用基类的地方必须能透明地使用其子类的对象。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
做法: 里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
class Father { private: /* data */ public: collecation cal(Hashmap map) { return map.values(); } }; class son : public Father { private: /* data */ public: // 子类缩小父类的参数,不可以 // collecation cal(HashmapSon map) // 子类放大父类的参数,可以 collecation cal(map map) { return map.values(); } };
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
class Father { private: /* data */ public: long cal(Hashmap map) { return map.values(); } }; class son : public Father { private: /* data */ public: // return int < long int cal(map map) { return map.values(); } };
例子:加减扩展的例子
3.依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率
做法:依赖倒置原则的核心思想是面向接口编程
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
例子:妈妈读报纸/童话,开宝马/奔驰
依赖的三种写法:构造函数传递依赖对象、设置Setter方法声明依赖关系、接口的方法中声明依赖对象,如
private ICar car; public Driver(ICar _car){this.car = _car;}
private ICar car; public Driver(ICar _car){ this.car = _car;}
public void drive(ICar car){car.run();}
1.函数的命名不能暴露实现细节:uploadToAliyun->upload
2.封装具体的细节:aliyun特有的token.
3.抽象公共的接口。
4.接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
做法:
- 接口尽量小,是“小”是有限度的,首先就是不能违反单一职责原则,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
例子:不要定义一个通用的接口,美女何其多,观点各不同,图书查询类图特殊权限
class IPettyGril { public: virtual void goodLooking() = 0; virtual void niceFigure() = 0; virtual void greatTemperatment() = 0; }; class PettyGirl : public IPettyGril { private: /* data */ public: virtual void goodLooking() { } virtual void niceFigure() { } virtual void greatTemperatment() { } }; class AbSearcher { private: IPettyGril mGirl; public: AbSearcher(IPettyGril girl) { mGirl = girl; } protected: virtual void show(); }; class Searcher : public AbSearcher { private: IPettyGril mGirl; public: Searcher(IPettyGril girl) : AbSearcher(girl) { mGirl = girl; } protected: virtual void show() { mGirl.goodLooking(); mGirl.niceFigure(); mGirl.greatTemperatment(); } }; int main () { IPettyGril girl = new PettyGirl(); AbSearcher searcher = new Searcher(girl); searcher.show(); return 0; }
class IPettyGrilgoodLooking { public: virtual void goodLooking() = 0; }; class IPettyGrilniceFigure { public: virtual void niceFigure() = 0; }; class IPettyGrilgreatTemperatment { public: virtual void greatTemperatment() = 0; }; class PettyGirl : public IPettyGrilgoodLooking, IPettyGrilniceFigure, IPettyGrilgreatTemperatment { private: /* data */ public: virtual void goodLooking() { } virtual void niceFigure() { } virtual void greatTemperatment() { } }; class AbSearcher { private: public: AbSearcher() { } protected: virtual void show(); }; class Searcher : public AbSearcher { private: IPettyGrilgoodLooking mgoodLookingGirl; IPettyGrilniceFigure mNiceGirl; IPettyGrilgreatTemperatment mTempGril; public: Searcher(IPettyGrilgoodLooking girl, IPettyGrilniceFigure mNiceGirl, IPettyGrilgreatTemperatment mTempGril) { mGirl = girl; mNiceGirl = mNiceGirl; mTempGril = mTempGril; } protected: virtual void show() { mgoodLookingGirl.goodLooking(); mNiceGirl.niceFigure(); mTempGril.greatTemperatment(); } }; int main () { IPettyGril girl = new PettyGirl(); AbSearcher searcher = new Searcher(girl); searcher.show(); return 0; }
class IPettyGrilMind { public: virtual void mind() = 0; }; class Pretty3 : public IPettyGrilMind { private: /* data */ public: virtual void mind() { } }; int main () { IPettyGril girl = new PettyGirl(); AbSearcher searcher = new Searcher(girl); searcher.show(); IPettyGril girl = new Pretty3(); AbSearcher searcher = new Searcher(girl); searcher.show(); return 0; }
5.迪米特法则
定义:一个对象应该对其他对象保持最少的了解。最少知道原则
问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
解决方案:尽量降低类与类之间的耦合。 虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大
做法:
- 迪米特法则还有一个更简单的定义:只与直接的朋友通信,其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
class Girl { }; class GroupLeader { private: /* data */ public: GroupLeader(/* args */); void count(List<Girl> girls); } class Teacher { private: /* data */ public: void command(GroupLeader leader) { // Teacher 里面和Girls没有必要 List<Girl> girls = new List(); leader.count(); } }; int main () { GroupLeader leader = new GroupLeader(); Teacher t = new Teacher(); t->command(leader); return 0; }
class Girl { }; class GroupLeader { private: /* data */ public: GroupLeader(List<Girl> girls); void count(); } class Teacher { private: /* data */ public: void command(GroupLeader leader) { leader.count(); } }; int main () { List<Girl> girls = new List; girls.append(new Girl()); girls.append(new Girl()); GroupLeader leader = new GroupLeader(girls); Teacher t = new Teacher(); t->command(leader); return 0; }
- 尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限
class Wizzd { private: /* data */ public: int first() { } int second() { } int third() { } }; class installSoftware { private: /* data */ public: void process(Wizzd w) { if (w.first() > 0) { if (w.second() > 0) { if (w.third() < 0) { } } } } };
class Wizzd { private: /* data */ public: int first() { } int second() { } int third() { } void procee(Wizzd w) { if (w.first() > 0) { if (w.second() > 0) { if (w.third() < 0) { } } } } }; class installSoftware { private: /* data */ public: void process(Wizzd w) { w.procee(); } };
- 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中,一个类跳转两次以上才能访问到另一个类,就需要想办法进行重构了
例子:总公司经理和分公司员工的关系,体育老师,体育委员,女生,Wizard类把太多的方法暴露给InstallSoftware类
6.开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
做法:
- 抽象约束:如果再增加计算机书,增加接口和实现类,ComputerBook类必须实现IBook的三个方法,是通过IComputerBook接口传递进来的约束,也就是我们制定的IBook接口对扩展类ComputerBook产生了约束力,正是由于该约束力,BookStore类才不需要进行大量的修改,如果再增加计算机书,增加接口和实现类,接口或抽象类一旦定义,就应该立即执行,不能有修改接口的思想
- 元数据,控制模块行为,参数可以从文件中获得,也可以从数据库中获得
例子:买书和降价(不是通过修改接口,修改实现类,而是扩展一个子类),
class IBook { }; class NoveBook : public IBook { private: /* data */ public: double getPrice() { return price; } }; // 需求:新增加一个获取打折后价格的功能 // 1.增加getOffPrice()接口, 但是IBook应该是稳定的 // 2.修改getPrice(); 之前的功能degree // 3.增加一个子类OffPrice class OffNoveBook : public NoveBook { private: /* data */ public: double getPrice() { return price * 0.8; } };
3.总结
恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的)
架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。