七种设计原则(一) 开闭原则(设计模式的基石)

面向对象和面向过程的区别:

面向过程:

吃 (狗.屎)

下雨了,我打开了雨伞.

面向对象:

狗 吃 (屎)

属性:我 雨伞 雨
动作:我打开雨伞

面向过程强调的是一件事情“该怎么做” 强调一次成型,是连续性的。比如制造一台高达,面向过程更像是流水线的形式。通过制定一系列规则来完成高达的拼装。

面向对象是一件事情“该让谁做”,然后那个谁就是对象。比如制造一台高达,面对对象更像是模块化的形式,四肢和头都可以作为那个"对象",然后拼装起来。

前辈们写的:

a) 认识问题角度:面向过程,死物受规则被动操控;面向对象,活物主动交互。

b) 解决问题模块:面向过程,函数;面向对象,对象。

c) 解决问题中心角度:面向过程,Hwo,“如何做?”(流程封装为函数,“如何”就是过程,谁来做仅是参数);面向对象,Who,“谁来做?”(“谁”就是对象,如何做是他自己的操作,多个对象协同完成任务)。

d) 解决问题步骤角度:面向过程,先具体逻辑细节,后抽象问题整体;面向对象,先抽象问题整体,后具体逻辑细节。

e) 数传递角度:面向过程,参数或全局变量;面向对象,方法。

f) 关系角度:面向过程,找不到对象;面向对象,可找到过程。

g) 复用层次角度:面向过程,方法层复用;面向对象,对象层复用。

h) 新概念角度:面向过程,句柄;面向对象,构造&析构。
链接:https://www.zhihu.com/question/19701980/answer/22817355
来源:知乎

内涵和外延

说到面向对象,一定要理解两个逻辑学中的概念:内涵和外延   (比较抽象)

内涵:概念中所反映的事物的特有属性
外延:具有概念所反映的特有属性的所有事物

打个比方,你面前有一些梨子、苹果、香蕉、菠萝,它们虽然样子不同,但它们都富含"水分",以及它们都是植物的"果实",我们抓住它们这两个特征,就把它们简化叫"水果"。当我们面对这堆东西,脑海中对这两个特征有一种模糊的感觉,为了把这种感觉说出来,我们用了一个汉语词汇"水果",不过我们也可以用英语词汇"fruit",用什么样的词汇不重要,重要的是这个词汇代表了一种我们对这两种特征的总结。
除了水果以外,我们对很多东西都有这样的总结,这些总结彼此不同,但它们又有两个共同特征:"特点的总结"、"能和其他总结区别"。我们就把对这些总结的总结取个了名字,叫"概念"。
再拿"水果"这个概念来说,"富含水分"和"植物果实"两个特征,是被"水果"一词包含在内了的,我们叫它"内涵",当把这个内涵发散出去,对应到具体的例子:苹果梨子香蕉菠萝,这里的具体例子,就是"水果"这个概念的"外延"。

面向对象的角度来说,内涵就是类的定义,外延就是类的实例

链接:https://www.zhihu.com/question/22267682/answer/134411093
来源:知乎


理解了上面的概念我们再来学习设计模式效果会更佳~

开闭原则(设计模式的基石):

开闭原则是面向对象中可复用设计的基石,是面向对象中最重要的原则之一。

1.定义

开闭原则强调一个软件实体(如:类,模块和函数)应该对外扩展开放,对修改关闭

对外扩展开放:模块对外扩展开放,意味着需求变化时,可以对模块扩展,使其具有改变的新行为。

模块通过扩展应对需求的变化。

对修改关闭:模块对修改关闭,表示当需求变化时,关闭对模块源码的修改,当然这里的“关闭”是尽可能不修改的意思,尽量在不修改源代码的基础上面扩展组件。

 

2.问题和解决方案

一个软件产品在声明周期内都会发生变化的,既然变化是一个事实,我们就应该在设计时尽量适应变化。以提高项目的稳定性和灵活性。开闭原则告诉我们应该尽量通过扩展软件实体的行为来完成新的变化,而不是通过修改现有代码来完成。

比如支付接口,设计时 支付流程 只有一套,然后在这一套流程的基础上扩展出来,当和某一家银行合作,就新添加一个实现,当和某一家银行终止合作时,就停用该实现。不用更改支付流程这个模块。这样在我们有新的需求变更时,是不需要修改支付模块的(修改源代码的风险很大,你可能看不懂别人的代码),只需要在源代码的基础上进行扩展(因为源代码已经经过成千上万次的访问测试,是正确的)。这也是我们常说的面向接口编程。先定义一个抽象类,然后抽象去界定扩展。

