一、开放封闭原则
OCP,Open For Extension Closed For Modification Principle,简称开闭原则。开闭原则是指软件实体是可以扩展的,但是不可修改。也就是说,模块和函数是对扩展(提供方)开放的,对修改(使用方)关闭的,对于一个新的需求,对软件的改动应该是通过增加代码来实现的,而不是通过改动代码实现的。开闭原则是面向对象的核心,是最基础、最重要的设计原则,开发过程中,应该把可能会频繁变动的部分抽象出来,当需要变动时,只需要去实现抽象即可,也就是面向抽象编程。对于C++类来说,对类的改动是通过增加代码实现的,而不是修改代码实现的,通过虚基类的继承和虚函数的实现来完成一个类功能的扩充,这也是多态在设计模式中重要地位的体现。
举例来说,假如我们要创建一个迪迦奥特曼类,迪迦奥特曼有三种形态,最简单的方式就是在一个类中实现,每次都在类中增删查改
1. //第一层次:直接修改类来实现增加功能 2. class TigaUltraman1 3. { 4. public: 5. void RedForm() //红色形态 6. { 7. cout << "红色形态的迪迦奥特曼" << endl; 8. } 9. 10. void BlueForm() //蓝色形态 11. { 12. cout << "蓝色形态的迪迦奥特曼" << endl; 13. } 14. 15. void CompreForm() //综合形态 16. { 17. cout << "综合形态的迪迦奥特曼" << endl; 18. } 19. }; 20. 21. int main() 22. { 23. //1. 直接修改迪迦奥特曼类来增加不同形态 24. TigaUltraman1* u1 = new TigaUltraman1; 25. u1->RedForm(); 26. u1->BlueForm(); 27. u1->CompreForm(); 28. delete u1; 29. cout << endl; 30. }
那么这样的话显然不满足开闭原则,实际上应该定义一个抽象类,这个抽象类只提供一个统一的接口,当需要增加功能时只需要继承这个抽象类,并实现抽象方法即可。
1. //第二层次,创建一个抽象类,通过继承实现形态扩充 2. class TigaUltraman //迪迦奥特曼抽象类 3. { 4. public: 5. virtual void uForm() = 0; 6. }; 7. 8. class RedTigaUltraman : public TigaUltraman 9. { 10. public: 11. virtual void uForm() 12. { 13. cout << "红色形态的迪迦奥特曼" << endl; 14. } 15. }; 16. 17. class BlueTigaUltraman : public TigaUltraman 18. { 19. public: 20. virtual void uForm() 21. { 22. cout << "蓝色形态的迪迦奥特曼" << endl; 23. } 24. }; 25. 26. class CompreTigaUltraman : public TigaUltraman 27. { 28. public: 29. virtual void uForm() 30. { 31. cout << "综合形态的迪迦奥特曼" << endl; 32. } 33. }; 34. 35. int main() 36. { 37. //2. 使用继承 38. TigaUltraman* u2 = NULL; 39. u2 = new RedTigaUltraman; 40. u2->uForm(); 41. delete u2; 42. u2 = new BlueTigaUltraman; 43. u2->uForm(); 44. delete u2; 45. u2 = new CompreTigaUltraman; 46. u2->uForm(); 47. delete u2; 48. cout << endl; 49. }
更进一步,我们可以提供一个接口,直接调用接口,把各种实现类的对象传递给抽象类的指针并产生多态。即使是子类的子类也可以传给抽象类产生多态。
1. //第三层次:使用一个统一接口 2. void get_form(TigaUltraman* u) 3. { 4. u->uForm(); 5. } 6. 7. //增加功能,进化版的综合形态 8. class EvolutionCompreTigaUltraman : public CompreTigaUltraman 9. { 10. public: 11. virtual void uForm() 12. { 13. cout << "进化版的综合形态的迪迦奥特曼" << endl; 14. } 15. }; 16. 17. int main() 18. { 19. //3. 使用统一接口 20. u2 = new RedTigaUltraman; 21. get_form(u2); 22. delete u2; 23. BlueTigaUltraman* u3 = new BlueTigaUltraman; 24. get_form(u3); 25. delete u3; 26. CompreTigaUltraman u4; 27. get_form(&u4); 28. cout << endl; 29. 30. u2 = new EvolutionCompreTigaUltraman; 31. get_form(u2); 32. delete u2; 33. cout << endl; 34. }
二、单一职责原则
SRP,Single Responsibility Principle,单一职责原则。对类来说,类的职责应该是单一的,一个类只能对外提供一种功能。换句话说,变动这个类的理由或动机只能有一个,如果第二个改动类的理由,就不是单一职责。单一职责相当于降低了各种职责的耦合度,如果一个类负责多个职责,那么改动类的某一职责时,可能会影响到类行使其他职责的能力。
三、依赖倒置原则
DIP,Dependence Inversion Principle,依赖倒置原则。抽象不应该依赖于细节,细节应该依赖于抽象。换句话说,依赖于抽象接口,而不是依赖于具体的类的实现,也就是面向抽象接口编程。依赖倒置原则是面向对象编程的标志,在具体软件设计时,上层模块不应该依赖于底层模块,底层模块更不应该依赖上层模块,而是上层模块和底层模块都向中间靠拢,共同依赖于二者中间的抽象接口层。整个软件程序设计的依赖关系应该终止于抽象接口层,上层和底层互不关心,甚至使用什么编程语言都不关心。抽象接口层提供一个标准或者协议,它对上提供访问的接口,对下提供实现的标准,抽象接口层本身不执行任何操作,具体的功能由它的实现去完成。
举例来说,假如我们要组装一台电脑,现在要选择硬盘、内存、屏幕。那么电脑类要集成硬盘、内存、屏幕这些组件,但是我们希望电脑类和组件类之间不应该是相互依赖的关系,我们就可以直接给出一套接口,各个组件厂商只要实现这些抽象接口就可以装入我们的电脑中。
1. //各个组件的抽象类 2. class DiskInterface 3. { 4. public: 5. virtual void infomation() = 0; 6. }; 7. 8. class MemoryInterface 9. { 10. public: 11. virtual void infomation() = 0; 12. }; 13. 14. class ScreenInterface 15. { 16. public: 17. virtual void infomation() = 0; 18. };
电脑类定义如下
1. class Computer 2. { 3. public: 4. Computer(DiskInterface* disk, MemoryInterface* memory, ScreenInterface* screen) 5. { 6. this->disk = disk; 7. this->memory = memory; 8. this->screen = screen; 9. } 10. public: 11. void get_information() 12. { 13. this->disk->infomation(); 14. this->memory->infomation(); 15. this->screen->infomation(); 16. } 17. private: 18. DiskInterface* disk; //使用指针而不能使用变量 19. MemoryInterface* memory; 20. ScreenInterface* screen; 21. };
各个厂商根据组件的抽象类去实现,来入围电脑类
1. //各厂商直接继承抽象类,来实现 2. class InterDisk : public DiskInterface 3. { 4. public: 5. virtual void infomation() 6. { 7. cout << "因特尔硬盘" << endl; 8. } 9. }; 10. 11. class WDMemory : public MemoryInterface 12. { 13. public: 14. virtual void infomation() 15. { 16. cout << "西部数据的内存条" << endl; 17. } 18. }; 19. 20. class HPScreen : public ScreenInterface 21. { 22. public: 23. virtual void infomation() 24. { 25. cout << "惠普的屏幕" << endl; 26. } 27. };
这样组件类和电脑类都依赖于抽象接口层,两者都向接口层靠近,这就是面向接口编程,也就是我们的依赖倒置原则。我们直接把各个组件的实现类定义对象并传到电脑类中即可
1. { 2. InterDisk* idisk = new InterDisk; 3. WDMemory* wdmem = new WDMemory; 4. HPScreen* hpscr = new HPScreen; 5. SamScreen* samscr = new SamScreen; 6. 7. Computer* c1 = new Computer(idisk, wdmem, hpscr); //使用惠普的屏幕 8. c1->get_information(); 9. 10. delete c1; 11. delete samscr; 12. delete hpscr; 13. delete wdmem; 14. delete idisk; 15. }
假如后来,三星也想入围这个电脑,那么三星直接去实现屏幕的抽象类即可
1. class SamScreen : public ScreenInterface 2. { 3. public: 4. virtual void infomation() 5. { 6. cout << "三星的屏幕" << endl; 7. } 8. };
我们再直接把三星的屏幕传入电脑类即可
1. { 2. Computer* c2 = new Computer(idisk, wdmem, samscr); //使用三星屏幕 3. c2->get_information(); 4. }
这就是面向接口编程。
四、接口隔离原则
ISP,Interface Segegation Principle,接口隔离原则。一个接口对外只提供一种功能,不同功能应该通过不同的接口提供,而不是把多种功能都封装到一个接口中,否则的话,可能有的客户只需要功能A不需要功能B,但是提供A功能的接口内还封装了功能B,这就造成了客户被迫依赖某种他们不需要的功能。
五、里氏替换原则
LSP,Liskov Substitution Principle,里氏替换原则。任何地方出现的抽象类,都可以使用该抽象类的实现类来代替,这其实类似于C++中的类型兼容性原则,所有基类出现的地方都可以用子类对象来代替。实际上,继承增强了类与类之间的耦合性,在继承中应该尽量不要重写(覆盖)基类中的非抽象方法,子类可以有自己的方法,但是不能重新定义或修改基类的方法,否则如果基类中发生改变,所有的子类都可能受影响,应该尽量使用组合或者聚合而不是继承。其实,准确来说,应该是尽量不要继承可实例化的类(非抽象类),而应该是从抽象类中继承。
1. class parent 2. { 3. public: 4. virtual void function1() = 0; 5. void function2() 6. { 7. cout << "parent function 2" << endl; 8. } 9. }; 10. 11. class child1 : public parent 12. { 13. public: 14. virtual void function1() 15. { 16. cout << "function 1" << endl; 17. } 18. void function2() 19. { 20. cout << "child function 2" << endl; 21. } 22. };
举例来说,应该去实现function1()这样的抽象方法,而不能去覆盖基类的方法function2()。当子类覆盖或实现基类的方法时,方法的前置条件(形参)要比基类方法的输入参数宽松;当子类实现父类的抽象方法时,方法的后置条件(方法返回值)要比基类更严格。
六、合成复用原则
CARP,Composite/Aggregate Reuse Principle,优先使用对象组合而不是继承原则。使用继承的时候,基类的变化可能会影响到子类,而如果使用组合/聚合就可以降低这种依赖关系。组合/聚合降低了类与类之间的耦合性,一个类的变化对另一个类的影响相对较小,如果要使用继承,必须要遵守里氏替换原则。
七、迪米特法则
LOD,Law of Demeter,迪米特法则,也叫做最少知道原则(The Least Knowledge Principle)。一个类对于其它类知道的越少越好,只和朋友说话,不要和陌生人说话,这里的朋友是指作为成员变量、方法输入输出参数的类(朋友类),如果是出现在一个方法内部的类就不是朋友类。迪米特法则降低了耦合度,各个模块之间通过一个接口来实现调用,而模块之间不需要知道对方的内部实现逻辑,并且一个模块的内部改变也不会影响到另一个模块(黑盒原理)。如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
朋友圈确定:
- 当前对象本身;
- 以参量形式传入到当前对象方法中的对象;
- 当前对象的实例变量直接引用的对象;
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素都是朋友;
- 当前对象所创建的对象;
满足条件之一就是朋友,否则就是陌生人。