设计模式
第1章 代码无错就是优?—代码无错未必优—简单工厂模式
1.1 面试受挫
1.2 初学者代码毛病
1.3 代码规范
1.4 面向对象编程
所有编程初学者都会有这样的问题。就是碰到问题就直觉地用计算机能够理解的逻辑来描述和表达待解决的问题及具体的求解过程。这其实是用计算机的方式去思考,比如这个计算器程序,先要求输入两个数和运算符号,然后根据运算符号判断选择如何运算,得到结果,这本身没错,但这样的思维却使得我们的程序只为满足实现当前的需求,程序不容易维护,不容易扩展,更不容易复用。从而达不到高质量代码的要求。
1.5 活字印刷,面向对象(故事请见书)
第一,要改,只需要改要改的字,此为可维护;
第二,这些字并非用完这次就无用,完全可以在后来的印刷中重复使用,此乃可复用;
第三,此诗若要加字,只需要另刻字加入即可,这是可扩展;
第四,字的排列其实可能是竖排可能是横排,此时只需将活字移动就可做到满足排列需求,此是灵活性好;
而在活字印刷术出现之前,上面的四种特性都无法满足,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完这本书后,此版已无任何可再利用价值。
1.6 面向对象的好处
学习了面向对象的分析设计编程思想,开始考虑通过封装、继续、多态把程序的耦合度降低,传统印刷术的问题就在于所有的字都刻在同一版面上造成耦合度太高所致,开始用设计模式使得程序更加灵活,容易修改,并且易于复用。
1.7 复制 VS 复用
有人说初级程序员的工作就是ctrl+c和ctrl+v,这其实是非常不好的编码习惯,因为当你的代码中重复的代码多到一定程序,维护的时候,可能就是一场灾难。越大的系统,这种方式带来的问题越严重,编程有一原则,就是用尽可能的办法去避免重复。
1.8 业务的封装
准确地说,就是让业务逻辑与界面逻辑分开,让它们之间的耦合度下降,只有分离开,可以达到容易维护或扩展。
1.9 紧耦合 VS 松耦合
应该把加减乘除等运算分离,修改其中一个不影响另外的几个,增加运算算法也不影响其他代码。
1.10 简单工厂模式
现在的问题其实就是如何去实例化对象的问题,简单工厂模式,也就是说,到底要实例化谁,将来会不会增加实例化的对象,比如增加开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造实例的过程,这就是工厂类。
你需要输入运算符号,工厂就实例化出合适的对象,通过多态,返回父类的方式实现了计算器的结果。
1.11 UML类图
类图分三层,第一层显示类的名字,如果是抽象类,则就用斜体显示,第二层是类的特性,通常就是字段和属性,第三层是类的操作,通常是方法或行为。注意前面的符号,‘+’表示public,'-'表示private,'#'表示protected
接口图,与类图的区别主要是顶端有<<interface>>。第一行是接口名称,第二行是接口方法。
继承关系用空心三角形△+实线来表示。
实现接口用空心三角线+虚线来表示。
关联关系用实线箭头→来表示。当一个类‘知道’另一个类时,可以用关联。
聚合关系用空心的菱形◇+实线箭头→来表示。例:大雁是群居动物,每只大雁都是属于一个雁群,一个雁群可以有多只大雁。所以它们之间就满足聚合(Aggregation)关系。聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。
合成关系用实心的菱形+实线箭头→来表示。例:鸟和翅膀就是合成(组合)关系,因为它们是部分和整体的关系,并且翅膀和鸟的生命周期是相同的。合成(组合)是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命同期一样。
依赖关系用虚线箭头来表示。例:动物几大特征,比如新陈代谢,能繁殖。而动物要有生命力,需要氧气、水以及食物等。也就是说,动物依赖于氧气和水。他们之间是依赖关系。
编程是一门技术,更加是一门艺术,不能只满足于写完代码运行结果正确就完事,时常考虑如何让代码更加简练,更加容易维护,容易扩展和复用。只有这样才可以真正得到提高。写出优雅的代码真的是一种很爽的事情。
第2章 商场促销——策略模式
2.1 商场收银软件
做一个商场收银软件,营业员根据客户所购买商品的单价和数量,向客户收费。
2.2 增加打折
需求1;要求商场对商品搞活动,所有的商品都打八折。还有可能因为周年庆,打五折情况。还有可能满300返100的促销算法。还有可能满200送50活动。难道再去增加子类?这当中哪些是相同,哪些是不同?
2.3 简单工厂实现
这里打折基本都是一样的,只要有个初始化参数就可以了。满几送几的,需要两个参数才行。
面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。打一折和打九折只是形式不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。
简单工厂模式虽然也能解决这个问题,这这个模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常性地更改打折额度和返利额度,每次维护或扩展收费方式都是改动这个工厂,以致代码需要重新编译部署,这真的是很糟糕,所以用它不是最好的办法。面对算法的时常变动,应该有更好的办法。
2.4 策略模式
它定义了算法家族,分别封装起来,让它们之间可以互相替换(这些算法是随时都可能互相替换的,这就是变化点),此模式让算法的变化,不会影响到使用算法的客户。封装变化行为为抽象或接口。
2.5 策略模式实现
正常收费、打折收费、返利收费就是三个具体策略,也就是策略模式中说是具体算法。
2.6 策略与简单工厂的结合
简单工厂模式需要让客户端认识两个类,cashSuper和cashFactory,而策略模式与简单工厂结合的用法,客户端就只需要一个类cashContent就可以了,耦合更加降低。使得具体的收费算法彻底与客户端分离。
2.7 策略模式解析
反思一下策略模式,策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类神之间的耦合。
策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。对于打折、返利或者其他的算法,其实都是对实际商品收费的一种计算方式,通过继承,可以得到它们的公共功能,这公共功能是什么呢?
公共的功能就是获得计算费用的结果GetResult,这使得算法间有了抽象的父类CashSuper。
另外一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
每个算法可保证它没有错误,修改其中一个是时也不会影响其他的算法。
策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象。这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context承担,这就最大化地减轻了客户端的职责。
但是这里还有一缺点,如果再次增加一种算法,比如满200送50,就必须要更改CashContext中的switch代码。解决办法使用反射,(在抽象工作模式中有对反射的讲解)。
第3章 拍摄UFO——单一职责原则
3.1 新手机
3.2 拍摄
3.3 没用的东西
3.4 单一职责原则
大多数时候,一件产品简单一些,职责单一一些,或许是更好的选择。这就和设计模式中的一大原则————单一职责的道理是一样的。
可以简单地这么理解,它的准确解释是,就一个类而言,应该仅有一个引起它变化的原因。我们在编程的时候,很自然地就会给一个类加各种各样的功能,比如我们写一个窗体应用程序,一般都会生成一个Form这样的类,于是我们把各种各样的代码,像某种商业运算的算法,像数据库访问的SQL语句什么的都写在这样的类当中,这就意味着,无论任何需求来,你都要更改空上窗体类,这其实是很糟糕的,维护麻烦,复用不可能,也缺乏灵活性。
3.5 方块游戏的设计
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会消弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。事实上,完全可以找出哪些是界面,哪些是游戏逻辑,然后进行分离。
软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。其实要去判断是否应该分享出来,也不难,那就是如果你能够想到多于一个的动机去改变一个类,那么这个类就是有多于一个的职责,就应该考虑类的职责分离。
界面的变化是和游戏本身没有关系的,界面是容易变化的,而游戏逻辑是不太容易变化的,将它们分离开有利于界面的改动。
3.6 手机职责过多吗?
总的来说,手机的发展有它的特点,我们却是要在类的职责分离上多思考,做到单一职责,这样你的代码才是真正的易维护、易扩展、易复用、灵活多样。
第4章 考研求职两不误————开放-封闭原则
4.1 考研失败
香港、澳门回归,一国两制思想。为了回归的大局,增加一种制度,一个国家,两种制度,这在政治上,是伟大的发明。在软件设计模式中,这种不能修改,但可以扩展的思想也是最重要的一种设计原则,它就是开放-封闭原则(the open-closeed principle,简称OCP)或叫开-闭原则。
4.2 开放-封闭原则
开放-封闭原则,是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。
这个原则其实是有两个特征,一个是说“对于扩展是开放的”,另一个是说“对于更改是封闭的”。
我们在做任何系统的时候,都不要指望系统一开始时需求确定,就再也不会变化,这是不现实也不科学的想法,而既然需要是一定会变化的,那么如何在面对需求的变化时,设计的软件可以相对容易修改,不至于说,新需求一来,就是把整个程序推倒重来。怎样的设计才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本呢?开放-封闭给我们答案。
设计软件要容易维护又不容易出问题的最好的办法,就是多扩展,少修改。
4.3 何时应对变化
开放-封闭原则的意思就是说,你设计的时候,时刻要考虑,尽量让这个类是足够好,写好了就不要去修改了,如果新需求来,我们增加一些类就完事了,原来的代码能不动则不动。
绝对的对修改关闭是不可能的。无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化。
我们是很难预先猜测,但我们却可以在发生小变化时,就及早去想办法应对发生更大的变化的可能。也就是说,等到变化发生时立即采取行动。正所谓,同一地方,摔第一跤不是你的错,再次在些摔跤就是你的不对了。
在我们最初编写代码时,假设变化不会发生。当变化发生时,我们就创建抽象来隔离以后发生的同类变化。比如,之前写的加法程序,很快在一个clieng类中就完成,此时变化还没有发生。然后添加一个减法功能,发会现,增加功能需要修改原来这个类,这就违背了今天讲到的“开放-封闭原则”,于是就该考虑重构程序,增加一个抽象的运算为在,通过一些面向对象的手段,如继承,多态等来隔离具体加法、减法与client耦合,需求依然可以满足,还能应对变化。这时又要再加乘除法功能,就不需要再去更改client以及加法减法的类了,而是增加乘法和除法子类就可。即面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。这就是“开放-封闭原则”的精神所在。
我们希望的是在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。
开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
4.4 两手准备,并全力以赴
第5章 会修电脑不会修收音机?——依赖倒转原则
5.1 MM请求修电脑
5.2 电话遥控修电脑
5.3 依赖倒转原则
可以把PC电脑理解成是大的软件系统,任何部件如CPU、内存、硬盘、显示等都可以理解为程序中封装的类或程序集,由于PC易插拔的方式,那么不管哪一个出问题,都可以在不影响别的部件的前提下进行修改或替换。
PC电脑里叫易插拔,面向对象里把这种关系叫强内聚、松耦合。
电脑里的CPU全世界也就是那么几家生产的,大家都在用,但却不知道Intel、AMD等公司是如何做出这个精密的小东西的。这就说明CPU的强内聚的确是强。但它又独自成为了产品,在千千万万的电脑主板上插上就可以使用。
因为CPU的对外就是针脚式或触点式等标准的接口。这就是接口的最大好处。CUP需要把接口定义好,内部再复杂我也不让外界知道,而主板只需要预留与CPU针脚的插槽就可以了。
这里面有提到了面向对象的几大设计原则,比如之前讲过的单一职责原则,就刚才修电脑的事,显然内存坏了,不应该成为更换CPU的理由,它们各自的职责是明确的。再比如开放—封闭原则,内存不够只要插槽足够就可以添加,硬盘不够可以用移动硬盘等,PC的接口是有限的,所以扩展有限,软件系统设计得好,却可以无限地扩展。这两个原则我们之前都已经提过了。这里重点讲讲一个新的原则,叫依赖倒转原则,也有翻译成依赖倒置原则。
依赖倒转原则,原话解释是抽象不应该依赖细节,细节应该依赖于抽象,这放绕口,说白了,就是要针对接口编程,不要对实现编程,无论主板、CPU、内存、硬盘都是在针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌的主板,那就会出现内存需要把主板也换了的尴尬。
依赖倒转原则:A.高层模块不应该依赖低层模块。两个都应该依赖抽象。B.抽象不应该依赖细节。细节应该依赖抽象。
1. 为什么要叫倒转呢?
这里面是需要好好解释一下,面向过程的开发是,为了使得常用代码可以复用,一般都会把这些常用代码写成许许多多函数的程序库,这样我们在做新项目时,去调用这些低层的函数就可以了。比如我们做的项目大多要访问数据库,所以我们就把访问数据库的代码写成了函数,每次做新项目时就去调用这些函数。这也就叫做高层模块依赖低层模块。
问题出在这里,我们要做新项目时,发现业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储信息方式,这时就出现麻烦了。我们希望能再次利用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起的,没办法复用这些高层模块,这就非常糟糕了。就像刚才说的,PC里如果CPU、内存、硬盘都需要依赖具体的主板,主板一坏,所有的部件就都没用了,这显然不合理。反过来,如果内存坏了,也不应该造成其他部件不能用才对。而如果不管高层模块还是低层模块,它们都依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个的更改都不用担心其他受到影响。这就使得无论高层模块还是低层模块都可以很容易地被复用。这才是最好的办法。
2. 为什么依赖了抽象的接口或抽象类,就不怕更改呢?
了解里氏代换原则。
5.4 里氏代换原则
里氏代换原则是Barbara Liskov女士在1988年发表的,它的白话翻译就是一个软件实体如果使用的是一个父类的话,那一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化,简单地说,子类型必须能够替换掉它们的父类型。
这好像是学继承时就要理解的概念,子类继承了父类,所以子类可以以父类的身份出现。
鸟会飞,而企鹅不会飞。尽管在生物学分类上,企鹅是一种鸟,但在编程世界里,企鹅不能以父类——鸟的身份出现,因为前提说所有鸟都能飞,而企鹅飞不了,所以,企鹅不能继承鸟类。
也正因为有了这个原则,使得继承复用成为了可能,只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。比方说,猫是继承动物类的,以动物的身份拥有吃、喝、跑、叫等行为,可当某一天,我们需要狗、牛、羊也拥有类似的行为,由于它们都是继承于动物,所以除了更改实例化的地方,程序其他处不需要改变。
由于里氏代换原则,才使得开放-封闭成为了可能。
这样说是可以的,正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。不然还谈什么扩展开放,修改关闭呢。再回过头来看依赖倒转原则,高层模块不应该依赖低层模块,两个都应该依赖抽象,对这句话就会有更深入的理解了。
依赖倒转其实就是谁也不要依靠谁,除了约定的接口,大家可以灵活自如。
5.5 修收音机
收音机里都是些电阻、三极管,电路板等等东西,全都焊接在一起。
收音机就是典型的耦合过度,只要收音机出故障,不管是没有声音、不能调频,还是有杂音,反正都很难修理,不懂的人根本没法修,因为任何问题都可能涉及其他部件。各个部件相互依赖,难以维护。依赖倒转其实可以说是面向对象设计的标志,用哪种语言来编写程序不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之那就是过程化的设计了。
第6章 穿什么有这么重要?——装饰模式
6.1 穿什么有这么重要?
6.2 小菜扮靓第一版
写一个可以给入搭配不同的服饰的系统,比如类似QQ、网络游戏或论坛都有的Avatar系统(通常用在电脑游戏上,是通过细分角色模型或图像并重新组合来增加角色外观数量的系统)。
意思是那种可以换各种各样的衣服裤子的个人开解系统。穿服饰可以理解为是一种行为,例如:穿T恤、穿球鞋等直接创建person类,添加各种行为。
6.3 小菜扮靓第二版
抽象出一个服饰类,穿球鞋定义为类,继承服饰类,添加自己的行为“穿球鞋”
建造者模式要求建造的过程必须是稳定的,而现在我们这个例子,建造过程是不稳定的,比如完全可以内穿西装,外套T恤,再加披风,打上领带,皮鞋外再穿上破球鞋;换句话就是说,通过服饰组合出一个有个性的人完全可以有无数种方案,并非是固定的。
我们需要把所需的功能按正确的顺序串联起来进行控制。
6.4 装饰模式
装饰模式(Decorator):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。
无论衣服、鞋子、领带、披风其实都可以理解为对人的装饰。
Component是定义一个对象接口,可以给这些对象动态地添加职责。
ConcreteCompoent是定义了一个具体的对象(人类person),也可以给这个对象添加一些职责。
Decorator是一个装饰抽象类,继承了Component,从外类来扩展Component类的功能,但对于Component来说,是无需知道Decorator的存在的。
至于ConcreteDecorator就是具体的装饰对象(T恤、球鞋等),起到给Component添加职责的功能。
原来装饰模式是利用SetComponent来对对象进行包装的。这样每个装饰对象的实现就和如何使用这个对象分离开了,每个装饰对象只关心自己的功能,不需要关心如何被添加到对象链当中。用刚才的例子来说就是,完全可以先穿外裤,再穿内裤,而不一定要先内后外。
学习模式要善于变通,如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decroator类可以是ContcreteComponent的一个子类。同样道理,如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
6.5 小菜扮靓第三版
6.6 装饰模式总结
装饰模式是为已有功能动态地添加更多功能的一种方式。但到底什么时候用它呢?
起初的设计中,当系统需要新功能的时候,是向旧的类中添加新的代码。这些新加的代码通常装饰了原有类的核心职责或主要行为,比如用西装或嘻哈服来装饰小菜,但这种做法的问题在于,它们在主类中加入了新的字段,新的方法和新的逻辑,从而增加了主类的复杂度,就像你起初的那个“人”类,而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要。而装饰模式却提供了一个非常好的解决方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它所要的装饰的对象,因此,当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。所以就出了上面的例子的情况,可以通过装饰,让你全副武装到牙齿,也可以只挂一丝到内裤。
那么装饰模式的优点总结下就是,把类中的装饰功能从类中搬移去除,这样可以简化原有的类。这样更大的好处就是有效地把类的核心职责和装饰功能区分开了。而且可以去除相关类中重复的装饰逻辑。
第7章 为别人做嫁衣——代理模式
7.1 为别人做嫁衣
7.2 没有代理的代码
追求者类---被追求者类
7.3 只有代理的代码
代理类---被追求者类:把追求者给忽略了,事实上应该是追求者通过代理送给被追求者礼物,这才是合理的。
仔细观察一下,追求者和被追求者是有相似的地方。他们都有送礼物的方法,只不过被追求者送的礼物是追求者买的,实质是追求者送的。既然两者都有相同的方法,那就意味他们需要实现同样的接口。
7.4 符合实际的代码
定义送礼物接口(送鲜花、送巧克力),追求者类实现送礼物接口,代理类也去实现送礼物接口(在实现方法中去调用“追求者”类的相关方法).
7.5 代理模式
代理模式(proxy):为其他对象提供一种代理以控制对这个对象的访问。
7.6 代理模式应用
代理模式都用在一些什么场合呢?
一般来说分为几种:
第一,远程代理,也就是为一个对象在不同的地址空间提供局部的代表。这样可以隐藏一个对象存在于不同地址空间的事实。
例如WebService在.NET中的应用,当在应用程序的项目中加入一个web引用,引用一个webService,此时会在项目中生成一个WebReference的文件夹和一些文件,其实它们就是代理,这就使用客户端程序调用代理就可以解决远程访问的问题。
第二,虚拟代表,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。这样可以达到性能的最优化。
例如打开一个很大的HTML网页时,里面可能有很多的文字和图片,但你还是可以很快打开它,此时你所看到的是所有的文字,但图片却是一张一张地下载后才能看到。那些未打开的图片框,就是通过虚拟代理来替代了真实的图片,此时代理存储了真实图片的路径和尺寸。
第三,安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候。
第四,智能指引,是指当调用真实的对象时,代理处理另外一些事。
例如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它;或当第一次引用一个持久对象时,将它装入内存;或在访问一个实际对象前,检查是否已经锁定它,以确保其他对象不能改变它。它们都是通过代理在访问一个对象时附加一些内务处理。
代理模式其实就是在访问对象时引入一定程序的间接性,因为这种间接性,可以附加多种用途。
7.7 秀才让小六代其求婚
第8章 雷锋依然在人间——工厂方法模式
8.1 再现活雷锋
8.2 简单工厂模式实现
一直在研究工厂方法模式,但还是不太理解它和简单工厂的区别,感觉还不如简单工厂方便,为什么要用这个模式,到底这个模式的精髓在哪是?
8.3 工厂方法模式实现
先构建一个工厂接口,然后加减乘除各建一个具体工厂(加法类工厂、减法类工厂)去实现这个接口。
8.4 简单工厂 VS 工厂方法
以前说过,如果现在需要增加其他运算,比如求M数的N次方,或者求M数的N次方根,这些功能的增加,在简单工厂里,是先去加“求M数的N次方”功能类,然后去更改工厂方法,当中加“Case”语句来做判断,现在用了工厂方法,加功能类没问题,再加相关的工厂类,这也没问题,但是我再去更改客户端,定不等于不但没有减化难度,反而增加了很多类和方法,把复杂性增加了吗?为什么要这样?
这其实就是工厂方法模式和简单工厂的区别所在。简单工厂模式的最大优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。
就像计算器,让客户端不用管该用哪个类的实例,只需要把“+”给工厂,工厂自动就给出了相应的实例,客户端只要去做运算就可以了,不同的实例会实现不同的运算。但问题也就在这里,如你所说,如果要加一个“求M数的N次方”的功能,我们是一定需要给运算工厂类的方法里加“Case”的分支条件的,修改原有的类?这可不是好办法,这就等于说,我们不但对扩展开放了,对修改也开放了,这样就违背了开放-封闭的原则。
工厂方法模式(Factory Method):定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使用一个类的实例化延迟到其子类。
我们讲过,既然这个工厂类与分支耦合,那么我就对它下手,根据依赖倒转原则,我们把工厂类抽象出一个接口,这个接口只有一个方法,就是创建抽象产品的工厂方法。然后,所有的要生产具体类的工厂,就去实现这个接口,这样,一个简单工厂模式的工厂类,变成了一个工厂抽象接口和多个具体生产对象的工厂,于是我们要增加“求M数的N次方”的功能时,就不需要更改原有的工厂类了,只需要增加此功能的运算类和相应的工厂类就可以了。
这样整个工厂和产品体系其实都没有修改的变化,而只是扩展的变化,这就完全符合了开放-封闭原则的精神。
其实仔细观察就会发现,工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是说,工厂方法把简单工厂的内部逻辑判断移到了客户端代码来进行。你想要加功能,本来是改工厂类的,而现在是修改客户端。
8.5 雷锋工厂
雷锋是众人皆知的做好人好事的模范,而班级里的某某现学以学习雷锋的名义做好事,而现在去代替他做好事,这其实就是典型的工厂方法模式应用了。
雷锋类,拥有扫地、洗衣、买米等方法。
学雷锋的大学生类,继承 雷锋
然后客户端实现,假设有三个人要去代替他做这些事,那就应该实例化三个“学雷锋的大学生”对象了。
学生都是要毕业的,而帮助老人却是长期工作,所以“社区志愿者”更合适,所以还需要增加一个继承“雷锋”类的“社区志愿者”类。
此时会发现,需要在任何实例化的时候写出这个工厂的代码。这里有重复,也就有了坏味道。再用工厂方法模式写一遍。
工厂方法克服了简单工厂违背开放-封闭原则的缺点,又保持了封装对象创建过程的优点。它们都是集中封装了对象的创建,使得要更换对象时,不需要做大的改动就可实现,降低了客户程序与产品对象的耦合。工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。但缺点是由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量。所以工厂方法也并不是最佳的做法。之前提到过,利用“反射”可以避免分支判断的问题。
第9章 简历复印——原型模式
9.1 夸张的简历
9.2 简历代码初步实现
简历类、客户端调用
三分简历需要三次实例化。如果要二十份,就需要二十次实例化。如果写错一个字,那就要改二十次。
9.3 原型模式
原型模式:用原型实例指定创建对象的各种类,并且通过拷贝这些原型创建新的对象。
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。原型模式代码如下:
抽象原型类(添加抽象clone方法,返回抽象原型类)、具体原型类实现抽象原型类、客户端代码
java类中提供了Cloneable接口,其中就是唯一的一个方法Clone(),这样就只需要实现这个接口就可以完成原型模式了。
9.4 简历的原型实现
简历类实现Cloneable接口、客户端调用(只需要调用clone方法就可以实现新简历的生成,并且可以再修改新简历的细节)
使用clone可对性能的提高,如果每new一次,都需要执行一次构造函数,如果构造函数的执行时间很长,那么多次的执行这个初始化操作就实在是太低效了。一般在初始化的信息不发生变化的情况下,克隆是最好的办法。这即隐藏了对象创建的细节,又对性能是大大的提高。
它等于是不用重新初始化对象,而是动态地获得对象运行时的状态。
9.5 浅复制与深复制
如果字段是值类型的,则对该字段执行逐位复制,如果字段是引用类型,则复制引用但不复制引用的对象,因此,原始对象及其复本引用同一对象。如果“简历”类当中有对象引用,那么引用的对象数据是不会被克隆过来的。
浅复制:被复制的对象所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用都仍然指向原来的对象。但我们可能更需要这样的一种需求,把要复制的对象所引用的对象都复制一遍。
深复制:把引用对象的变量指向复制过的新对象,而不是原有的被引用的对象。
如果“简历”对象引用了“工作经历”,“工作经历”再引用“公司”,“公司”再引用“职位”......,这样一个引用一个很多层,如何办?
这的确是个很难回答的问题,深复制要深入到多少层,需要事先就考虑好,而且要当心出现循环引用的问题,需要小心处理,这里比较复杂,可以慢慢研究。就现在这个例子,问题应该不大,深入到第一层就可以了。
9.6 简历的深复制实现
代码组织:让工作经历也实现Cloneable接口,实现克隆方法。在简历类中,调用工作经历的私有构造函数(clone),以便克隆工作经历数据。
由于在一些特定场合,会经常涉及到深复制或浅复制,比如说,数据集对象DataSet,它就有Clone()方法和Copy()方法,Clone()方法用来复制DataSet的结构,但不复制DataSet的数据,实现了原有模式的浅复制。Copy()方法不但复制结构,也复制数据,其实就是实现了原型模式的深复制。
9.7 复制简历 VS 手写求职信
第10章 考题抄错会做也白搭——模板方法模式
10.1 选择题不会做,蒙呗!
考试试卷最大的好处就是,大家都是一样的题目,特别是标准化的考试,比如全是选择或判断的题目,那就最大化地限制了答题者的发挥,大家都是ABCD或打勾打叉,非对即错的结果,这其实就是一个典型的设计模式。
10.2 重复=易错+难改
抄题目的程序如下:
学生甲抄的试卷
学生乙抄的试卷
客户端代码:学生甲 = new 学生甲;学生乙 = new 学生乙
10.3 提炼代码
学生甲和学生乙两个抄试卷类非常类似,除了答案不同,没什么不一样,这样写又容易错,又难以维护。
老师出一份试卷,打印多份,让学生填写答案就可以了。在这里应该就是把试题和答案分享,抽象出一个父类,让两个子类继承于它,公共的试题代码写到父类当中,就可以了。
试卷父类代码:
父类金庸小说考题试卷,行为(考题),学生子类代码=学生甲抄的试卷继承父类,学生乙抄的试卷继承父类 行为(给出各个考题的答案)
以上代码中还有相同的东西,比如都有base.考题1(),还有console.writeline("答案:"),除了选项的abcd,其他都是重复的。
我们既然用了继承,并且肯定这个继承有意义,就应该要成为子类的模板,所有重复的代码都应该要上升到父类去,而不是让每个子类都去重复。
模板方法登场,当我们要完成在某一细节层次一致的一个过程或一系列督,但其个别步骤在更详细的层次上的实现可能不同是时,我们通常考虑用模板方法模式来处理。
考题都一样,只有答案不一样,这时需要增加一个答案的虚方法。虚方法的目的就是给继承的子类重写,因为这里每个人的答案都是不同的。
子类只需要重写虚方法,把答案填上,其他什么都不用管。因为父类建立了所有重复的模板。
客户端代码需要改动一个小地方,即本来是子类变量的声明,改成了父类,这样就可以利用多态性实现代码的复用了。而这其实就是典型的模板方法模式。
10.4 模板方法模式
模板方法模式(TemplateMethod):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
AbstractClass是抽象类,其实也就是一抽象模板,定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
ConcreteClass是子类,实现父类所定义的一个或多个抽象方法。每一个AbstractClass都可能 有任意多个ConcreteClass与之对应,而每一个ConcreteClass都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
10.5 模板方法模式特点
模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势。
模板方法模式就是提供了一个很好的代码复用平台。有时候,我们会遇到由一系列步骤构成的过程需要执行。这个过程从高层次上看是相同的,但有些步骤的实现可以能不同。这时候,我们通常就应该要考虑用模板方法模式了。
碰到这个情况,当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。
模板方法模式是很常用的模式,对继承和多态玩得好的人几乎都会在继承体系中多多少少用到它。比如在.NET或Java类库的设计中,通常都会利用模板方法模式提取类库中的公共行为到抽象类中。
注:每当有两个行为类似但又不完全相同的代码段时,我总是会想到模板方法。提取公共流程和可复用的方法到父类,保留不同的地方作为abstract方法,由不同的子类去实现。
10.6 主观题,看你怎么蒙
第11章 无熟人难办事?—— 迪米特原则
11.1 第一天上班
到IT部门领取电脑,认识一个叫小张的同事,结果正打算装电脑的时候,来了个电话,叫他马上去一个客户那里处理PC故障。结果一等就是一上午。去问人事部可不可以让其他人帮忙,人事部让自己去找一下IT部的小李,小李接过领取电脑单子,看上面写着小张的名字,于是说这个事是小张负责,他不管。还得等小张回来再说。
11.2 无熟人难办事
以上问题倒是我们设计模式的一个原则。如果IT部有一个主管,负责分配任务,不管任何需要IT部配合的工作都让主管安排,不就没问题了吗?
没有管理,单靠人际关系协调是很难办成事的。如果公司IT部就一个小张,那什么问题也没有。再来个小李,那工作叫谁去做呢?外人又不知道他们两人谁忙谁闲的。要是三个人在IT部还没有管理人员,则更加麻烦了。正所谓一个和尚挑水吃,两个和尚抬水吃,三个和尚没水吃。
其实不管认不认识IT部的人,只要电话或亲自找到IT部,他们都应该想办法帮忙解决问题。不管公司任何人,无论认不认识,找IT部就可以了。
在这里,IT部代表是抽象类或接口,小张小李代表是具体类,之前分析会修电脑不会修改收音机里讲的依赖倒转原则,即面向接口编程,不要面向实现编程就是这个意思。
11.3 迪米特原则
迪米特原则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
迪米特原则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开。
当然,面向对象的设计原则和面向对象的三大特性本就不是矛盾的。迪米特法则其根本思想,是强调了类之间的松耦合。就拿今天碰到的这件事做例子,第一天去公司,怎么会认识IT部的人呢,如果公司有很好的管理,那么应该是人事部打个电话到IT部,告诉主管安排人装电脑,就算开始是小张负责,他临时有事,主管也可以再安排小李来处理。同样道理,我们在程序设计时,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。
第12章 牛市股票还会亏钱?—— 外观模式
12.1 牛市股票还会亏钱?
自己的钱买了股票,天天都在变化,谁能不关心,特别是刚开始,都希望能涨涨涨。尽管不现实,不过赚钱的人还是有的是。不过一打开股票软件,一千多只股票,红红绿绿,又是指数大盘,又是个股K线指标,一下说基本面如何如何重要,一下又说什么有题材才可以赚大钱,头晕眼花,迷茫困惑阿。
基金就是你的帮手。它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有投资者所有,管理机构收取一定比例的托管管理费用。想想看,这样做有什么好处?
由于众多投资者对众多股票的联系太多,反而不利于操作,这在软件中就称为耦合性过高。而有了基金以后,变成众多用户只和基金打交道,关心基金的上涨和下跌就可以了,而实际上的操作却是基金经理人在与上千支股票和其他投资产品打交道。
这里其实提到了一个在面向对象开发当中用得非常多的一个设计模式——外观模式,又收门面模式。先试着把股民炒股票的代码写写看。
12.2 股民炒股代码
客户类、股票1类(行为买、卖)、股票2类,债券类等,用户需要了解股票、债券情况,需要参与这些项目的具体买和卖。耦合性很高。
12.3 投资基金代码 增加基金类
客户类、基金类(它需要了解所有的股票或其他投资方式的方法和属性,进行组合,以备外界调用。行业-基金购买和基金赎回)、股票1类(行为买、卖)、股票2类,债券类等
客户端调用,此时用户不需要了解股票,甚至可以对股票一无所知,买了基金就回家睡觉,一段时间后再赎回就可以大把数钱。参与股票的具体买卖都由基金公司完成。客户端代码简捷明了。
这样的写法其实就是外观模式的基本代码结构。
12.4 外观模式(Facade)
外观模式:为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
对于面向对象有一定基础的朋友,即使没有听说过外观模式,也完全有可能在很多时候使用它,因为它完美地体现了依赖倒转原则和迪米特原则的思想,所以是非常常用的模式之一。
12.5 何时使用外观模式
这是分三个阶段来说:
首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。
其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式使用时都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。
第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时用外观模式Facade也是非常合适的。
总结,你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。
对于复杂难以维护的老系统,直接去改或去扩展可能产生很多问题,分两个小组,一个开发Facade与老系统的交互,另一个只要了解Facade的接口,直接开发新系统调用这些接口即可,确实可以减少很多不必要的麻烦。
第13章 好菜每回味不同 —— 建造者模式
13.1 炒面没放盐
麦当劳、肯德基的汉堡,不管在哪家店里吃,什么时间去吃,至少在中国,味道基本都是一样的。而我们国家,比如那道鱼香肉丝,几乎是所有大小中餐饭店都有的一道菜,但却可以吃出上万种口味来,这是为什么?因为厨师不一样,每个人的做法不同的。
麦当劳、肯德基他们比较规范,味道是由他们的工程流程决定的,原料放多少,加热几分钟,都有严格规定。
而我们的炒面有没有放盐、好吃不好吃都是由烧菜的人决定的,心情好就是一盘好面,心情不好,就是一盘垃圾。
今天就吃了两盘垃圾,其实这里面最关键的就在于我们是吃得爽还是吃得难受都要依赖于厨师。再想想我们设计模式的原则?
依赖倒转原则?抽象不应该依赖细节,细节应该依赖于抽象,由于我们要吃的菜都依赖于厨师这这样的细节,所以我们就很被动。
老麦老肯的工作流程可以是一种抽象的流程,具体放什么醭、烤多长时间等细节依赖于这个抽象。
13.2 建造小人一
要求用程序画一个小人,这在游戏里非常常见,现在简单一点,要求是小人要有头、身体、两手、两脚就可以。
实现:建立一支黄色的画笔,在画布上画出头、身体、左手、右手、左脚、右脚。
现在要求再画一个身体比较胖的小人呢
实现:改变画笔其中一个身体即可。但是却少画了一条腿。
这就和吃炒面一样,老板忘记了放盐,让本是非常美味的夜宵变得无趣。在游戏程序里,画人的时候,头身手脚必不可少,不管什么人物,开发时是不能少的。
现在的代码全写在单独的窗体里,要是需要在别的地方用这些画小人的程序怎么办?
13.3 建造小人二
分离,建两个类,一个是瘦人类,一个是胖人类,不管谁都可以调用它。
瘦人类:初始化时确定画板和颜色,build()建造小人
胖人类:初始化时确定画板和颜色,build()建造小人
客户端实现:创建瘦人类、胖人类
这样写还是没有解决炒面忘记放盐的问题。比如现在需要加一个高个的小人,会不会因为编程不注意,又让他缺胳膊少腿呢?
最好的办法是规定,凡是建造小人,都必须要有头和身体,以及两手两脚。
13.4 建造者模式
建造者模式(Builder):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
仔细分析会发现,这里建造小人的”过程“是稳定的,都需要头身手脚,而具体建造的“细节”是不同的,有胖有瘦有高有矮。但对于用户来讲,我才不管这些,我只想告诉你,我需要一个胖小人来游戏,于是你就建造一个给我就行了。如果你需要将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示的意图时,我们需要应用于一个设计模式,建造者(Builder)模式,又叫生成器模式。
建造者模式可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同的内部表象的产品对象。如果我们用了建造都模式,那么用户就只需指定需要建造的类型就可以得到它们,而具体建造的过程和细节就不需知道了。
那怎么用建造者模式呢?
首先我们要画小人,都需要画什么?头、身体、左手、右手、左脚、右脚。
对的,所以我们先定义一个抽象的建造人的类,来把这个过程给稳定住,不让任何人遗忘当中的任何一步。
然后,我们需要建造一个瘦的小人,则让这个瘦子类去继承这个抽象类,那就必须去重写这些抽象方法。否则编译器也不让你通过。
这样子,在客户端要调用时,还是需要知道头身手脚这些方法,没有解决问题。
我们还缺建造都模式中一个很重要的类,指挥者(Director),用它来控制建造过程,也用它来隔离用户与建造过程的关联。
用户告诉指挥者PersonDirector,我需要什么样的小人,根据用户的选择建造小人createPerson。
PersonDirector类的目的就是根据用户的选择来一步一步建造小人,而建造的过程在指挥者这时完成了,用户就不需要知道了,而且,由于这个过程每一步都是一定要做的,那就不会让少画了一只手,少画一条腿的问题出现了。
13.5 建造者模式解析
建造者模式(Builder)结构图
Builder是为创建一个Product对象的各个部件指定的抽象接口。
Director指挥者,是构建一个使用Builder接口的对象。
ConcreteBuilder,具体建造者,实现Builder接口,构造和装配各个部件。
Product,具体产品。
Builder是什么?是一个建造小人各个部分的抽象类。概括地说,是为创建一个Product对象的各个部件指定的抽象接口。
ConcreteBuilder是什么?具体的小人,具体实现如何画出小人的头身手脚各个部分。它是具体建造者,实现Builder接口,构造和装配各个部件。
Product是什么?产品的角色。
Director是什么?指挥者,用来根据用户的需求构建小人对象。它是构建一个使用Builder接口的对象。
什么时候需要使用建造者模式呢?
它主要是用于创建一些复杂的对象,这些对象内部构建间的建造顺序通常是稳定的,但对象内部的构建通常面临着复杂的变化。
建造者的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者就行了。
13.6 建造者模式基本代码
Product类——产品类,由多个部件组成,行为add(string part)-添加产品部件
Builder类——抽象建造者类,确定产品由两个部件BuilderPartA和BuilderPartB组成,并声明一个得到产品建造后结果的方法GetResult.
ConcreteBuilder1类——具体建造者类,建造具体的两个部件是部件A和部件B。
Director类——指挥类,用来指挥建造过程.
客户端代码,客户不需知道具体的建造过程。指挥者用ConcreteBuilder1的方法来建造产品。
所以说,建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式。
如果今天做炒面的老板知道建造者模式,他就明白,盐是一定要放的,不然,编译就通不过。
第14章 老板回来,我不知道———观察者模式
14.1 老板回来?我不知道!
通过前台秘书,如果老板出门后回来,就打个电话进来,大家(同事)马上各就各位,这样就不会被老板发现问题了。
14.2 双向耦合的代码
前面说的情形,其实是一个典型的观察者模式。
前台秘书类:class Secretary(通知者类)
拥有属性:同事列表 list<StockObserver>
拥有方法: 增加(就是有几个同事请前台帮忙,于是就给集合增加几个对象) void Attach(StockObserver observer)
拥有方法:通知(待老板来时,就给所有的登记的同事发通知,老板来了) void Notify()
拥有方法:前台状态(前台通过电话,所说的话或所做的事) string SecretaryAction
看股票同事类:class StockObserver(观察者类)
拥有方法:更新(得到前台的通知,赶快采取行动) void update()
客户端:
初始化创建前台秘书类;初始化创建看股票的同事(两个);前台记下两位同事.Attach(observer);发现老板回来.SecretaryAction = "老板来了";通知两个同事.Notify();
以上代码中,两个类之间互相耦合,前台类要增加观察者,观察者类需要前台的状态。如果观察者当中还有人是想看NBA的网上直播,前台类就需要改动。
根据设计原则,首先开放—封闭原则,修改原有代码就说明设计不够好。其次是依赖倒转原则,应该让程序都依赖抽象,而不是相互依赖。
14.3 解耦实践一
增加抽象的观察者 abstract class Observer抽象方法更新 void update();
增加两个具体观察者继承抽象观察者 class StockObserver(看股票的同事):Observer,对update的方法做重写操作。
增加两个具体观察者继承抽象观察者 class NBAObserver(看NBA的同事):Observer,对update的方法做重写操作。
前台秘书类:class Secretary,把所有的与具体观察者耦合的地方都改成抽象观察者。针对抽象编程,减少与具体类的耦合
前台秘书也是一个具体的类,也应该抽象出来。通知者可能是前台秘书也可能是老板。
14.4 解耦实践二
增加抽象通知者接口 interface Subject
增加(就是有几个同事请前台帮忙,于是就给集合增加几个对象) void Attach(Observer observer)
减少(如果一个同事与前台(通知者有矛盾),于是不通知这位同事)void Deatch(Observer observer)
发出通知void Notify();
状态(前台通过电话,所说的话或所做的事)String subjectState{get;set;}
具体的通知类可能是前台,也可能是老板,它们也许有各自的一些方法,但对于通知者来说,它们是一样的,所以它们都去实现这个接口。
老板类class Boss:Subject 同是列表、增加、减少、通知、老板状态
对于具体的观察者,需要改动的地方就是把与"前台"耦合的地方都改成针对抽象通知者。
14.5 观察者模式
观察者模式又叫做发布-订阅(Publish/Subscribe)模式。
观察者模式:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
观察者(Observer)结构图
Subject(抽象通知者)类,它把所有对观察者对象的引用保存在一个聚集里,每个主题都可以有任何数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
Observer类,抽象对象者,为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。
ConcreteSubject类,具体主题通知者,将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。
ConcreteObserver类,具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态想协调。
14.6 观察者模式特点
用观察者的动机是什么?
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会难维护、扩展和重用都带来不便。而观察者模式的关键对象是主题(通知者)Subject和观察者Observer,一个Subject可以有任意数目的依赖它的Observer,一旦Subject的状态发生了改变,所有的Observer都可以得到通知。Subject发出通知时并不需要知道谁是它的观察者,也就是说,具体观察者是谁,它根本不需要知道。而任何一个具体观察都不知道也不需要知道其他观察者的存在。
什么时候考虑使用观察者模式呢?
当一个对象的改变需要同时改变其他对象的时候。而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。当一个抽象模型有两个方面,其中一方面依赖于另一方面,这时用观察者模式可以将这两者封装在独立的对象中使它们各自独立地改变和复用。
总的来讲,观察者模式所做的工作其实就是在解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。从而使用得各自的变化都不会影响另一边变化(这也是依赖倒转的最佳体现)。
在抽象观察者者时,代码里用的是抽象类,为什么不用接口?
因为两个具体观察者,看股票观察者和看NBA观察者类是相似的,所以用了抽象类,这样可以共用一些代码。
那么抽象观察者可不可以用接口来定义?interface observer{void update();}
现实编程中,具体的观察者完全有可能是风马牛不相及的类,但它们都需要根据通知者的通知来做出Update()的操作,所以让它们都实现一个接口就可以实现这个想法了。
14.7 观察者模式的不足
回到刚才那个"老板、前台与同事的例子",看看它还有什么不足之处?
尽管已经用了依赖倒转原则,但是"抽象通知者"还是依赖"抽象观察者",也就是说,万一没有了抽象观察者这样的接口,我这通知的功能就完不成了。另外就是每个具体观察者,它不一定是"更新"的方法要调用,就像刚才说的,我希望的是"工具箱"是隐藏,"自动窗口"是打开,这根本就不是同名的方法。这就是不足的地方。
如果通知者和观察者之间根本就互相不知道,由客户端来决定通知谁,那就好了。
14.8 事件委托实现
"看股票观察者"类和"看NBA观察者"类,去掉了父类"抽象观察类",所以补上一些代码,并将“更新”方法名改为各自适合的方法名。
看股票的同事:StockObserver-void CloseStockMarket()方法"更新"名改为"关闭股票程序"。
看NBA的同事:NBAObserver-void CloseNBADirectSeeding()方法"更新"名改为"关闭NBA直播"。
现实中就是这样的,方法名本就不一定相同。
抽象通知者由于不希望依赖抽象观察者,所以增加或减少的方法也就没有必要了(抽象观察者已经不存在了)。
通知者接口:interface Subject{void Notify();}
下面就是如何处理老板类和前台类的问题,它们当中通知方法有了对观察者遍历,所以不可小视之。但如果在.NET中,我们可以用一个非常好的技术来处理这个问题,它叫委托。
声明一个委托,名称叫EventHandler(事件处理程序),无参数,无返回值。delegate void EventHandler();Delegate用来实现函数方法的间接调用
老板类和前台秘书类:声明一个事件Update,类型为委托EventHandler;public event EventHandler Update;(声明一EventHandler(事件处理程序)的委托事件,名称叫Update(更新))
在访问通知方法时,调用更新public void Notify(){Update();}
客户端:将看股票者的关闭股票程序方法和看NBA者的关闭NBA直播方法挂钩到老板的更新上,也就是将两个不同类的不同方法委托给老板类的更新了。boss.Update += new EventHandler(nbaObserver.CloseNBADirectSeeding);
14.9 事件委托说明
委托就是一种引用方法类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托方法的使用可以像其他任何方法一样,具有参数和返回值。委托可以看作是对函数的抽象,是函数的类,委托的实例将代表一个具体的函数。
delegate void EventHandler();可以理解为声明了一特殊的类,而public event EventHandler Update;可以理解为声明了一个类的变量(应该是声明了一个事件委托变量叫更新)。
委托的实例将代表一个具体的函数,意思是说,new EventHandler(nbaObserver.CloseNBADirectSeeding)其实就是一个委托的实例,而它就等于将nbaObserver.CloseNBADirectSeeding这个方法委托给boss.Update这个方法了。
一旦为委托分配了方法,委托将与该方法具有完全相同的行为。而且,一个委托要以搭载多个方法,所有方法被依次响起。更重要的是,它可以使得委托对象所搭载的方法羡慕不需要属于同一个类。
这样主使得,本来是在老板类中的增加或减少的抽象观察者集合以及通知时遍历的抽象观察者都不必要了。转到客户端来让委托搭载多个方法,这样就解决了本来与抽象观察者的耦合问题。
但委托也是有前提的,那就是委托对旬所搭载的所有方法必须具有相同的原形和形式,也就是拥有相同的参数列表和返回值类型。
注意,是先有观察者模式,再有委托事件技术的。
14.10 石守吉失手机后的委托
石守吉的手机丢失,原来的同学号码都没有了,委托小菜帮忙把同学的号码抄一份发邮件给他。但是班级人数多,抄起来容易出错,而且,如果现在同学有急事要找,还是会找不到。
使用观察者模式,小菜在这里给班级所有同学群发一条短消息,通知他们,石守吉换新号,请大家更新号码。
第15章 就不能不换DB吗?——抽象工厂模式
15.1 就不能不换DB吗?
给一家企业做电子商务网站,是用SQL Server作为数据库的。而后,公司接到另外一家公司类似需求的项目,但这家公司只能用Access,于是就要改造原来的项目代码。
15.2 最基本的数据访问程序
工厂方法模式是定义一个用于创建对象的接口,让子类决定实例化哪一个类。
15.3 用了工厂方法模式的数据访问程序
代码结构图:
IUser接口,用于客户端访问,解除与具体数据库访问的耦合。interface IUser{void Insert(User user);User GetUser(int id)}
SqlserverUser类,用于访问SQL Server的User。 class SqlserverUser:IUser
Accessuser类,用于访问Access的User。class AccessUser:IUser
IFactory接口,定义一个创建访问User表对象的抽象的工厂接口。interface IFactory{IUser CreateUser();}
SqlServerFactory类,实现IFactory接口,实例化SqlserverUser。 class SqlServerFactory:IFactory{IUser CreateUser(){return new SqlserverUser();}}
AccessFactor类,实现IFactory接口,实例化AccessUser。class AccessFactory:IFactory{IUser CreateUser(){return new AccessUser();}}
客户端代码:IFactory factory = new SqlServerFactory();若要更改成Access数据库,只需要将本名改成IFactory factory = new AccessFactory();
但是,代码里还是有指明new SqlServerFactory(),要改的地方,依然很多。数据库里不可能只有一个User表, 很可能有其他表,比如增加部门表(Department表),此时如何办?
15.4 用了抽象工厂模式的数据访问程序
添加Department表与User表步骤一样。只需要改IFactory factory = new AccessFactory()为IFactory factory = new SqlServerFactory(),就实现了数据库访问的切换了。
其实在不知不觉中,已经通过需要的不断演化,重构出了一个非常重要的设计模式。
只有一个User类和User操作类的时候,是只需要工厂方法模式的,但现在显然数据库中有很多的表,而SQL Server与Access又是两大不同的分类,所以解决这种涉及到多个产品系统的问题,有一个专门的工厂模式叫抽象工厂模式。
15.5 抽象工厂模式
抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂结构图:
AbstractFactory:抽象工厂接口,它里面应该包含所有的商品创建的抽象方法 CreateProductA();CreateProductB();
ConcreteFactory1:ConcreteFactory2:具体的工厂,创建具有特定实现的产品对象。
AbstractProductA:AbstractProductB:抽象产品,它们都有可能有两种不同的实现。
ProductA1:ProductA2:ProductB1:ProductB2:对两个抽象产品的具体分类的实现。
AbstractProductA和AbstractProductB是两个抽象产品, 之所以为抽象,是因为它们都有可能有两种不同的实现,就刚才的例子来说就是User和Department,而ProductA1、ProductA2、和ProductB1、ProductB2就是对两个抽象产品的具体分类的实现,比如ProductA1可以理解为是SqlserverUser,而ProductB1是AccessUser。
可以这个理解,IFactory是一个抽象工厂接口,它里面应该包含所有的产品创建的抽象方法,而ConcreteFactory1和ConcreteFactory2就是具体的工厂了。就像SqlserverFactory和AccessFactory一样。
通常是在运行时刻再创建一个ConcreteFactory类的实例,这个具体的工厂再创建具有特定实现的产品对象,也就是说,为创建不同的产品对象,客户端应使用不同的具体工厂。
15.6 抽象工厂的优点与缺点
这样做的好处是什么呢?
最大的好处便是易于交手产品系统,由于具体工厂类,例如IFactory factory = new AccessFactory(),在一个应该中只需在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需改变具体工厂即可使用不同的产品配置。
我们的设计不能去防止需要的更改,那么我们的理想便是让改动变得最小,现在如果你要更改数据库访问,我们只需要改具体工厂就可以做到。第二大好处是,它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。事实上,刚才写的例子,客户端所认识的只有IUser和IDepartment,至于它是用SQL Server来实现还是Access来实现就不知道了。
抽象模式也有缺点,是个模式都会有缺点,都有不适用的时候。抽象工厂模式可以很方便地切换两个数据库访问的代码。但是如果需要来自增加功能,比如现在要增加项目表Project,需要改动哪些地方?
那就至少要增加三个类,IProject、SqlserverProject、AccessProject,还需要更改IFactory、SqlserverFactory和AccessFactory才可以完全实现。这太糟糕了。
还有,客户端程序类显然不会是只有一个,有很多地方都在使用IUser或IDepartment,而这样的设计,其实在每一个类的开始都需要声明IFactory factory = new SqlServerFactory,如果我有100个调用数据库访问的类,是不是就要更改100次IFactory factory = new SqlServerFactory这样的代码才行?这不能解决我要更改数据库访问时,改动一处就完全更改的要求。
编程是门艺术,这样大批量的改动,显然是非常丑陋的做法。
15.7 用简单工厂来改进抽象工厂
去除IFactory、SqlserverFactory和AccessFactory三个工厂类,取而代之的是DataAccess类,用一个简单工厂模式来实现。
由于事先设置了db的值(Sqlserver和Access),所以简单工厂的方法都不需要输入参数,这样在客户端就只需要DataAccess.CreateUser和DataAccess.CreateDepartment()来生成具体的数据库访问类实例,客户端没有出现任何一个SQL Server或Access的字样,达到了解耦的目的。
但是,此方式还有些问题,原因是如果需要增加Oracle数据库访问,本来抽象工厂只需增加一个OracleFactory工厂类就可以了,现在就比较麻烦了。需要在DataAccess类中每个方法的switch中加case了。
15.8 用反射+抽象工厂的数据访问程序
我们要考虑的就是可不可以在程序里写明,如果是Sqlserver就去实例化SQL Server数据库相关类,如果是Access就去实例化Access相关类这样的语句,而是根据字符串db的值去某个地方找应该要实例化的类是哪一个。这样,我们的switch就可以对它说再见了。
所谓反射,是指在运行时状态中,获取类中的属性和方法,以及调用其中的方法的一种机制。这种机制的作用在于获取运行时才知道的类(Class)及其中的属性(Field)、方法(Method)以及调用其中的方法,也可以设置其中的属性值。
反射格式:.NET实现方式=Assembly.Load("程序集名称").CreateInstance("命名空间.类名")、java实现方式=Class.forName("包名.类名");
只要在程序的顶端写上using SystemReflection;来引用Reflection,就可以使用反谢来帮我们克服抽象工厂模式的先天不足了。
实例化效果是一样的,但这两种方法的区别在哪里?
常规方法是写明了要实例化SqlserverUser对象。反谢的写法,其实也是指明了要实例化SqlserverUser对象。常规方法不能灵活更为AccessUser,反射中CreateInstance("抽象工厂模式.SqlserverUser")可以灵活更找SqlserverUser为AccessUser。
原因是这里是字符串,可以用变量来处理,也就可以根据需要更换。
总结这里的差别主要在原来的实例化是写死在程序里,而现在用了反射就可以利用字符串来实例化对象,而变量是可以更换的。
写死在程序里,太难听了。准确地说,是将程序由编译时转为运行时。由于CreateInstance("抽象工厂模式.SqlserverUser")中的字符串是可以写成变量的,而变量的值到底是SQL Server,还是Access,完全可以由事先的那个db变量来决定。所以就去除了switch判断的麻烦。
总体感觉还是有点缺憾,因为在更换数据库访问时,还是需要去改程序(改db这个字符串的值)重编译,如果可以不改程序,那才是真正地符合开放-封闭原则。
15.9 用反射+配置文件实现数据访问程序
可以利用配置文件来解决更改DataAccess的问题。添加一个App.confing文件,给DB字符串赋值,在配置文件中写是Sqlserver还是Access,这样就边DataAccess类也不用更改了。
现在我们应用于反射+抽象工厂模式解决了数据库访问的可维护、可扩展的问题。
从这个角度来说,所有在用简单工厂的地方,都可以考虑用反射技术来去除switch或if,解除分支判断带来的耦合。
15.10 无痴迷,不成功
第16章 无尽加班何时休——状态模式
16.1 加班,又是加班
上午状态好,中午想睡觉,下午渐恢复,加班苦煎熬。其实是一种状态的变化,不同的时间,会有不同的状态。用代码实现其实就是根据时间的不同,做出判断来实现。
16.2 工作状态-函数版
定义一个“写程序”的函数,用来根据时间的不同体现不同的工作状态:WriteProgram(){if(Hour< 12){}else if(Hour<13){}}
16.3 工作状态-分类版
定义work工作类,提供void WriteProgram()方法,判断不同的时间状态
16.4 方法过长是坏味道
方法很长,而且有很多的判断分支,这也就意味着它的责任过大了。无论是任何状态,都需要通过它来改变,任何需求的改动或增加,都需要去更改这个方法,这实际上是很糟糕的。面向对象设计其实就是希望做到代码的责任分解。
把这些分支想办法变成一个又一个的类,增加时不会影响其他类。然后状态的变化在各自的类中完成。针对这类问题Gof提供了解决方案,那就是状态模式。
16.5 状态模式
状态模式(State):当一个对象的内在状态改变时允许改变期行为,这个对象看起来你是改变了其类。
状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。当然,如果这个状态判断很简单,那就没必要用状态模式了。
状态结构图:
abstract class State{abstract void Handle(Context context);} State类,抽象状态类,定义一个接口以封装与Context的一个特定状态相关的行为。
class ConcreteStateA:state{override void Handle(Context context){context.State = new ConcreteStateB();//设置ConcreteStateA的下一状态是ConcreteStateB}} ConcreteState类,具体状态,每一个子类实现一个与Context的一个状态相关的行为。
class ConcreteStateB:state{override void Handle(Context context){context.State = new ConcreteStateA();//设置ConcreteStateB的下一状态是ConcreteStateA}}
class Context{State state;void Request(){state.Handle(this);//对请求做处理,并设置下一状态}}Context类,维护一个ConcreteState子类的实例,这个实例定义当前的状态。
客户端 Main{Context c = new Context(new ConcreateStateA();//设置Context的初始状态为ConcreteStateA c.Request();c.Request();//不断的请求,同时更改状态)}
16.6 状态模式好处与用处
状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
其实就是将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个ConcreteState中,所以通过定义新的子类可以很容易地增加新的状态和转换。
这样的目的就是为了消除庞大的条件分支语句,在的分支判断会使得它们难以修改和扩展,就像我们最早说的刻版印刷一样,任何改动和变化都是致命的。
状态模式通过把各种状态转换逻辑分成到State的子类之间,来减少相互间的依赖,好比把整个版面改成一个又一个的活字,此时就容易维护和扩展了。
什么时候考虑使用状态模式呢?
当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。另外如果业务需求某项业务有多个状态,通常都是一些枚举变量,状态的变化都是依靠大量的分支判断语句来实现,此时应该考虑将每一种业务状态定义为一个State的子类。这样这些对象就可以不依赖于其他对象而独立变化了,某一天客户需要更改需求,增加或减少业务状态或改变状态流程,对我们来说都是不困难的事。
16.7 工作状态-状态模式版
代码结构图:
抽象状态类,定义一个抽象方法“写程序” abstract class State{abstract void WriteProgram(Work w)}
上午和中午工作状态类:class ForenoonState:State{override void WriteProgram(Work w){if(w.Hour < 12){}else{w.State(new NoonState());w.WriteProgram();//超过12点,则转入中午工作状态}}}
中午工作状态类:class NoonState:State{override void WriteProgram(Work w){if(w.Hour < 13){}else{w.State(new AfternoonState());w.WriteProgram();超过13点,则转入下午工作状态}
下午和傍晚工作状态类:
晚间工作状态类:
睡眠状态和下班休息状态类:
工作类,此时没有了过长的分支判断语句:class Work{Work(){sate = new ForenoonState();//工作初始化为上午工作状态,即上午9点开始上班}}
此时的代码,如果要完成“员工必须在20点之前离开公司”,我们只需要增加一个“强制下班状态”,并改动一个傍晚工作状态类的判断就可以了,而这是不影响其他状态的代码的。
第17章 在NBA我需要翻译——适配器模式
17.1 在NBA我需要翻译
17.2 适配器模式
适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原来由于接口不兼容而是不能一起工作的那些类可以一起工作。
适配器模式主要解决什么问题呢?
简单地说,就是需要的东西就在面前,但却不能使用,而短时间又无法改造它,于是我们想办法适配它。
适配是什么意思?
这个词最早出现在电工学里,有些国家用110V电压,而我们国家用的是220V,但我们的电器,比如笔记本电脑是不能什么电压都能用的,但国家不同,电压可能不相同也是事实,于是就用一个电源适配器,只要是电,不管多少伏,都能把电源变成需要的电压,这就是电源适配器的作用。适配器的意思就是使得一个东西适合另一个东西的东西。
姚明刚到NBA打球,之前又没有时间在学校里认真学好英语,马上学到可以听懂会说的地步是很困难的。
在我们不能更改球队的教练、球员和姚明的前题下,我们能做的就是想办法找个适配器。
在软件开发中,也就是系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使用控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是掊口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有实际价值。
适配器(Adapter)结构图:
class Target{virtual void Request(){普通请求}};Target这是客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
class Adaptee{void SpecificRequest(){特殊请求};};Adaptee需要适配的类
class Adapter:Target{new Adaptee()//建立一个私有的Adaptee对象;override void Request(){adaptee.SpecificRequest();//这样就可以把表面上调用Request()方法变成实际调用SpecificRequest()};}
class Main{Target target = new Adater();target.Request();//对客户端来说,调用的就是Target的Request()};客户端类
17.3 何时使用适配器模式
在想使用一个已经存在的类,但如果它的接口,也就是它的方法和你的要求不相同是时,就应该考虑用适配器模式。
两个类所做的事情相同或相似,但是具有不同的接口时要使用它。而且由于类都共享同一个接口,使用客户代码可以统一调用同一接口,这样可以更简单、更直接、更紧凑。
其实用适配器模式也是无奈之举,很有点“亡羊补牢”的感觉,没办法,是软件就有维护的一天,维护就有可能会因不同的开发人员、不同的产品、不同的厂家而造成功能类似而接口不同的情况,此时就是适配器模式在展拳脚的时候了。
有没有之初就需要考虑用适配器模式的时候?
当然有,比如公司设计一系统时考虑使用第三方开发组件,而这个组件的接口与我们自己的系统接口是不相同的,而我们也完全没有必要了为迎合它而改动自己接口,此时尽管是在开发的设计阶段,也是可以考虑用适配器模式来解决接口不同的问题。
17.4 篮球翻译适配器
17.5 适配器模式的.NET应用
在.NET中有一个类库已经实现的、非常重要的适配器,那就是DataAdaper。DataAdapter用作DataSet和数据源之间的适配器以便检索和保存数据。DataAdapter通过映射Fill(这更改了DataSet中的数据以便与数据源中的数据相匹配)和Update(这更改了数据源中的数据以便与DataSet中的数据相匹配)来提供这一适配器。由于数据源可能是来自SQL Server,可能来自Oracle,也可能来自Access、DB2,这些数据在组织上可能有不同之处,但我们希望得到统一的DataSet(实质是XML数据),此时用DataAdapter就是非常好的手段,我们不必关注不同数据库的数据细节,就可以灵活的使用数据。
17.6 扁鹊的医术(详情见书)
如果能事先预防接口不同的问题,不匹配问题就不会发生;在有小的接口不统一问题发生时,及时重构,问题不至于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配。
事后控制不如事中控制,事中控制不如事前控制。适配器模式当然是好模式,但如果无视它的应用场合而盲目使用,其实是本末倒置了。
第18章 如果再回到从前——备忘录模式
18.1 如果再给我一次机会......
单机的PC游戏,通常都是在打大Boss之前,先保存一个进度,然后如果通关失败了,可以再返回刚才那个进度来恢复原来的状态,从头来过。
通常这种保存都是存在磁盘上了,以便日后读取。但对于一些更为常规的应用,比如我们下棋时需要悔棋、编写文档时需要撤销、查看网页时需要后退,这些相对频繁而简单的恢复并不需要存在磁盘中,只要将保存在内存中的状态恢复一下即可。这是更普通的应用,很多开发中都会用到。
实现场景,用代码实现。一游戏角色有生命力、攻击力、防御力等等数据,在打Boss前和后一定会不样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗前。
18.2 游戏存进度
class GameRole{int vit;//生命力 int atk;//攻击力 int def;//防御力 void StateDisplay(){//状态显示};void GetInitState(){vit=atk=def=100//获得初始状态};void Fight(){vit=atk=def=0//战斗}};游戏角色类,用来存储角色的生命力、攻击力、防御力的数据。
class Main{front = new GameRole();//大战Boss前获得初始角色状态 backup = new GameRole();backup = front;//保存进度,通过游戏角色的新实例,来保存进度 front.Fight();大战Boss时,损耗严重所有数据全部损耗为零 front = backup;游戏结束不甘心,恢复之前进度,重新来玩};
以上代码问题很多,主要在于这客户端的调用。因为这样写就把整个游戏角色的细节暴露给了客户端,你的客户端职责就太大了,需要知道游戏角色的生命力、攻击力、防御力这些细节,还要对它进行备份。以后需要增加新的数据,例如增加魔法力或修改现在的某种力,例如生命力改为经验值,这部分就一定要修改。同样的道理也存在于恢复时的代码。
显然,我们希望的时把这些游戏角色的存取状态细节封装起来,而且最好是封装在外部的类当中。以体现职责分离。
18.3 备忘录模式
备忘录(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
备忘录模式结构图:
Originator(发起人。对应游戏角色类):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator可根据需要决定Memento存储Originator的哪些内部状态。
Memento(备忘录):负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问备忘录Memento。备忘录有两个接口,Caretaker只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originagor能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。
Caretakeer(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查。
就刚才的例子,游戏角色类其实就是一个Originator,而用了同样的游戏角色实例备份来做备忘录,这在当需要保存全部信息时,是可以考虑的,而用clone(克隆)的方式来实现Memento的状态保存可能是更好的办法,但是如果是这样的话,使得我们相当于对上层应用开放了Originator的全部(public)接口,这对于保存备份有时候是不合适的。
如果不需要保存全部的信息以备使用时,这或许是更多可能发生的情况,我们需要保存的并不是全部信息,而只是部分,那么就应该有一个独立的备忘录类Memento,它只拥有需要保存的信息的属性。
18.4 备忘录模式基本代码
发起人类:class Originator{string state;//需要保存的属性,可能有多个 Memento CreateMemento(){return (new Memento(state));//创建备忘录,将当前需要保存的信息导入并实例化一个Memento对象};SetMenento(Memento memento){state = memento.State;//恢复备忘录,将Memento导入并将相关数据恢复}}。
备忘录类:class Memento{string state;Memento(string state){//构造方法,将相关数据导入};string State{get{return state;//需要保存的数据属性,可以是多个};}}。
管理者类:class Createker{Memento memento; Memento Memento{get{return memento;}set{memento = value;}//得到或设置备忘录}}。
客户端类:class Main{Originator o = new Originator();o.State="on";//Originator初始状态,状态属性为On Caretakeer c = new Caretakeer();c.Memento = o.CreateMemento();//保存状态时,由于有了很好的封装,可能隐藏Originator的实现细节 o.State = "off";//Originator改变了状态属性为off o.SetMenento(c.Memento);//恢复原初始状态}
这当中就是把要保存的细节给封装在了Memento中了,哪一天要更改保存的细节也不用影响客户端了。
备忘录模式都用在一些什么场合呢?
Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator可以根据保存的Memento信息还原到前一状态。
命令模式也有实现类似撤销的作用,如果在某个系统中使用命令模式时,需要实现命令的撤销功能,那么命令模式可以使用备忘录模式来存储可撤销操作的状态。有时一些对象的内部信息必须保存在对象以外的地方,但是必须要由对象自己读取,这时,使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
其最大的作用还是在当角色的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。
18.5 游戏进度备忘
备忘录模式也是有缺点的,角色状态需要完整存储到备忘录对象中,如果状态数据很大很多,那么在资源消耗上,备忘录对象会非常耗内存。所以也不是用得越多越好。
第19章 分公司=-部门——组合模式
19.1 分公司就就是一部门吗?
有一家公司,总部在北京,在全国几大城市高有分公司,比如上海设有华东区分部,然后在一些省会城市还设有办事处,比如南京办事处、杭州办事处。现在有个问题是,总公司的人力资源部、账务部等办公管理功能在所有的分公司或办事处都需要有,该怎么办?
之前讲过简单的复制是最糟糕的设计,所以想法是共享功能到各个分公司,也就是让总部、分公司、办事处用同一套代码,只是根据ID的不同来区分。
这种方法不可区,因为他们的要求,总部、分部和办事处是成树状结构的,也就是有组织结构的,不可以简单的平行管理,因为实际开发时就得一个一个的判断它是总部,还是分公司的财务,然后再执行其相应的方法。
这种情况很多见,例如卖电脑的商家,可以卖单独配件也可以卖组装整机,又如复制文件,可以一个一个文件复制粘贴还可以整个文件夹进行复制,再比如文本编辑,可以给单个字加粗、变色、改字体,当然也可以给整段文字做同样的操作。其本质都是同样的问题。
其实分公司或办事处与总公司的关系,就是部分与整体的关系。
希望总公司的组织结构,比如人力资源部、财务部的管理功能可以复用于分公司。这其实就是整体与部分可以被一致对待的问题。
来分析一下刚才讲到的这个项目,如果把北京总公司当做一棵大树的根部的话,它的下属分公司其实就是这棵树的分析,至于各办事处是更小的分支,而它们的相关的职能部门由于没有分枝了,所以可以理解为树叶。
也就是说,所希望的总部的财务管理功能也最好是能复用到子公司,那么最好的办法就是,我们在处理总公司的账务管理功能和处理子公司的账务管理功能的方法都是一样的。这涉及到一个设计模式叫组合模式。
19.2 组合模式
组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
组合模式结构图:
Component为组合中的对象声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component的子部件。abstract class Component{void Add(Component c){};void Remove(Component c){};//通常都用Add和Remove方法来提供增加或移除树叶或树叶的功能 void Display(int depth){}}
Leaf在组合中表示叶节点对象,叶节点没有子节点。calss Leaf:Component{override void Add(Component c){};override void Remove(Component c){};};由于叶子没有再增加分枝和树叶,所以Add和Remove方法实现它没有意义,但这样做可以消除叶节点和枝节点对象在抽象层次的区别,它们具备完全一致的接口。
Composite定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关的操作,比如增加Add和删除Remove。class Composite:Component{children=new List<Component>();//一个子对象集合用来存储其下属的枝节点和叶节点 override void Add(Component c){children.Add(c)};override void Remove(Component c){children.Remove(c)};override void Display(int depth){foreach(Component component in children){component.Display(depth + 2)}}//显示其枝节点名称,并对其下级进行遍历}
客户端代码,能通过Component接口操作组合部件的对象。class Main{Composite root = new Composite("root");//生成树根据root,根上长出两叶LeafA和LeafB}
19.3 透明方式与安全方式
树可能有无数的分枝,但只需要反复用Composite就可以实现树状结构了。有点疑问,为什么Leaf类当中也有Add和Remove,树叶不是不可以再长分枝吗?
是的,这种方式叫做透明方式,也就是说在Component中声明所有来管理子对象的方法,其中包括Add、Remove等。这样实现Component接口的所有子类都具备了Add的Remove。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为Leaf类本身不具备Add()、Remove()方法的功能,所以实现它是没有意义的。
那么如果不希望做这样的无用功呢?也就是Leaf类当中不用Add和Remove方法,可以吗?
当然是可以的,那么就需要安全方式,也就是在Component接口中不去声明Add和Remove方法,那么子类的Leaf也就不需要去实现它,而是在Composite声明所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类将不具备有相同的接口,客户端的调用需要做相应的判断,带来了不便。
19.4 何时使用组合模式
什么地方用组合模式比较好呢?
当发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。
以前曾经用过的ASP.NET和TreeView控件就是典型的组合模式应用。还有自定义控件,也就是把一些基本的控件组合起来,通过编程写成一个定制的控件,比如用两个文本框和一个按钮就可以写一下自定义的登录框控件,所有的Web控件的基类都是System.Web.UI.Control,而Control基类中就有Add和Remove方法,这就是典型的组合模式的应用。
19.5 公司管理系统
公司抽象类或接口 abstract class Company{void Add(Company c);//增加 void Remove(Company c);//移除 void LineOfDuty();//履行职责,不同的部门需要履行不同的职责}
具体公司类,实现接口 树枝节点 class ConcreteCompany:Company{children = new List<Company>();override void LineOfDuty(){foreach(Company component in children){component.LineOfDuty();}}}
人力资源部与账务部类 树叶节点 class HRDepartment:Company{override void LineOfDuty(){"{0}员工招聘培训管理",name}}
账务部 树叶节点 class FinanceDepartment:Company{override void LineOfDuty(){"{0}公司账务收支管理",name}}
客户端调用 class Main{ConcreteCompany root = new ConcreteCompany("北京总公司");root.Add(new HRDepartment(“总公司人力资源部”));root.Add(new FinanceDepartment("总公司账务部"));ConcreteCompany root = new ConcreteCompany("上海华东分公司")}
19.6 组合模式好处
组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象了。
还有,用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。简单点说,就是组合模式让客户可以一致地使用组合结构和单个对象。
第20章 想走?可以!先买票——迭代器模式
20.1 乘车买票,不管你是谁!
每个售票员都在做一件重要的事,就是把车厢里的所有人都遍历了一遍,不放过一个不买票的乘客。这也是一个设计模式的体现。迭代器模式。
20.2 迭代器模式
迭代器模式(Iterator):提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
售票员不管你上来的是人不是物(行李),不管是中国人还是外国人,不管是不是内部员工,甚至哪怕是马上要抓走的小偷,只要是来乘车的乘客,就必须要买票。同样道理,当你需要访问一个聚集(集合)对象,而且不管这些对象是什么都需要遍历的时候,就应该考虑用迭代器模式。另外,售票员从车头到车尾来售票,也可以从车尾向车头售票,也就是说,你需要对聚集有多种方式遍历时,可以考虑用迭代器模式。
由于不管乘客是什么,售票员的做法始终是相同的,都是从第一个开始,下一个是谁,是否结束,当前售到哪个人了,这些方法每天他都在做,也就是说,为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一接口。
20.3 迭代器实现
迭代器模式结构图:
abstract class Iterator{object First();object Next();boolean IsDone();object CurrentItem();} 迭代抽象类,用于定义得到开始对象、得到下一个对象、判断是否到结尾、当前对象等抽象方法,统一接口。
abstract class Aggregate{abstract Iterator CreateIterator();//创建迭代器}聚集(集合)抽象类
class ConcreateIteator:Iterator{ConcreteAggregate aggregate;//定义一个具体的聚集(集合对象);override object First(){return aggregate[0]}}
class ConcreateAggregte:Aggregate{private List<object> items = new List<object>;//声明一个List泛型变量,用于存放聚合(集合)对象,用ArrayList同样可以实现;override Iterator CreateIterator(){return new ConcreateIteator(this)}}
客户端类class Main{ConcreteAggregate a = new ConcreateAggregte();//公交车,即聚集(集合)对象 a[0]=老外;a[0]=小偷;//新上来的乘客,即对象数组 Iterator i = new ConcreateIteator(a);//售票员出场,先看好了上车的是哪些人,即声明迭代器对象}
为什么要用具体的迭代器ConcreteIterator来实现抽象的Iterator呢?感觉这里不需要抽象,直接访问ConcreteIterator不是更好吗?
那是因为刚才有一个迭代器的好处没注意,当你需要对聚集(集合)有多种方式遍历时,可以考虑用迭代器模式,事实上,售票员不一定要从车头到车尾这样售票,还可以从后向前遍历。
20.4 .NET的迭代器实现
不只有.NET,java也同样提供有现成的迭代器框架相关接口,只需要去实现就好。例如ArrayList:
我们不需要显式的引用迭代器,但系统本身还是通过迭代器来实现遍历的。总地来说,迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器来负责,这样即可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
20.5 迭代高手
第21章 有些类也需要计划生育——单例模式
21.1 类也需要计划生育
每点击一次工具箱的菜单项,就产生一个新的工具箱窗体,但实际上,只希望它出现一次,或者干脆不出现。
21.2 判断对象是否是null
这个其实不难办,判断一下,这个FromToolbox有没有实例化过不就行了。
为什么要在点击按钮时才声明FormToolbox对象呢,完全可以把声明的工作放到类的全局变量中完成。这样就可以去判断这个变量是否被实例化过了。
21.3 生不是不生是自己的责任
夫妻已经有一个小孩子,下面是否生第二胎,这是谁来负责?
当然是他们自己负责,要是超生了,违反了国家的政策,那也是他们自己的原因。
再想想这种场景:领导问下属,报告交了没有,下属可以说早交了,于是领导满意地点点头,下属也可以说还剩下一点内容没写,很快上交,领导皱起眉头说要抓紧。此时这份报告交还是没交,由谁来判断?
当然是下属自己的判断,因为下属最清楚报告交了没有,领导只需要问问就行了。
同样的,现在工具箱FromToolbox是否实例化都是在MDI主窗体From1的代码里判断,这并不合逻辑,Form1里应该只是通知启动工具箱,至于工具箱窗体是否实例化过,应该由工具箱自己来判断。
实例化其实就是new的过程,但问题就是我怎么让人家不用new呢?
是的,如果不对构造方法做改动的话,是不可能阻止他人不去用new的。所以我们完全可以直接把这个类的构造方法改成私有(private)。
所有类都有构造方法,不编码则系统默认生成空的构造方法,若有显示定义的构造方法,默认的构造方法就会失效。于是只要将工具箱类的构造方法写成是private的,那么外部程序就不能用new来实例化它了。
对于外部代码,不能用new来实例化它,但是我们完全可以重写一个public方法,叫做GetInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。如果没有实例化过。由调用private的构造方法new出这个实例,之所以它可以调用是困为它们在同一个类中,private方法可以被调用的。
这样一来,客户端不再考虑是否需要去实例化的问题,而把责任都给了应该负责的类去处理。其实这就是一个很基本的设计模式:单例模式。
21.4 单例模式
单例模式(singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。
单例模式结构图:
Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetInstance是一个静态方法,主要负责创建自己的唯一实例。
单例模式除了可以保证唯一的实例处,还有什么好处呢?
比如单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。
怎么感觉单例有点像一个实用类的静态方法,比如.net框架里的Math类,有很数据计算方法,这两者有什么区别呢?
它们之间的确很类似,实用类通常也会采用私有化的构造方法来避免其有实例。但它们还是有很多不同的:
1、比如实用类不保存状态,仅提供一些静态方法或静态属性让你使用,而单例类是有状态的。
2、实用类不能用于继承多态,而单例虽然实例唯一,却是可以有子类来继承。
3、实用类只不过是一些方法属性的集合,而单例却是有着唯一的对象实例。
21.5 多线程时的单例
需要注意一些细节,比如说,多线程的程序中,多个线程同时,注意是同时访问Singleton类,调用GetInstance()方法,会有可能造成创建多个实例的。
可以给进程一把锁来处理。这里需要解释一下lock语句(java中使用synchronized)的涵义,lock是确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
class Singleton{private static Singleton instance;private static readonly object syncRoot = new object();//程序运行时创建一个静态只读的进程辅助对象 singleton GetInstance(){lock(syncRoot){if(instance == null){instance = new Singleton();}}//在同一个时刻加了锁的那部分程序只有一个线程可以进入}}
这段代码使得对象实例由最先进入的那人线程创建,以后的线程在进入时不会再去创建对象实例了。由于有了lock,就保证了多线程环境下的同时访问也不会造成多个实例的生成。
为什么不直接lock(instance),而是再创建一个syncRoot来lock呢?
因为在加锁时,instance实例有没有被创建过实例都不知道,怎么对它加锁呢。但是还有问题,就是每次调用GetInstance方法时都需要lock,这种做法是会影响性能的,所以对这个类再做改良。
21.6 双重锁定
class Singleton{public static Singleton GetInstance(){if(instance == null)//先判断实例是否存在,不存在再加锁处理{lock(syncRoot){if(instance == null){instance = new Singleton();}}}}}
现在这样,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全。这种做法被称为Double-Check Locking(双重锁定)。
有个问题,在外面已经判断了instance实例是否存在,为什么在lock里面还需要做一次instance实例是否存在的判断呢?
仔细分析下,对于instance存在的情况,就直接返回,这没有问题。当instance为null并且同时有两个线程调用GetInstance()方法时,它们将都可以通过第一重instance==null的判断。然后由于lock机制,这两个线程则只有一个进入,另一个在外排队等候,必须要其中的一个进入并出来后,另一个才能进入。而此时如果没有第二重的instance是否为null的判断,则第一个线程创建了实例,而第二个线程还是可以继续再创建新的实例,这就没有达到单例的目的。
21.7 静态初始化
由于构造方法是私有的,因此不能在类本身以外实例化Singleton类;因此,变量引用的是可以在系统中存在的唯一的实例。
只能在静态初始化期间或在类构造函数中分配变量,这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象地称之为饿汉式单例类。
原先的单例模式处理方法是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。
饿汉-懒汉,它们主要有什么区别呢?
由于饿汉式,即静态初始化的方式,它是类一加载就实例化的对象,所以要提前占用系统资源。
懒汉式,又会面临着多线程访问的安全性问题,需要做双重锁定这样的处理才可以保证安全。所以到底使用哪一种方式,取决于实际的需求。从C#语言角度来讲,饿汉式的单例类已经足够满足我们的需求了。
第22章 手机软件何时统一——桥接模式
22.1 凭什么你的游戏我不能玩
你的手机是M品牌的,我的是N品牌的,按道理我这里的游戏你是不能玩的。
同品牌的手机、型号不同,软件还算是基本兼容,可惜不同品牌,软件基本还是不能整合在一起。
然而,在计算机领域里,就完全不一样了。比如由于有了Window操作系统,使得所有的PC厂商不用关注软件,而软件制造商也不用过多关注硬件,这对计算机的整体发展是非常有利的。
22.2 紧耦合的程序演化
手机硬件软件和PC硬件软件,其实蕴含两种完全不同的思维方式。
如果我现在有一个N品牌的手机,它有一个小游戏,我要玩游戏,程序应该如何写?
写一个此品牌的游戏类,再用客户端调用即可。
N品牌的手机中的游戏:class HandsetNGame{void Run(){//运行N品牌手机游戏}}
客户端代码:class Main{game = new HandsetNGame();game.Run();}
现在又有一个M品牌的手机,也有小游戏,客户端也可以调用。
两个品牌,都有游戏,觉得从面向对象的思想来说,应该有一个父类手机品牌游戏,然后让N和M品牌的手机游戏都继承于它,这样可以实现同样的运行方法。
由于手机都需要通讯录功能,于是N品牌和M品牌都增加了通讯录的增删改查功能。如何处理?
那就意味着,父类应该是手机品牌,下有手机品牌M和手机品牌N,每个子类下各有通讯录和游戏子类。
代码结构图:
手机类
手机品牌类:class HandsetBrand{void Run(){}}
手机品牌N和手机品牌M类:class HandsetBrandM:HandsetBrand{void Run(){}}
下属的各自通讯录类和游戏类
手机品牌M的游戏类:class HandsetBrandMGame:HandsetBrandM{override void Run(){//运行M品牌手机游戏}}
手机品牌M的通讯录类:class HandsetBrandMAddressList:HandsetBrandM{override void Run(){//运行M品牌手机通讯录}}
手机品牌N同上
如果现在需要每个品牌都增加一个MP3音乐播放功能,如何做?
那就在每个品牌的下面都增加一个子类。如果又来了一家新的手机品牌S,它也有游戏、通讯录、MP3音乐播放器功能,那就得再增加手机品牌S类和三个下属功能子类,这显然有点麻烦了。
感觉一直在用面向对象的理论设计的,先有一个品牌,然后多个品牌就抽象出一个品牌抽象类,对于每个功能,就都继承各自的品牌。或者,不从品牌,从手机软件的角度去分类,这有什么问题呢?
问题是有的,这就好比是有了新锤子,所有的东西看上去都成了钉子。但事实上,很多情况有继承会带来麻烦。比如,对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
如果按照前面的继承结构,不断地增加新品牌或新功能,类会越来越多的。
在面向对象设计中,我们还有一个很重要的设计原则,那就是合成(组合)/聚合复用原则。即优先使用对象合成(组合)/聚合,而不是类继承。
22.3 合成(组合)/聚合复用原则
合成(组合)/聚合复用原则:尽量使用合成/聚合,尽量不要使用类继承。
合成(Composition,也有翻译成组合)和聚合(Aggregation)都是关联的特殊种类。
聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分;
合成则是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
比方说,大雁有两个翅膀,翅膀与大雁是部分和整体的关系,并且它们的生命周期是相同的,于是大雁和翅膀就是合成关系。
而大雁是群居动物,所以每只大雁都是属于一个雁群,一个雁群可以有多只大雁,所以大雁和雁群是聚合关系。
合成/聚合复用原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务中。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
就刚才的例子,需要学会用对象的职责,而不是结构来考虑问题。其实答案就在之前我们聊到的手机与PC电脑的差别上。
手机是不同的品牌公司,各自做自己的软件,就像现在的设计一样,而PC却是硬件厂商做硬件,软件厂商做软件,组合起来才是可以用的机器。
实际上,像游戏、通讯录、MP3音乐播放这些功能都是软件,如果我们可以让其分离与手机的耦合,那么就可以大大减少面对新需求是改动过大的不合理情况。
意思其实就是应该有个手机品牌(硬件)抽象类和手机软件(软件)抽象类,让不同的品牌和功能都分别继承于它们,这样要增加新的品牌或新的功能都不用影响其他类了。
手机品牌和手机软件之间的关系,是手机品牌包含有手机软件,但软件并不是品牌的一部分,所以它们之间是聚合关系。
22.4 松耦合的程序
手机软件抽象类
手机软件:abstract class HandsetSoft{abstract void Run();}
游戏、通讯录等具体类
手机游戏:class HandsetGame:HandsetSoft{override void Run(){//运行手机游戏}}
手机品牌(硬件类)
手机品牌:abstract class HandsetBrand{HandsetSoft soft;void SetHandsetSoft(HandsetSoft soft){this.soft = soft;}//品牌需要关注软件,所以可以机器中安装软件(设置手机软件),以备运行 abstract void Run();}
品牌N品牌M具体类
手机品牌N:class HandsetBrandN:HandsetBrand{override void Run(){soft.Run();}}
客户端调用:
现在如果要增加一个功能,比如MP3音乐播放功能,那么只要增加这个类就和地。不会影响其他任何类。类的个数增加也只是一个。
如果是要增加S品牌,只需要增加一个品牌子类就可以了。个数也是一个,不会影响其他类的改动。
这显然是也符合了我们之前的一个开放-封闭原则。这样的设计显然不会修改原来的代码,而只是扩展类就行了。但最深的是合成/聚合复用原则,也就是优先使用对象的合成或聚合,而不是类继承。聚合的魅力无限。相比,继承的确很容易造成不必要的麻烦。
盲目使用继承当然就会造成麻烦,而其本质原因主要是什么?
应该是,继承是一种强耦合的结构。父类变,子类就必须要变。
所以我们在使用继承时,一定要在是is-a的关系时再考虑使用,而不是任何时候都去使用。今天这个例子应该的一个设计模式叫做桥接模式。
22.5 桥接模式(Bridge)
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
这里需要理解一下,什么叫抽象与它的实现分离,这并不是说,让抽象类与派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象。就刚才的例子,就是让手机即可以按照品牌来分类,也可以按照功能来分类。
桥接模式的核心意思就是把这些实现独立出来,让它们各自地变化。这就使得每种实现的变化不会影响其他实现,从而达到应对变化的目的。
22.6 桥接模式基本代码
桥接模式所说的将抽象部分与它的实现部分分离,还是不好理解。通俗的理解就是实现系统可能有多个角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。
也就是说,在发现我们需要多角度去分类实现对象,而只用继承会造成大量的类增加,不能满足开放-封闭原则时,就应该要考虑用桥接模式了。
感觉只要真正深入地理解了设计原则,很多设计模式其实就是原则的应用而已,或许在不知不觉中就在使用设计模式了。
22.7 我要开发好的游戏
第23章 烤羊肉串引来的思考——命令模式
23.1 吃烤羊肉串
打游击烤羊肉串和烤肉店哪个更赚钱。其实门店好过马路游击队,还可以对应一个很重要的设计模式。
23.2 烧烤摊vs烧烤店
烧烤摊吃烤串的人太多,都希望能最快吃到肉串,烤肉老板一个人,所以有些混乱。人一多,他就未必记得信谁交过钱,要几串,需不需要放辣等等。
由于客户和烤羊肉串老板的紧耦合所以使得容易出错,容易混乱,也容易挑剔。
这其实就是行为请求者与行为实现者的紧耦合。我们需要记录哪个人要几串羊肉串,有没有特殊要求(放辣不放辣),付没付过钱,谁先谁后,这其实都相当于对请求做记录,应该是做日志。
那么如果有人需要退回请求,或者要求烧肉重烤,这其实就是相当于撤销和重做。
所以,对请求排队或记录请求日志,以及支持可撤销的操作等行为时,行为请求者与行为实现者的紧耦合是不太适合的。
其实,我们不用去认识烧肉者是谁,连他的面都不用见到,我们只需要给接待我们的服务员说我们要什么就可以了。他可以记录我们的请求,然后再由他去通知烤肉师傅做。
而且,由于我们所做的请求,其实也就是我们点肉的订单,上面有很详细的我们的要求,所有的客户都有这一份订单,烤肉师傅可以按先后顺序操作,不会混乱,也不会遗忘了。
还有,如果想到更改订单,只需通知服务员,然后服务员会在小本上划一上,再去通知烤肉师傅了。这其实是在做撤销行为的操作。由于有了记录,所以最终算账还是不会错的。这种就是利用一个服务员来解耦客户和烤肉师傅的处理好处。
23.3 紧耦合设计
路边烤羊肉串的实现:class Barbecuer{void BackMutton(){//烤羊肉串}}
客户端调用:class Main(){boy = new Barbecure();//客户端程序与烤肉串者紧耦合,尽管简单,但却极为僵化,有许许多多的隐患}
如果用户多了,请求多了,就容易乱了。这里需要增加服务员类。
要知道,不管是烤羊肉串,还是烤鸡翅,还是其他烧烤,这些都是烤肉串者类的行为,也就是他的方法,具体怎么做都是由方法内部来实现,我们不用管它。但是对于服务员类来说,他其实就是根据用户的需要,发个命令,说:有人要十个羊肉串,有人要两个鸡翅,这些都是命令。
意思是把烤肉串者类当中的方法,分别写成多个命令类,那么它们就可以被服务员来请求了。这些命令其实差不多都是同一个样式,于是就可以泛化出一个抽象类,记服务员只管对抽象的命令发号施令就可以了。具体是什么命令,即是烤什么,由客户来决定吧。
23.4 松耦合设计
代码结构图:
抽象命令类
abstract class Command{Barbecure receiver;public Command(Barbecure receiver){this.receiver = receiver;}//抽象命令类,只需要确定烤肉串者是谁 abstract void ExcuteCommand();//执行命令}
具体命令类
烤羊肉串命令:class BakeMuttonCommand:Command{override void ExcuteCommand(){receiver.BakeMutton();//具体命令类,执行命令时,执行具体的行为}}
烤鸡翅命令:class BakeChickenWingCommand:Command{override void ExcuteCommand(){receiver.BakeChickenWing();}}
服务员类:class Waiter{Command command;//设置订单void SetOrder(Command command){this.command = command;//服务员类,不用管用户想要什么烤肉,反正都是命令,只管记录订单,然后通知烤肉串者执行即可}//通知执行void Notify(){command.ExcuteCommand();}}
客户端实现:class Main{//开店前的准备 Barbecuer boy = new Barbecuer();其他略......}
基本代码已经实现,但有几个问题,
第一,真实的情况其实并不是用户点一个菜,服务员就通知厨房去做一个,那样不科学,应该是点完烧烤后,服务员一次通知制作;
第二,如果此时鸡翅没了,不应该是客户来判断是否还有,客户哪知道有没有,应该是服务员或烤肉串者来否决这个请求;
第三,客户到底点了哪些烧烤或饮料,这是需要记录日志的,以备收费,也包括后期的统计;
第四,客户完全有可能因为点的肉串太多而考虑取消一些还没有制作的肉串。
这些问题都需要得到解决。
23.5 松耦合后
服务员类:
class Waiter{orders = new List<Command>();//增加存放具体命令的容器;设置订单void SetOrder(Command command){if("鸡翅没了"){syso.print("鸡翅没了,点别的烧烤");}else{orders.Add(command);syso.print("增加订单+时间");//记录客户所点的烧烤的日志,以备算账收钱};//取消订单void CancelOrder(Command command){orders.Remove(command)};//通知全部执行void Notify(){foreach(Command cmd in orders){cmd.ExcuteCommand();//根据用户点好的烧烤订单通知厨房制作}}}
这就是大名鼎鼎的命令模式。
23.6 命令模式
命令模式(Command):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式结构图:
Command类,用来声明执行操作的接口。abstract class Command{Receiver receiver;Command(Receiver receiver){this.receiver = receiver;} abstract void Execute();}
ConcreteCommand类,将一个接收者对象绑定于一个动作,调用接收者相应的操作,以实现Execute。class ConcreteCommand:Command{ConcreteCommand(Receiver receiver){base(receiver)};override void Execute(){receiver.Action();})}
Invoker(服务员)类,要求该命令执行这个请求。class Invoker{Command command;void setCommand(Command command){this.command= command};void ExceutCommand(){command.Execute();}}
Receiver(烤肉师傅)类,知道如何实施与计划一个与请求相关的操作,任何类都可能作为一个接收者。class Receiver{void Action(){//执行请求}}
客户端代码,创建一个具体命令对象并设定它的接收者。class Main{r = new Receiver();c = new ConcreteCommand(r);i = new Invoker();i.SetCommand(c);i.ExceutCommand();}
23.7 命令模式作用
总结命令模式的优点。
第一,它能较容易地设计一个命令队列;
第二,在需要的情况下,可以较容易地将命令记入日志;
第三,允许接收请求的一方决定是否要否决请求;
第四,可以容易地实现对请求的撤销和重做;
第五,由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很窠。
最关键的优点就是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。
是否是碰到类似情况就一定要实现命令模式呢?
这就不一定,比如命令模式支持撤销/恢复操作功能,但还不清楚是否需要这个功能时,要不要实现命令模式?
其实应该是不要实现。敏捷开发原则告诉我们,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现这个模式并不困难,只有在真正需要如撤销/恢复操作等功能时,把原来的代码重构命令模式才有意义。
第24章 加薪非要老总批?——职责链模式
24.1 老板,我要加薪
满试用期马上要办转正手续,提提加薪的事,向经理提出,经理去找人力资源总监,总监又去找总经理。
24.2 加薪代码初步
无论加薪还是请假,都是一种申请。申请就应该有申请类别、申请内容和申请数量。
申请:class Request{string requestType;//申请类别 string requestContent;//申请内容 int number;//数量}
然后经理、总监、总经理都是管理者。
管理者:class Manager{string name;void GetResult(string managerLevel,Request request){if(managerLevel=='经理'){}else if(managerLevel=='总监'){}else if(managerLevel=='总经理'){}}}
客户端:class Main{jinli = new Manager('1');zongjian = new Manager('2');//两个管理者 request=new Reqeust()//加薪请求;jinli.GetResult('经理',request)request=new Reqeust()//请假请求;jinli.GetResult('经理',request)}
管理者类,里面的结果方法比较长,加上有太多的分支判断,其实是非常不好的设计。因为如果再增加其他的管理类加,比如项目经理、部门经理等等。那就意味着都需要去更改这个类,这个类承担了太多的责任,违背了单一职责原则,增加新的管理类别,需要修改这个类,违背了开放-封闭原则。
需要重构,刚提到可能会增加管理类别,那就意味着这里容易变化,可以把这些公司管理者的类别各做成管理者的子类,这就可以利用多态性来化解分支带来的僵化。
如何解决经理无权上报总监,总监无权再上报总经理这样的功能呢?它们之间有一定的关联,把用户的请求传递,直到可以解决这个请求为止。这里引用一个行为设计模式职责链模式。
24.3 职责链模式
职责链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
这里发出这个请求的客户端并不知道这当中的哪一个对象最终处理这个请求,这样系统的更改可以在不影响客户端的情况下动态地重新组织和分配责任。
职责链模式结构图:
Handle类,定义一个处理请求的接口。
abstract class Handle{Handle successor;void setSuccessor(Handle successor){this.successor=successor;}//设置继任者 abstract void HandleReqeust(int request);//处理请求的抽象方法}
ConcreteHandler类,具体处理者类,处理它所负责的请求,可访问它的后继者,如果可处理该请求,就处理之,否则就将该请求转发给它的后继者。
ConcreteHandler1,当请求数在0到10之间则有权处理,否则转到下一位。
class ConcreteHandler1:Handle{override HandleReqeust(int request){if(request>=0 && request <10){//处理请求}else if(successor != null){successor.HandleReqeust(request);//转移到下一位}}}
ConcreteHandler2,当请求数在10到20之间则有权处理,否则转到下一位。
class ConcreteHandler2:Handle{override HandleReqeust(int request){if(request>=10 && request <20){//处理请求}else if(successor != null){successor.HandleReqeust(request);//转移到下一位}}}
客户端代码,向链上的具体处理者对象提交请求。
class Main{h1 = new ConcreteHandler1();h2 = new ConcreteHandler2();h1.SetSuccessor(h2);//设置职责链上家与下家 int[]reqeusts = {2,5,8,13,27};foreach(int request in reqeusts){h1.HandleReqeust(request);//循环给最小处理者提交请求,不同的数额由不由权限处理者处理}}
24.4 职责链的好处
这当中最关键的是当客户提交一个请求时,请求是沿链传递直至有一个ConcreteHandler对象负责处理它。
这样做的好处就是请求都不用管理哪个对象来处理,反正该请求会被处理就对了。
这就使得接收者和发送者都没有对方的明确信息,且链中的对象自己并不知道链的结构。结果是职责链可简化对象的相互连接,它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。这也就大大降低了耦合度了。
由于是在客户端来定义链的结构,也就是说,可以随时地增加或修改处理一个请求的结构。增强了给对象指派职责的灵活性。
这的确是很灵活,不过也要当心,一个请求极有可能到了链的末端都得不到处理,或者因为没有正确配置而得不到处理,需要事先考虑全面。这就跟现实中邮寄一封信,因地址不对,最终无法送达一样。
就刚才的例子而言,最重要的有两点,一个是你需要事先给每个具体管理者设置他的上司是哪个类,也就是设置后继者。另一点是你需要在每个具体管理者处理请求时,做出判断,是可以处理这个请求,还是必须要推卸责任,转移给后继者去处理。
其实就是把现在写的这个管理者类当中的那些分支,分解到每一个具体的管理者类当中,然后利用事先设置的后继者来实现请求处理的权限问题。
24.5 加薪代码重构
先来改造这个管理者类,此时它将成为抽象的父类,其实就是Handler;
代码结构图:
管理者:abstract class Manager{string name;Manager superior;//管理者上级 void SetSuperior(Manager superior){this.superior = superior;}关键的方法,设置管理者的上级 abstract void ReqeustApplications(Reqeust reqeusts);//申请请求}
经理类就可以继承这个管理者类,只需要重写申请请求的方法就可以。
经理:class CommonManager:Manager{override void ReqeustApplications(Reqeust reqeusts){if(request.RequestType=='请假'&&request.Number<=2){//被批准}else{if(superior != null){superior.ReqeustApplications(request);//其余的申请都需要转到上级}}}}
总监类同样继承管理者类:略
总经理的权限就全部都需要处理:略
由于把原来的一个管理者类改成了一个抽象类和三个具体类,此时类之间的灵活性就大大增加了。如果我们需要扩展新的管理者类别,只需要增加子类就可以。
注:经常看到这样的代码,一连串类似的行为,只是数据或者行为不一样。如一堆校验器,如果成功怎么样、失败怎么样;或者一堆对象构建器,各去构造一部分数据。碰到这种场景,我总是喜欢定义一个通用接口,入参是完整的要校验/构造的参数,出参是成功/失败的标示或者是void。然后有很多实现器分别实现这个接口,再用一个集合把这堆行为串起来。最后,遍历这个集合,串行或者并行的执行每一部分的逻辑。
这样做的好处是:
很多通用的代码可以在责任链原子对象的基类里实现;
代码清晰,开闭原则,每当有新的行为产生的时候,只需要定义行的实现类并添加到集合里即可;
为并行提供了基础。
24.6 加薪成功
第25章 世界需要和平——中介者模式
25.1 世界需要和平
中介者模式又叫做调停者模式。其实就是中间人或者调停者的意思。
由于各国之间代表的利益不同,所以矛盾冲突是难免的,但如果有这样一个组织,由各国的代表组成,用来维护国际和平与安全,解决国际间经济、社会、文化和人道主义性质的问题,这就是联合国组织。它就是一个调停者、中介者的角色。
国与国之间的关系,就类似于不同的对象与对象之间的关系,这就要求对象之间需要知道其他所有对象,尽管将一个系统分割成许多对象通常可以增加可复用性,但是对象间相互连接的激增又会降低其可复用性了。为什么会这样?
因为大量的连接使得一个对象不可能在没有其他对象的支持下工作,系统表现为一个不可分割的整体,所以,对系统的行为进行任何较大的改动就十分困难了。
要解决这样的问题,需要应用之前讲过的迪米特法则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。在这里,意思就是说,国与国之间完全可以通过联合国这个中介来发生关系,而不用直接通信。
通过中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,每个具体对象不再通过直接的联系与另一个对象发生相互作用,而是通过中介者对象与另一个对象发生相互作用。
中介者对象的设计,使得系统的结构不会因为新对象的引入造成大量的修改工作。
25.2 中介者模式
中介者模式(Mediator):用一个中介对象来封闭一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式结构图:
Colleague叫做抽象同事类;
ConcreteColleague是具体同事类,每个具体同事只知道自己的行为,而不了解其他同事类的接口,但它们却都认识中介者对象。
Mediator是抽象中介者,定义了同事对象到中介者对象的接口。
ConcreteMediator是具体中介者对象,实现抽象类的方法,它需要知道所有具体同事类,并从具体同事接收消息,向具体同事对象发出命令。
Mediator类抽象中介者类:abstract class Mediator{abstract void Send(string message,Colleague colleague);//定义一个抽象的发送消息方法,得到同事对象和发送消息}
Colleague类抽象同事类:abstract class Colleague{Mediator mediaotr;Colleague(Mediator mediaotr){this.mediaotr=mediaotr;//构造方法,得到中介者对象}}
ConcreteMediator类具体中介类:class ConcreteMediaotr:Mediator{ConcreteColleague1 Colleague1{set{colleague1 = vlaue;}ConcreteColleague2 Colleague2{set{colleague2=value;}}override void send(){if(colleague==colleague1){colleague2.Notify(message);}else{colleague1.Notify(message;)//重写发送信息的方法,根据对象做出选择判断,通知对象}}}}
ConcreteColleague1和ConcreteColleague2等各种同对象:class ConcreteColleague1:Colleague{//构造ConcreteColleague1(Mediator mediaotr){mediaotr;}void Send(string message){mediaotr.Send(message,this);//发送信息时通常是中介者发送出去的}void Notify(string message){syso.print('同事1得到消息'+message);}}
客户端调用:class Main{//略}
由于有了Mediator,使得ConcreteColleague1和ConcreteColleague2在发送消息和接收信息时其实是通过中介者来完成的,这就减少了它们之间的耦合度了。
这里人个问题,美国和伊拉克都是国家,有一个国家抽象类和两个具体国家类就可以了,但联合国到底是Mediator还是ConcreteMediator呢?
这要取决于未来是否有可能扩展中介者对象,比如觉得联合国除了安理会,还有如国际劳工组织、教科文组织、世界卫生组织等等,所以Mediator应该是联合国机构,而安理会是一个具体的中介者。
如果不存在扩展情况,那么Mediator可以与ConcreteMediator合二为一。
25.3 安理会做中介
联合国机构类,相当于Mediagor类
联合国机构类:abstract class UnitedNations{abstract void Declare(string message,Country colleague);//声明}
国家类,相当于Colleague类
国家类:abstrace class Country{UnitedNations mediaotr;Country(UnitedNations mediaotr){this.mediaotr = mediaotr;}}
美国类,相当于ConcreteColleague1类
美国类:class USA:Country{USA(UnitedNations mediaotr){base(mediaotr)};void Declare(string message){mediaotr.Declare(mess,this);//声明}void GetMessage(string message){syso.print('美国获取对方信息'+message);}}
伊拉克类,相当于ConcreteColleague2类
伊拉克类:class Iraq:Country{Iraq(UnitedNations mediaotr){base(mediaotr)};void Declare(string message){mediaotr.Declare(mess,this);//声明}void GetMessage(string message){syso.print('伊位克获取对方信息'+message);}}
联合国安理会,相当于ConcreteMediator类
联合国安理会:class UnitedNationsSecurityCouncil:UnitedNations{USA colleague1;Iraq colleague2;//美国set{colleague1=value;}//伊拉克set{colleague2=value;}//联合国安理会了解所有的国家,所以拥有美国和伊拉克的对象属性 override void Declare(string message,Country colleague){if(colleague==colleague1){colleague2.GetMessage(message);//重写了声明方法,实现了两个对象间的通信}else{colleague1.GetMessage(message);}}}
客户端类:略
有个问题需要思考,最关键的问题在于ConcreteMediator这个类必须要知道所有的ConcreteColleague,尽管这样的设计可以减少了ConcreteColleague类之间的耦合,但这又使得ConcreteMediator责任太多了,如果出了问题,则整个系统都会有问题。
25.4 中介者模式优缺点
是的,如果联合国安理会出了问题,当然会对世界都造成影响。所以说,中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了多对多交互复杂的对象群时,不要急于使用中介者模式,而要先反思你的系统在设计上是不是合理。总结下中介者模式的优缺点。
中介者的优点:首先是Mediator的出现减少了各个Colleague的耦合,使得可以独立地改变和复用各个Colleague类和Mediator,比如任何国家的改变不会影响到其他国家,而只是与安理会发生变化。其次,由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。
中介者的缺点:刚才提到的,具体中介者类ConcreteMediator可能会因为ConcreteColleague的越来越多,而变得非常复杂,反而不容易维护。由于ConcreteMediator控制了集中化,于是就把交互复杂性变为了中介者的复杂性,这就使得中介者变得比任何一个ConcreteColleague都复杂。中介者模式的优点来自集中控制,其缺点也是它,使用时要考虑清楚。
中介者模式应用于平时用.NET写的Windows应用程序中的Form或Wdb网站程序的aspx就是典型的中介者。
比如计算器程序,它上面有菜单控件、文本控件、多个按钮控件和一个Form窗体,每个控件之间的通信都是通过Form窗体来完成的。因为每个控件类代码都被封装了,所以它们的实例是不会知道其他控件对象的存在的,比如点击数字按钮要在文本框中显示数字,按照以前的想法就应该要在Button类中编写给TextBox类实例的Text属性赋值的代码,造成两个类有耦合,这显然是非常不合理的。但实际情况是它们都有事件机制,而事件的执行都是在Form窗体的代码中完成,也就是说所有的控件的交互都是由Form窗体来作中介,操作各个对象,这就是典型的中介者模式应用。
中介者模式一般应用于一组对象以定义良好但是复杂的方式进行通信的场合,比如刚才得到的窗体Form对象或Web页面aspx,以及想定制一个分布在多个类中的行为,而又不想生成太多的子类的场合。
第26章 项目多也别傻做——享元模式
26.1 项目多也别傻做
私营业主做网站,有100家类似的商家客户,要求基本上都是信息发布、产品展示、博客留言、论坛功能等。难道去申请100个空间,用100个数据库,然后用类似的代码复制100遍?如果有Bug或是新的需求改动,维护量就太可怕了。
现在的大型博客网站、电子商务网站,里面每一个博客或商家也可以理解为一个小的网站,但它们是如何做的?
利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。
如何共享一份实例呢?
26.2 享元模式
享元模式(Flyweight):运用共享技术有效地支持大量细粒度的对象。
享元模式结构图:
Flyweight类,它是所有具体享元类的超类或接口,通过这个接口,Flyweight可以接受并作用于外部状态。
abstract class Flyweight{abstract void Operation(int extrinsicstate);}
ConcreteFlyweight是继承Flyweight超类或实现Flyweight接口,并为内部状态增加存储空间。
class ConcreteFlyweight:Flyweight{override void Operation(int extrinsicstate){syso.print("具体Flyweight:"+extrinsicstate);}}
UnsharedConcreteFlyweight是指那些不需要共享的Flyweight子类。因为Flyweight接口共享成为可能,但它并不强制共享。
class UnsharedConcreteFlyweight:Flyweight{override void Operation(int extrinsicstate){syso.print("不共享的具体Flyweight"+extrinsicstate);}}
FlyweightFactory,是一个享元工厂,用来创建并管理Flyweight对象。它主要用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个(如果不存在的话)。
class FlyweightFactory{Hashtable flyweights = new Hashtable();FlyweightFactory(){flyweights.Add("X",new ConcreteFlyweight());flyweights.Add("Y",new ConcreteFlyweight();)}//根据客户端请求,获得已生成实例Flyweight GetFlyweight(String key){return ((Flyweight)flyweights[key];)}}
客户端代码:
class Main{int extrinsicstate =22;//代码外部状态 f = new FlyweightFactory();fx = f.GetFlyweight("x");fx.Operation(--extrinsicstate);fy.Operation(--extrinsicstate);uf = new UnsharedConcreteFlyweight();uf.Operation(--extrinsicstate);}
有个问题,FlyweightFactory根据客户需要返回早已生成好的对象,但一定要事先生成对象实例吗?
实际上是不一定需要的,完全可以初始化时什么也不做,到需要时,再去判断对象是否为null来决定是否实例化。
还有个问题,为什么要有UnsharedConcreteFlyweight的存在呢?
这是因为尽管我们大剖分时间都需要共享对象来降低内存的损耗,但个别时候也有可能不需要共享的,那么此时的UnsharedConcreteFlyweight子类就有存在的必要了,它可以解决那些不需要共享对象的问题。
26.3 网站共享代码
网站抽象类:abstract class WebSite{abstract void Use();}
具体网站类:class ConcreteWebSite:WebSite{ConcreteWebSite(String name){this.name=name;} override void Use(){sys.print("网站分类""+name)}}
网站工厂类:class WebSiteFactory{flyweights = new Hahstable();//获得网站分类,判断是否存在这个对象,如果存在,则直接返回,若不存在,则实例化它再返回WebSite GetWebSiteCategory(string key){if(!flyweights.Containskey(key)flyweights.Add(key,new ConcreteWebSite(key));return ((WebSite)flyweights[key]);)}//获取网站分类总数,得到实例的个数int GetWebSiteCount(){return flyweights.Count();}}
客户端代码:class Main{f = new WebSiteFactory();//实例化产品展示的网站对象WebSite fx = f.GetWebSiteCategory("产品展示");fx.Use();//共享上方生成的对象,不再实例化WebSite fy = f.GetWebSiteCategory("产品展示");fy.Use();//博客等其它分类以此为例}}
这样写算是基本实现了享元模式的共享对象的目的,也就是说,不管建几个网站,只要是产品展示,都是一样的,只要是博客,也是完全相同的,但这样是有问题的,你给企业建的网站不是一家企业的,它们的数据不会相同,所以至少它们都应该有不同的账号,那该怎么办?
实际上这样写没有体现对象间的不同,只体现了它们共享的部分。
26.4 内部状态与外部状态
在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态,而随环境改变而改变的、不可以共享的状态就是外部状态了。
事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递过来,就可以通过共享大幅度地减少单个实例的数目。
也就是说,享元模式Flyweight执行时所需的状态是内部的也可能是外部的,内部状态存储于ConcreteFlaweight对象之中,而外部对象则应该考虑由客户端对象存储或计算,当调用Flyweight对象的操作时,将该状态传递给它。
例如,客户的账号就是外部状态,应该由专门的对象来处理。
代码结构图:
用户类,用于网站的客户账号,是网站类的外部状态:class User{string name;//提供get-set}
网站抽象类:abstract class WebSite{abstract void Use(User user);//使用方法需要传递用户对象}
具体网站类:略
网站工厂类:略
客户端代码:class Main{f = new WebSiteFactory();WebSite fx = f.GetWebSiteCategory("产品展示"");fx.Use(new User("娇娇");fy = f.GetWebSiteCategory("产品展示");fy.Use(new User("老童"));//博客略}
尽管有不同用户使用网站,但实际上只有两个网站实例。这样就可以协调内部与外部状态了。
由于用了享元模式,哪怕你接手了100个网站的需求,只要要求相同或类似,你的实际开发代码也就是分类的那几种,对于服务器来说,占用硬盘空间、内存、CPU资源都是非常少的,这确实是很好的一个方式。
26.5 享元模式应用
现实中什么时候才应该考虑使用享元模式呢?
如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。
在实际使用中,享元模式到底能达到什么效果呢?
因为用了享元模式,所以有了共享对象,实例总数就大大减少了,如果共享的对象越多,存储节约也就越多,节约量随着共享状态的增多而增大。
有些什么具体是用到享元模式的?
实际上在.NET中(包括java),字符串string就是运用了Flyweight模式。举个例子。Object.equals(object objA,object objB)方法是用来确定objA与objB是否相同的实例,返回值为bool值。
string titleA = "大话设计模式";string titleB = "大话设计模式";syso.print(Object.equals(titleA,titleB));返回值是true,这两个字符串是相同的实例。
如果每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存的开销会很大。所以如果第一次创建了字符串对象titleA,下次再创建相同的字符串titleB时只是把它的引用指向大话设计模式,这样就实现了大话设计模式在内存中的共享。
虽说享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的。比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大理的棋子对象,分析下,它们的内部状态和外部状态各是什么?
围棋和五子棋只有黑白两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。
在某些情况下,对象的数量可能会太多,从而导致了运行时的资源与性能损耗。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题,享元模式,可以运用共享技术有效地支持大量细粒度的对象。不过,使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使用对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够的对象实例可供共享时才值得使用享元模式。
第27章 其实你不懂老板的心——解释器模式
27.1 其实你不懂老板的心
老板私下对某员工大加夸奖时,多半是最近有更多的任务需要你去完成的意思。
通常老板说某个员工是普通员工,其实他的意思是说,这个员工不够聪明,工作能力不足。
要是有一个翻译机,或解释器就好了,省得每次讲话还需要多动脑筋。
27.2 解释器模式
解释器模式(interpreter):给定一个语言,定义它的文法(语法)的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
解释器模式需要解决的是,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。比方说,我们常常会在字符串中搜索匹配的字符或判断一个字符串是否符合我们规定的格式,此时一般我们会用正则表达式。
其实像IE、Firefox这些浏览器,其实也是在解释HTML文法,将下载到客户端的HTML标记文本转换成网页格式显示到用户,不过编写一个浏览器的程序,当然要复杂很多。
解释器模式结构图:
AbstractExpression(抽象表达式),声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享:
abstract class AbstractExpression{abstract void Interpret(Context context);}
TerminalExpression(终结符表达式),实现与文法中的终结符相关联的解释操作。实现抽象表达式中所要求的接口,主要是一个interpret()方法。文法中每一个终结符都有一个具体终结表达式与之相应。
class TerminalExpression:AbstractExpression{override void Interpret(Context context){syso.print("终端解释器")}}
NoterminalExpression(非终结符表达式),为文法中的非终结符实现解释操作。对文法中每一条规则R1、R2...Rn都需要一个具体的非终结符表达式类。通过实现抽象表达式的interpret()方法实现解释操作。解释操作以递归方式调用上面所提到的代表R1、R2...Rn中各个符号的实例变量。
class NoterminalExpression:AbstractExpression{override void Interpret(Context context){syso.print("非终端解释器");}}
Context,包含解释器之外的一些全局信息
class Context{string input;//提供get-set string output;//提供get-set}
客户端代码,构建表示该文法(语法)定义的语言中一个特定的句子的抽象语法树。调用解释操作。
class Main{context = new Context();list = new List<AbstractExpression>();list.Add(new TerminalExpression());list.Add(new NoterminalExpression();)for(AbstractExpression exp in list){exp.Interpret(context)}}
27.3 解释器模式好处
用解释器模式,就如同你开发了一个编程语言或脚本给自己或别人用。
解释器模式就是用迷你语言来表现程序要解决的问题,以迷你语言写成迷你程序来表现具体的问题。
通常当有一个语言需要来解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。
解释器模式有什么好处呢?
用了解释器模式,就意味着可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。
解释器模式就是将一句话,转变成实际的命令程序执行而已。而不用解释器模式本来也可以分析,但通过继承抽象表达式的方式,由于依赖倒转原则,使得对文法的扩展和维护都带来了方便。
解释器模式也有不足的,解释器模式为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理。
27.4 音乐解释器
以前用的手机里就有编辑铃声的功能,通过输入一些简单的字母数字,就可以让手机发出音乐。用QB或者手机说明书中定义的规则去编写音乐程序,就是一段文法让QB或手机去翻译成具体的指令来执行。
27.5 音乐解释器实现
代码结构图:
演奏内容类(context):class PlayContext{//演奏文本string text;//提供get-set}
表达式类(AbstractExpression):abstract class Expression{//解释器void Interpret(PlayContext context){//略} abstract void Excute(string key,double value);//抽象方法执行,不同的文法子类,有不同的执行处理}
音符类(TerminalExpression):class Note:Expression{override void Excute(string key,double value){string note="";switch(key){case "C":note=1;//表示如果获得的key是C则演奏1(do),如果是D则演奏2(Re)}}}
音符类(TerminalExpression):class Scale:Expression{override void Excute(string key,double value){string scale="";switch(value){case 1:scale="低音";//表示如果获得的key是O并且value是1则演奏低音,2则是中音,3则是高音}}}
客户端代码:class Main{//略}
现在需要增加一个文法,就是演奏速度,要求是T代表速度,以毫秒为单位,T1000表示每节拍一秒,T500表示每节拍半秒。
首先加一个表达式的子类叫音速。然后在客户端的分支判断中增加一个case分支就可以了。
音速类:class Speed:Expression{override void Excute(string key,double value){string speed;if(value < 500){speed="快速"}}}
其实这个例子是不能代表解释器模式的全貌的,因为它只有终结符表达式,而没非终结符表达式的子类,因为如果想真正理解解释器模式,还需要去研究其他的例子。另外这是个控制台的程序,如果给出钢琴所有按键的声音文件,MP3格式,可利用Media Player控件,写出真实的音乐语言解释器。只要按照简谱编好这样的语句,就可以让电脑模拟钢琴弹奏出来。
27.6 料事如神
第28章 男人和女人——访问者模式
28.1 男人和女人
访问者模式讲的是表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
男女对比这么多的原因主要就是因为人类在性别上就只有男人和女人两类。人类只分为男人和女人,所以才会有这么多的对比。
28.2 最简单的编程实现
class Main{syso.print("男人成功时,背后多半有一个伟大的女人");syso.print("女人成功时,背后多半有一个不成功的男人");//其它对比略}
28.3 简单的面向对象实现
人类是男人和女人的抽象类。
abstract class Person{string action;//提供get-set abstract void GetConlusion();//得到结论或反应}
男人类:class Man:Person{//得到结论或反应 override void GetConlusion(){if(action=="成功"){//背后多半有一个伟大的女人}};}
女人类:class Woman:Person{//得到结论或反应 override void GetConlusion(){if(action=="成功"){//背后多半有一个不成功的男人}};}
客户端:class Main{persons = new List<Person>;man1 = new Man();man1.Action="成功";persons.Add(man1);//其它略}
以上代码还是有问题的,在男人类与女人类当中的那些if...else...很是碍眼,如果现在要增加一个结婚的状态,那这两个类都需要增加分支判断了。如果把这些状态写类,那又如何处理呢?
28.4 用了模式的实现
状态抽象类:
abstract class Action{//得到男人结论或反应 abstract void GetManConclusion(Man concreteElementA);//得到女人结论或反应 abstract void GetWomanConclusion(Woman concreteElementB);}
人的抽象类:
abstract class Person{//接受 abstract void Accept(Action visitor//它是用来获得状态对象的);}
这里关键就在于人就只分为男人和女人,这个性别的分类是稳定的,所以可以在状态类中,增加男人反应和女人反应两个方法,方法个数是稳定的,不会很容易的发生变化。而人抽象类中有一个抽象方法接受,它是用来获得状态对象的。每一种具体状态都继承状态抽象类,实现两个反应的方法。
具体状态类:
成功状态类:class Success:Action{override void GetManConclusion(Man man1){syso.print("男人成功时....");}override void GetWomanConclusion(Woman woman1){syso.print("女人成功时......")}}
失败状态类:class Failing:Action{//与上面成功状态类同,省略}
恋爱状态类:class Amativeness:Action{//与上面成功状态类同,省略};
男人和女人类:
男人类:class Man:person{override void Accept(Action visitor){visitor.GetManConclusion(this);}}
女人类:class Woman:person{override void Accept(Action visitor){visitor.GetWomanConclusion(this);}}
这里需要提一下当中用到到一种分派的技术,首先在客户程序中将具体状态作为参数传递给男人类完成了一次分派,然后男人类调用作为参数的具体状态中的方法男人反应,同时将自己(this)作为参数传递进去。这便完成了第二次分派。
双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型。接受方法就是一个双分派的操作。它得到执行的操作不仅决定于状态类的具体状态,还决定于它访问的人的类别。
对象结构类由于总是需要男人与女人在不同状态的对比,所以我们需要一个对象结构类来针对不同的状态遍历男人与女人,得到不同的适应。
对象结构类:class ObjectStructure{elements = new List<Person>();//增加 void Attach(Prrson element){elements.Add(element);}//移除 void Detach(Person element){elements.Remove(element);//查看显示 void Display(Action visitor){foreach(person p in elements){e.Accept(visitor);//遍历方法}}}}
客户端类:class Main{o = new ObjectStructure();o.Attach(new Man());o.Attach(new Woman());//在对象结构中加入要对比的男人和女人;//成功时的反应v1 = new Success();o.Display(v1);//查看在各种状态下,男人和女人的反应;//失败时的反应 v2 = new Failing();o.Display(v2);//其它略}
这样做到底有什么好处呢?
如果我们现在要增加结婚的状态来考查男人和女人的反应,由于用了双分派,使得只需要增加一个状态子类,就可以在客户端调用来查看,不需要改动其他任何类的代码。
这下完美的体现了开放-封闭原则,这种模式叫做和访问者模式。
28.5 访问者模式
访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的前提下定义作用于这些元素的新操作。
访问者模式结构图:
为该对象结构中ConcreteElement的每一个类声明一个Visit操作:
abstract class Visitor{abstract void VisitConcreteElementA(ConcreteElementA a);bstract void VisitConcreteElementB(ConcreteElementB b);}
具体访问者,实现每个由Visitor声明的操作。每个操作实现算法的一部分,而该算法片断乃是对应于结构中对象的类:
class ConcreteVisitor1{void VisitConcreteElementA(ConcreteElementA a){};void VisitConcreteElementB(ConcreteElementB b){}};
class ConcreteVisitor2{void VisitConcreteElementA(ConcreteElementA a){};void VisitConcreteElementB(ConcreteElementB b){}};
定义一个Accept操作,它以一个访问者为参数:
abstract class Element{void Accept(Visitor visitor);};
具体元素,实现Accept操作
class ConcreteElementA{void Accept(Visitor visitor){};void OperatorA(){}};
class ConcreteElementB{void Accept(Visitor visitor){};void OperatorB(){}};
对象结构类,能枚举它的元素,可以提供一个高层的接口以允许访问者访问它的元素
class ObjectStructure{}
在这里,Element就是我们的人类,而ConcreteElementA和ConcreteElementB就是男人和女人,Visitor就是我们写的状态类,具体ConcreteVisitor就是那些成功、失败、恋爱等等状态。至于ObjectStructure就是对象结构类了。
男女对比这么多的原因主要就是因为人类在性别上就只有男人和女人两类。而这也正是访问者模式可以实施的前提。
这个前提是什么呢?
如果人类的性别不止是男和女,而是可有多种性别,那就意味状态类中的抽象方法就不可能稳定了,每加一种类别,就需要在状态类和它的所有下属类中都增加一个方法,这就不符合开放-封闭原则了。
也就是说,访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。
访问者模式的目的是什么?
访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。反之,如果这样的系统的数据结构对象易于变化,经常要有新的数据对象增加进来,就不适合使用访问者模式。
其实访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者。访问者模式将有关的行为集中到一个访问对者对象中。
通常ConcreteVisitor可以单独开发,不必跟ConcreteElementA或ConcreteElementB写在一起。正因为这样,ConcreteVisitor能提高ConcreteElement之间的独立性,如果把一个处理动作设计成ConcreteElementA和ConcreteElementB类的方法,每次想新增处理以扩充功能时就得去修改ConcreteElementA和ConcreteElementB了。这也就是之前写的代码,在男人和女人类中加了对成功、失败等状态的判断,造成处理方法和数据结构的紧耦合。
访问者的缺点其实也就是使用增加新的数据结构变得困难了。
Gof四人中的一个作者就说过:大多数时候你并不需要访问者模式,但当一旦你需要访问者模式时,那就是真的需要它了。事实上,我们很难找到数据结构不变化的情况,所以用访问者模式的机会也就不太多了。这也就是为什么谈到男人女人对比时讨论的原因,因为人类性别这样的数据结构是不会变化的。
28.6 访问者模式基本代码
Visitor类,为该对象结构中ConcreteElement的每一个类声明一个Visit操作。
abstract class Visitor{abstract void VisitConcreteElementA{ConcreteElementA concreteElementA};abstract void VisitConcreteElementB{ConcreteElementB concreteElementB};}
ConcreteVisitor1和ConcreteVisitor2类,具体访问类,实现每个由Visitor声明的操作。每个操作实现算法的一部分,而该算法片断乃是对应于结构中对象的类。
class ConcreteVisitor1:Visitor{override void VisitConcreteElementA(ConcreteElementA ca){syso.print(ca.GetType().Name被this.GetType.Name访问);override void VisitConcreteElementB(ConcreteElementB cb){syso.print(cb.GetType.Name被this.GetType.Name访问);}}}
class ConcreteVisitor2:Visitor{//代码与上类类似,省略}
Element类,定义一个Accept操作,它以一个访问者为参数
abstract class Element{abstract void Accept(Visitor visitor);}
ConcreteElementA和ConcreteElementB类,具体元素,实现Accept操作。
class ConcreteElementA:Element{override void Accept(Visitor visitor){visitor.VisitConcreteElementA(this);//充分利用双分派技术,实现处理与数据结构的分离};void OperatorA(){};//其他的相关方法}
class ConcreteElementB:Element{//代码与上类类似,省略}
ObjectStructure类,能枚举它的元素,可以提供一个高层的接口允许访问者访问它的元素
class ObjectStructure{elements = new List<Element>();void Attach(Element element){elements.Add(element);}void Detach(Element element){elements.Remove(element);}void Accept(Visitor visitor){for(Element e in elements){e.Accept(visito);}}}
客户端代码:
class Main{ObjectStructure o = new ObjectStructure();o.Attach(new ConcreteElementA());o.Attach(new ConcreteElementB());ConcreteVisitor1 v1 = new ConcreteVisitor1();ConcreteVisitor2 v2 = new ConcreteVisitor2();o.Accept(v1);o.Accept(v2);}
28.7 比上不足,比下有余
访问者模式的能力和复杂性是把双刃剑,只有当你真正需要它的时候,才考虑使用它。有很多的程序员为民展示自己的面向对象的能力或是沉迷于模式当中,往往会误用这个模式,所以一定要好好理解它的适用性。
总结:最常用的设计模式 1、工厂方法模式 2、外观模式 3、观察者模式 4、策略模式 5、适配器模式
开发程序步骤总结(个人观点):
1、确定开发程序使用的语言
2、了解开发语言的特性
3、以使用java为例:java语言的特性是面向对象
4、既然是面向对象那么就要知道面向对象的特性
5、面向对象的三大特性:封装、继承、多态
6、了解这些特性的使用,其主要作用是什么
7、写程序之前加入设计模式思想
8、使用设计模式之前需要了解设计模式的四大原则,以保证设计程序的架构。
9、设计模式的四大原则:单一职责原则(减少类的职责)、开放-封闭原则(对扩展开放-对修改关闭)、依赖倒转原则(针对接口编程,不要对实现编程)、迪米特原则(强调了类之间的松耦合)
10、开始架构
23种设计模式代码见附件:----------------
-
-
-
-
-
!
相关推荐
设计模式是软件工程中的一种重要思想,它是在特定情境下,为解决常见问题而形成的一套最佳实践。在本文中,我们将深入探讨24种设计模式,并结合混合设计模式的概念,以及它们在实际项目中的应用案例。 首先,设计...
人人都懂设计模式 设计模式是软件开发中的一种解决方案,它提供了一种通用的设计思想和方法论,可以帮助开发者更好地设计和实现软件系统。设计模式可以分为三大类:创建型模式、结构型模式和行为型模式。 在本书中...
主要是介绍各种格式流行的软件设计模式,对于程序员的进一步提升起推进作用,有时间可以随便翻翻~~ 23种设计模式汇集 如果你还不了解设计模式是什么的话? 那就先看设计模式引言 ! 学习 GoF 设计模式的重要性 ...
Java 经典设计模式讲解以及项目实战 设计模式简介:主要介绍各种设计模式的概念和运用场景等 设计模式综合运用:主要是笔者在实际工作中运用到的一些设计模式综合运用事例的提炼 Spring设计模式简介:主要是讲述...
《Head First 设计模式》与《Java设计模式(第2版)》是两本非常重要的IT书籍,专注于软件开发中的设计模式。设计模式是解决软件设计中常见问题的经验总结,它们提供了一种标准的方法来处理特定场景下的问题,使得代码...
### 设计模式解析:深入理解软件设计的核心原则与实践 #### 标题解析:设计模式解析 设计模式是软件工程领域的重要概念,旨在提供解决常见软件设计问题的模板或指导原则。《设计模式解析》这一标题暗示了书籍将...
根据提供的文档概览,我们可以对每个章节所涉及的设计模式进行详细的阐述和解释。下面将针对文档中提及的设计模式逐一展开,以便更好地理解这些模式的概念、结构、应用场景以及优缺点。 ### 1. 面向对象程序设计...
这个压缩包文件"设计模式(包含5个设计模式)含源代码报告.rar"显然是一份宝贵的资源,它涵盖了五个核心的设计模式,并附带了详细的类图、源代码以及文档报告,这对于学习和理解设计模式至关重要。 首先,我们要探讨...
在计算机科学领域,设计模式是软件工程中用于解决特定问题的一般性方案,它们是经过实践检验的最佳实践。这些模式被广泛应用于面向对象软件设计中,能够提高代码的可重用性、灵活性和可维护性。设计模式通常被划分为...
设计模式是构建大型软件系统zui强大的方法之一,优化软件架构和设计已经逐渐成为软件开发和维护过程中的一个重要课题。 Python设计模式(第2版)通过11章内容,全面揭示有关设计模式的内容,并结合Python语言进行示例...
《Java设计模式》课程设计报告主要探讨了如何利用Java编程语言和MyEclipse集成开发环境来实现基于设计模式的西瓜市场系统。这个项目涵盖了四种重要的设计模式:单例模式、代理模式、建造者模式和抽象工厂模式,为...
《新版设计模式手册 - C#设计模式(第二版)》是一部深入探讨C#编程中设计模式的权威指南,尤其适合已经有一定C#基础并希望提升软件设计能力的开发者阅读。设计模式是解决软件开发中常见问题的经验总结,是软件工程的...
软件设计模式(Java版)习题答案 本资源为软件设计模式(Java版)习题答案,由程细柱编著,人民邮电出版社出版。该资源涵盖了软件设计模式的基础知识,包括软件设计模式的概述、UML中的类图、面向对象的设计原则、...
设计模式精解- GoF 23种设计模式解析附C++实现源码 懂了设计模式,你就懂了面向对象分析和设计(OOA/D)的精要。反之好像也可能成立。道可道,非常道。道不远人,设计模式亦然如此。 一直想把自己的学习经验以及在...
全书用两章篇幅对设计模式和GRASP作了基本介绍,3种设计模式的讲解:对于每一种模式,先给出定义,接着通过类比方式用一个现实世界中的例子说明模式的应用,然后分别以C#和Java代码例述模式的架构实现。最后一章给出...
资源名称:MongoDB应用设计模式内容简介:无论是在构建社交媒体网站,还是在开发一个仅在内部使用的企业应用程序,《MongoDB应用设计模式》展示了MongoDB需要解决的商业问题之间的连接。你将学到如何把MongoDB设计...
设计模式是软件开发中的经典实践,它们为解决特定问题提供了通用、可复用的解决方案。在Java编程中,运用合适的设计模式可以提高代码的可维护性、可扩展性和可复用性。以下是关于Java版本设计模式实现demo的一些补充...
设计模式是软件工程中的一种最佳实践,它是在特定上下文中解决常见问题的经验总结。"设计模式之美——教你写出高质量代码"这个主题旨在帮助开发者更好地理解和应用设计模式,从而提升代码的质量和可维护性。设计模式...