再比如 张全蛋开了一家书店 类图如下:

这里写图片描述

BookStore 指的是张全蛋的书店

IBook 定义了书籍的三个属性:名称、价格和作者

NovelBook 是一个具体的实现类,所有小说书籍的总称。
 

IBook接口

public interface IBook {

//书籍有名称 public String getName();

//书籍有售价 public int getPrice();

//书籍有作者 public String getAuthor();

}

public class NovelBook implements IBook { 、

//书籍名称 private String name;

//书籍的价格 private int price;

//书籍的作者 private String author;

//通过构造函数传递书籍数据

public NovelBook(String _name,int _price,String _author){

this.name = _name;

this.price = _price;

this.author = _author;

}

//获得作者是谁

public String getAuthor() {

return this.author;

}

//书籍叫什么名字

public String getName() {

return this.name;

}

//获得书籍的价格

public int getPrice() {

return this.price;

}

}

张全蛋书店是怎么销售书籍的:

public class BookStore {

private final static ArrayList<IBook> bookList = new ArrayList<IBook>();

   //静态模块初始化,项目中一般是从持久层初始化产生

   static{

    bookList.add(new NovelBook("天龙八部",3200,"金庸"));

    bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));

    bookList.add(new NovelBook("悲惨世界",3500,"雨果"));

    bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生"));

    }

     //模拟书店买书

     public static void main(String[] args) {

        NumberFormat formatter = NumberFormat.getCurrencyInstance();

        formatter.setMaximumFractionDigits(2);

        System.out.println("------------书店买出去的书籍记录如下:---------------------");

        for(IBook book:bookList){

        System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + book.getAuthor()+ "\t书籍价格:" + formatter.format(book.getPrice()/100.0)+"元");

        }

    }

}

------------书店买出去的书籍记录如下:---------------------

书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥32.00元

书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥56.00元

书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥35.00元

书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥43.00元

到这里 张全蛋的书店上线了,全蛋很开心,项目完成了。

过了一段时间,全蛋的书店很少有人光顾。全蛋打算打折出售这批书。对于已经运行的项目来说这就是一个新的需求。

我们有三种方法来完成全蛋的要求:

修改接口:

在IBook上新增一个方法getOffPrice(); 在 IBook 上新增加一个方法 getOffPrice(), 专门进行打折处理, 所有的实现类实现该方法。但是这样修改的后果就是实现类 NovelBook 要修改,BookStore 中的main方法也修改, 同时 IBook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,——因此,该方案否定。 

修改实现类:

修改 NovelBook 类中的方法,直接在 getPrice()中实现打折处理,好办法,我相信大家在项目中经常使用的就是这样办法,通过 class 文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。——因此,该方案也不是一个最优的方案。
 

通过扩展实现变化:

增加一个子类 OffNovelBook,覆写 getPrice 方法,高层次的模块(也就是 static静态模块区)通过 OffNovelBook 类产生新的对象,完成对业务变化开发任务。——好办法,修改也少,风险也小,我们来看类图: 

这里写图片描述
OffNovelBook 类继承了NovelBook,并覆写了 getPrice 方法,不修改原有的代码。我们来看新增加的子类 OffNovelBook:

public class OffNovelBook extends NovelBook {

public OffNovelBook(String _name,int _price,String _author){

super(_name,_price,_author);

}

//覆写销售价格

    @Override

    public int getPrice(){

    //原价

    int selfPrice = super.getPrice();

    // 打8折

    int offPrice=0;

    offPrice = selfPrice * 80 /100;

    return offPrice;

    }

}

很简单,仅仅覆写了 getPrice 方法.

public class BookStore {

private final static ArrayList<IBook> bookList = new ArrayList<IBook>();

//静态模块初始化,项目中一般是从持久层初始化产生

static{

bookList.add(new OffNovelBook("天龙八部",3200,"金庸"));

bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果"));

bookList.add(new OffNovelBook("悲惨世界",3500,"雨果"));

bookList.add(new OffNovelBook("金瓶梅",4300,"兰陵笑笑生"));

}

//模拟书店买书

public static void main(String[] args) {

NumberFormat formatter = NumberFormat.getCurrencyInstance();

formatter.setMaximumFractionDigits(2);

System.out.println("------------书店买出去的书籍记录如下:---------------------");

for(IBook book:bookList){

System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + book.getAuthor()+ "\t书籍价格:" + formatter.format(book.getPrice()/100.0)+"元");

}

}

}

