最近学习了李建忠老师设计模式教程,感觉有一种豁然开朗的感觉。
"每一个模式描述了在我们周围不断重复发生变化的问题,以及该问题的解决方案的核心。这样,你就能一次又一次的使用该方案而不必重复的劳动"——christopher alexander
当然,设计模式的有本很著名的书,常考教程:
书名:设计模式:可复用面向对象软件的基础
当然,我不是推销书的。主要是书名很明确的给出了,软件设计模式中使用的手法,常用就是面向对象,确实,在我第一次听到面向对象的时候,我的理解是完全不懂,在我的二次学习面向对象的时候,我的理解是封装,继承,多态。而到现在,我觉得,我们的最终目的,还是编写可复用,可扩展,便于维护的软件。当然,如何把面向对象的特性运用到极致,我想还得深入去理解设计模式,不然,很多时候,即使我们用了面向对象语言编程,往往出现适得其反的效果。
这里先给出面向对象五大基本原则:
(DIP)依赖倒置原则:
-
高层模块(稳定)不应该依赖于底层模块(变化),二者都应该依赖于抽象(稳定)。
-
抽象(稳定)不应该依赖于实现细节(变化),实现细节依赖于抽象(稳定)。
我的理解:总结为,稳定的东西不应该依赖于变化的东西。在面向对象设计过程中,可能有时候,我会把一些具体实现依赖另外一些具体实现,但是我们知道,具体实现是容易变化的,如果依赖的实现发现变化,意味着上层也将发生变化。这里的变化,不是说具体的逻辑变动,而是接口变动,简单的说就是,面向对象应该面向抽象接口来实现,而不是面向具体的逻辑,这个抽象接口应该是十分稳定的,依赖于一个稳定的接口的好处,就是,我们具体实现和耦合性会比较低。这意味着我们需要很敏感的发现系统的稳定点和变化点,将稳定点变成抽象接口,把变化点变成扩展实现。
我自己想了一个坏栗子,不知道合不合适:
class Waiter //服务生 { } class Cater //计算员 { } class Guard //保安 { }
class Bank { public: vector<Waiter> waiterList; vector<Cater> caterList; vector<Guard> guardList; protect: void doWork(); } void Bank::doWord() { for(auto a in waiterList) { a.doWord(); } for(auto a in caterList) { a.doWord(); } for(auto a in guardList) { a.doWord(); }
作为伪代码,随意看看吧,我想意思很简单,首先有个员工文件,里面我定义了三种员工,然后银行定义了员工的LIst,当然,实际可能还需要添加员工犯法,dowork方法用来让他的员工工作,代码冗余问题虽然还可以解决,但是,最大的问题是,你应该发现,员工种类我们假设是需要扩展的,这种方式,就是具体依赖于具体。你可以假设如果现在多了一种员工,叫做数据库管理员,那么你对于上面的代码应该做那些修改。我想你应该,写一个类,然后,在银行类里面,添加一个数据库管理员list对象,最后,还要在员工在工作里面在添加方法。是不是很不利。
我想,好的方式是这样:
class Staff { public: void doWork()=0; void ~Staff() {} } class Waiter:public Staff //服务生 { public: void doWork(); } class Cater:public Staff //计算员 { void doWork(); } class Guard:public Staff //保安 { void doWork(); }
class Bank { public: vector<Staff *> stafflList; protect: void doWork(); } void Bank::doWord() { for(auto a in staffList) { a.doWork(); }
你现在可以想象下,如果这种情况下,老板让你添加一种新的员工,叫做数据库,管理员,那么是不是方便,你只需要,在写一个类,然后继承抽象基类staff即可。这也就是面向抽象基础编程。
开放封闭原则(OCP)
-
对扩展开放,对更改封闭。
-
类模块应该可扩展的,但是不可更改。
我的理解:开放封闭原则,是最为重要的设计原则。而后面的Liskov替换原则和合成/聚合复用原则为开放封闭原则的实现提供保证。
我这里无耻的引用百度百科的例子:
class BusyBankStaff { private BankProcess bankProc = new BankProcess(); // 定义银行员工的业务操作 public void HandleProcess(Client client) { switch (client.ClientType) { case "存款用户": bankProc.Deposit(); break; case "转账用户": bankProc.Transfer(); break; case "取款户": bankProc.DrawMoney(); break; } } }
其实这代码跟第一个原则差不多,对扩展开发怎么理解,这段代码问题主要在于,如果有新的业务出现,那么扩展,将在switch中进行修改,实际上违背了对修改封闭的原则,也就是说,如果出现新的业务,我们需要的是扩展,而不是修改之前的类。那么好的实现方法可以是这样的:
class IBankProcess { virtual void Process()=0; }
<-接口基类
class DepositProcess : IBankProcess { //IBankProcess Members public void : Process() { // 办理存款业务 } } class TransferProcess : IBankProcess { //IBankProcess Members public : void Process() { // 办理转账业务 } } class DrawMoneyProcess : IBankProcess { //IBankProcess Members public : void Process() { // 办理取款业务 } }
<-不同业务
class EasyBankStaff { private : IBankProcess *bankProc = NULL; public : void HandleProcess(Client client) { bankProc = client.CreateProcess(); bankProc.Process(); } }
<-用户切换
class BankProcess { public: void Main() { EasyBankStaff bankStaff = new EasyBankStaff(); bankStaff.HandleProcess(new Client("转账用户")); } }
处理业务,就这个方法而言,对于银行业务扩展,我们只需要定义新的类,这叫对扩展开放,而不需要修改原先类的代码,这叫对修改封闭。这样一个原则,是不是很像一个衡量标准呢。
单一职责原则(SRP)
-
一个类应该仅有一个引起它变化的原因。
-
变化的方向隐含着类的责任。
这是相对比较好理解的吧,"如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。"
一个类有且只有一个可以改变的理由。
liskov替换原则(LSP)
-
子类必须能够替换它们的基类(IS-A)。
-
继承表达类型抽象。
接口隔离原则(ISP)
-
不可以强迫客户程序依赖他们不用的方法。
-
接口应该小而完备。
几个额外的建议:
组合复用原则(carp):
优先使用对象组合,而不是类继承
-
类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。
-
继承在某种程度上破坏了封装性,之类父类耦合度高。
-
而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低
封闭变化点
使用封装来创建对象之间的分界层。让设计者可以在变化一侧进行修改,而不会影响另一侧产生不良影响,从而实现松耦合。
针对接口编程
针对接口编程,而不是针对实现编程
1.不将变量类型声明为某个特定的具体类,而是声明为某个接口
2.客户程序不需要获知对象的具体类型,只需要知道对象所具有的接口。
3.减少系统中各部分的依赖关系,从而实现“高内聚,松耦合”的设计类型。
对设计模式按照目的分类可以分为以下三种:
1.创建型:与对象创建有关。
2.结构型:处理类或对象的组合。
3.行为型:模式对类或者对象怎么交互和分配职责进行描述。
按照其作用范围又可分为类模式和对象模式 ,前者主要处理类和子类的关系,通过继承建立,具有静态,在编译时刻就稳定下来。后者处理对象之间的关系,这些关系可能是运行时刻变化的,具有动态性。
具体模式可以参考书上的表格:
如何使用设计模式:
我想大概是要达到对症下药的层次吧,在此之前,我们更应该关注原则。