我们只修改了静态模块初始化部分,其他的部分没有任何改动,看运行结果:

------------书店买出去的书籍记录如下:---------------------

书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.60元

书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.40元

书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元

书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥38.70元

通过扩展完成了打折的业务,满足了全蛋的要求。

 

3.归纳变化

逻辑变化:

只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是a*b+c,现在要求a*b*c,可能通过修改原有类中的方法方式来完成,前提条件是所有依赖或关联类都按此相同逻辑处理。

子模块变化:

一个模块变化,会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的。

可见视图变化:

可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应,如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是六列,突然有一天要增加一列,而且这一列要跨度N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但是我们还是可以通过扩展来完成变化,这就依赖我们原有的设计是否灵活。

 

4.优点:开闭原则提高了系统的可维护性和代码的重用性

可复用性好。

我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。

可维护性好。

由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。

 

5.如何使用开闭原则:

写代码之前一定要多想,多考虑。要对可能扩展的需求有前瞻性和预见性(此处需要经验...)

实现开闭原则的关键就在于“抽象”。把系统的所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征。作为系统设计的抽象层,要预见所有可能的扩展,从而使得在任何扩展情况下,系统的抽象底层不需修改;同时,由于可以从抽象底层导出一个或多个新的具体实现,可以改变系统的行为,因此系统设计对扩展是开放的。

我们在软件开发的过程中,一直都是提倡需求导向的。这就要求我们在设计的时候,要非常清楚地了解用户需求,判断需求中包含的可能的变化,从而明确在什么情况下使用开闭原则。

关于系统可变的部分,还有一个更具体的对可变性封装原则(Principle of Encapsulation of Variation, EVP),它从软件工程实现的角度对开闭原则进行了进一步的解释。EVP要求在做系统设计的时候,对系统所有可能发生变化的部分进行评估和分类,每一个可变的因素都单独进行封装。

我们在实际开发过程的设计开始阶段,就要罗列出来系统所有可能的行为,并把这些行为加入到抽象底层,根本就是不可能的,这么去做也是不经济的。因此我们应该现实的接受修改拥抱变化,使我们的代码可以对扩展开放,对修改关闭。

尽量不修改原来的代码

除非是修改原来代码中的错误,否则尽量不要去修改原来的代码,但是也有例外,比如扩展了底层模块,高层模块还是需要发生一些变化的,不然低层模块的扩展就是没有任何意义的代码片段;

以抽象代替实现

这也是我们一直所说的面向接口编程,当然这里的抽象并不仅仅是指接口,还可以是抽象类;

以抽象隔离变化

首先,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化,既然不可能完全封闭,那么设计人员必须对于其他设计的模块应该对哪种变化封闭进行选择,他必须猜测出最有可能发生的变化种类,然后构造抽象来隔离变化,其次,我们并没有未卜先知的能力,所以在最初编写代码时可以假设变化不会发生,但是当变化发生时,我们就需要去创建抽象来隔离以后发生的同类的变化;

抽象层设计到整个项目的架构,因此抽象层需要尽量保持稳定,一旦确定就不要轻易修改;

避免不合理的抽象

开闭原则需要使用抽象,但是过度的抽象或者说不合理的抽象同样会带来很大的问题,因此抽象应该做到合理的抽象;

其他设计原则是实现开闭原则的一种手段

其中单一原则要求做到类的职责单一,里式替换原则要求不能破坏继承体系,依赖倒置原则要求我们要面向接口编程,接口隔离原则要求做到接口要精简单一,迪米特法则则是要求做到降低耦合度,如果遵循了前面的五个法则,那么自然的也就做到了开闭原则,因此说开闭原则是设计原则的总纲

6.总结:

开闭原则:用抽象构建框架,用实现扩展细节。 

感谢百度百科~

看只有一丢丢作用,还是要多写,多用,多总结,多思考才能体会其中的奥秘。 小伙子当你看到这里说明你已经深得朕的真传~

链接自http://blog.csdn.net/zhengzhb/article/details/7296944

看完这些前辈写的,感觉收获满满~    还是要多写,多用才能体会其中的奥秘。

转载于:https://my.oschina.net/u/3701483/blog/1574779

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值