面向对象软件开发简介
- 结构化程序设计面临的一些问题
- 程序各部分承担责任不平衡,“主”程序承受的责任太多,容易产生非常复杂的代码。
- “主”程序需要关注太多的细节,对需求的变化,十分无力。
- 理解对象
概念–什么责任;规约–怎么用;实现–怎么做到的
- 在概念层次上,对象是一组责任
- 在规约层次上,对象是一组可以被其它对象或自己调用的方法
- 在实现层次上,对象是代码和数据,以及它们之间的计算交互
UML 统一建模语言
- UML图类型
- 用例图–>分析阶段
- 交互图–>观察对象交互
- 类图–>设计阶段
- 状态图–>观察对象状态与行为
- 部署图–>配置阶段
- 类之间关系
- 泛化(Generalization)–> is-a
- 实现(Realization)–> 实现接口
- 依赖(Dependency)–> 一般指由局部变量、函数参数、返回值建立的对于其他对象的调用关系
关联(Association)–> 包括:聚合(Aggregation)、组合(Composition)
聚合和组合的区别在于:聚合关系是“has-a”关系,组合关系是“contains-a”关系;聚合关系表示整体与部分的关系比较弱,而组合比较强;聚合关系中代表部分事物的对象与代表聚合事物的对象的生存期无关,一旦删除了聚合对象不一定就删除了代表部分事物的对象。组合中一旦删除了组合对象,同时也就删除了代表部分事物的对象。更多参考
多重性(Multiplicity)–> 通常在关联中使用。就是代表有多少个关联对象存在。
传统面向对象设计的局限
传统面向对象设计:特化和复用
传统面向对象设计奖一切作为特例来解决问题。这种解决方案直截了当。但是有几个缺点:高冗余、低内聚和类爆炸。
设计模式
- 面向对象设计的一些策略
- 按接口编程
- 尽量用聚合代替继承
- 找出变化并封装
Facade模式
定义: 为子系统中的一组接口提供一个统一接口。
意图: 希望简化原有系统的使用方式,定义自己的接口。
问题: 只需要摸个复杂系统的子集,或者需要以一种特殊的方式与系统交互。
解决方案: Facade为原有系统的客户提供一个新的接口。
效果: Facade模式简化了对所需子系统的使用过程。但是由于Facade并不完整,因此客户可能无法使用某些功能。
实现: 定义一个或多个具备所需接口的新类。
举例: 比如某产品中对Service的单例封装。
应用: 1.简化复杂系统;2.封装或隐藏原系统;3.希望在原系统基础上增加一些新的功能。Adapter模式
定义: 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使原本由于接口不兼容而不能一起工作的类可以一起工作。
意图: 使控制范围之外的一个原有对象与某个接口匹配。
问题: 系统的数据和行为都正确,但是接口不符。通常用于必须从抽象类派生使。
解决方案: Adapter模式提供了具有所需接口的包装类。
参与者与协作者: Adapter改变了Adaptee的接口,使Adaptee与Adapter的基类Target匹配。这样Client就可以使用Adaptee了,就好像它是Target。
效果: Adapter模式使原有对象能够适应新的类结构,不受其他接口的限制。
实现: 将原有类包含在另一个类之中。让包含类与需要的接口匹配,调用被包含类的方法。
类型: 对象Adapter模式(组合);类Adapter模式(继承)。
与Facade的异同: Facade模式简化了接口,而Adapter模式将一个已有的接口转换成另一个接口。开拓视野
软件设计思路: 功能分隔–>定义接口–>实现接口(关注动机而非实现)。
理解封装: 任何形式的隐藏,可是是数据、实现细节、派生类、设计细节、实例化规则。比如:通过抽象类或接口隐藏类。
使用继承来特化的问题: 1.可能导致弱内聚;2.减少复用的可能性;3.无法根据变化很好地伸缩。
发现变化并将其封装
共性和可变性分析: 共性分析寻找的是不可能随时间而变化的结构,而可变性分析则要找到可能变化的结构。可变性分析只在共性分析定义的上下文中才有意义。从架构的角度来看,共性分析为架构提供长效的要素,而可变性分析则促成它适应实际使用的需求。更具体的了解:抽象类(共性)、派生类(可变性)。
敏捷编程的品质: 极限编程的核心是循序渐进的开发,在编程的同时进行验证。大的概念是从众多小的概念演化而来的。敏捷的品质:1.无冗余;2.可读;3.可测试。
测试驱动开发TDD的优势: 1.最后能够得到一组自动化测试。2.必须按方法的接口而非实现来设计,这样得到封装更好、耦合更松散的方法。3.关注测试会让你注意到概念费城多个可测试的部分,这样能够获得强内聚和松耦合。Strategy模式
定义: 定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。Strategy模式使算法可以独立于使用它的客户变化。
意图: 可以根据上下文,使用不同的业务规则或算法。
问题: 对所需算法的选择取决于发出请求的客户或者要处理的数据。如果只有一些不会变化的算法,就不需要Strategy模式。
解决方案: 将对算法的选择和算法的实现分离。允许根据上下文进行选择。
参与者与协作者: 1.Strategy指定了如果有使用不同的算法;2.各ConcreteStrategy实现了这些不同的算法;3.Context根据类型为Strategy的引用使用具体的ConcreteStrategy。
效果: 1.Strategy模式定义了一系列算法;2.可以不使用switch语句或条件语句;3.必须以相同的方式调用所有的算法。
实现: 让使用算法的类Context包含一个抽象类Strategy,该抽象类有一个抽象的方法指定如何调用短发。每个派生类按需要实现算法。
扩展: C++函数指针和C#委托都可以维持一个无状态的Strategy。Bridge模式
定义: 将抽象与其实现解耦,使它们可以独立地变化。
意图: 将一组实现与另一组使用它们的对象分离。
问题: 一个抽象类的派生类必须使用多个实现,但不能出现类数量的爆炸性增长。
解决方案: 为所有实现定义一个接口,供抽象类的所有派生类使用。
参与者与协作者: Abstraction为要实现的对象定义接口,Implementor为具体的实现定义接口。Abstraction派生类使用Implementor的派生类,却无需知道自己具体使用哪一个ConcreteImplementor。
效果: 实现与使用实现的对象解耦,提供了可扩展性,客户对象无需操心实现问题。
实现: 1.将实现封装在一个抽象类中;2.在要实现的抽象的基类中包含一个实现的句柄。
应用举例: 打印机驱动程序,驱动程序打印不同种类的对象(抽象)与使用的打印机(实现)可以解耦。AbstractFactory模式
定义: 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类。
意图: 需要为特定的客户(或情况)提供对象组。
问题: 需要实例化一组相关的对象。
解决方案: 协调对象组的创建。提供一种方式,将如何执行对象实例化的规则从使用这些对象的客户对象提取出来。
参与者与协作者: AbstractFactory为如何创建对象组的每个成员定义接口。一般每个组都由独立的ConcreteFactory进行创建。
效果: 这个模式将“使用那些对象”的规则与“如何使用这些对象”的逻辑分离开来,重新划分了类的责任。
实现: 定义一个抽象类来指定创建那些对象。然后为每个组实现一个具体类。
模式组合
专家设计之道
Alexander描述了一种基于模式的方法:1.从对整体的概念性理解开始,以理解需求实现的目标;2.找到在整体中出现的模式;3.从为其他模式创建背景的那些模式开始;4.从背景向内,应用这些模式,找到新模式,并重复;5.最后,通过每次应用一个模式,改进设计,并在所创建的背景中予以实现。
用模式思考
- 找出模式。在问题领域中找出模式。
- 分析和应用模式。对于要进行分析的模式集合,执2a到2d。
2a. 按背景的创造顺序将模式排序。根据为其他模式创造背景的情况将模式排序。其原理是,一个模式将为另一个模式的创造背景,不会出现两个模式互为彼此创建背景的情况。
2b. 选择模式并扩展设计。根据排序,选择列表中的下一个模式,用它得到高层的概念设计。
2c. 找到其他模式。找到在分析中可能出现的其他模式,将它们添加到要分析的模式集合中。
2d. 重复。对没有融合概念设计的模式重复以上步骤。 - 添加细节。根据设计的需要添加细节。扩展方法和类的定义。
模式思考
- 设计模式的原则与策略
开闭原则 模块、方法和类应该对扩展开放,对修改封闭。
从背景设计原则 从背景设计,在设计各部分所呈现的细节之前,先创建总体概念。
依赖倒置原则 依赖抽象。1.高层模块不应该依赖于低层模块。高层模块和低层模块都应该依赖抽象。2.抽象不应该依赖于细节。细节应该依赖于抽象。
封装变化原则
抽象类与接口 1.抽象类允许有公共的状态和行为;2.不直接共享状态或行为的对象或者必须从另一个类派生的对象实现接口。
其他模式
- Decorator模式
定义 动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式比生成子类更为灵活。
意图 动态地给一个对象添加职责。
问题 要使用的对象拥有执行所需的基本功能。但是,可能需要为这个对象添加某些功能,这些附加功能可能发生在对象的基础功能之前或之后。
解决方案 可以无需创建子类,而扩展一个对象的功能。
参与者与协作者 ConcreteComponent让Decorator对象为自己添加功能。有时候用ConcreteComponent的派生类提供核心功能,在这种情况下ConcreteComponent类就不再是具体的,而是抽象的。Component类定义了这些类所使用的接口。
效果 所添加的功能放在小对象中。好处是可以在ConcreteComponent对象的功能之前或之后动态添加功能。注意,虽然装饰对象可以在被装饰对象之前或之后添加功能,但对象链总是终于ConcreteComponent对象。
实现 创建一个抽象类来表示原类和要添加到这个类的新功能。在装饰类中,将对新功能的调用放在对紧随其后对象的调用之前或之后,以获得正确的顺序。
拓展 Decorator模式要求对象链的实例化与使用它的Client对象完全分离开。最典型的实现是通过使用工厂对象,根据某些配置信息实例化对象链。对对象进行前置测试和后置测试也是这种模式的一个重要用途。 Observer模式
定义 定义对象间的一种一对多的依赖关系,当一个对象状态发生变化时,所有依赖它的对象都将得到通知并自动更新。
意图 在对象之间定义一种一对多的依赖关系,这样当一个对象的状态改变时,所有依赖者都将得到通知并自动更新。
问题 当某个事件发生时,需要向一系列变化的对象发出通知。
解决方案 Observer将监听某个事件的责任委托给中心对象:Subject。
参与者与协作者 Subject知道自己的Observer,因为Observer要向它注册。Subject必须在所监视的事件发生时通知Observer。Observer负责向Subject注册,以及在得到通知时从Subject处获取信息。
效果 如果某些Observer只对事件的一个子集感兴趣,那么Subject可能会告诉它们不需要知道的事件。如果Subject通知Observer,Observer还返回请求更多信息,则可能需要额外的通信。
实现 当某个事件发生时,需要知道的对象将自己注册到另一个监视事件发生或自己触发事件的对象上。事件发生时,Subject告诉Observer事件已经发生。为了对所有Observer类型的对象实现Observer接口,有时候需要使用Adapter模式。Template Method模式
定义 定义一个操作中算法的骨架,而将一些步骤延迟到子类中。不改变算法的结构而重定义它的步骤。
意图 定义一个操作中算法的骨架,将一些步骤推迟到子类中实现。可以不改变算法的结构而重定义该算法的步骤。
问题 要完成在某一细节层次一致的一个过程或一系列步骤,但某个别步骤在更详细的层次上实现可能不同。
解决方案 允许定义可变的子步骤,同时保持基本过程一致。
参与者与协作者 Template Method模式由一个抽象类组成,这个抽象类定义了需要覆盖的基本TemplateMethod方法。每个从这个抽象类派生的具体类将为此模板实现新方法。
效果 模板提供了一个很好的代码复用平台。它还有助于确保所需步骤的实现。它将每个Concrete类的覆盖步骤绑定起来,因此只有在这些变化总是并且只能一起发生时,才应该使用TemplateMethod模式。
实现 创建一个抽象类,用抽象方法实现一个过程。这些抽象方法必须在子类中实现,以执行过程的每个步骤。如果这些步骤是独立变化的,那么每个步骤都可以用Strategy模式来实现。工厂模式
工厂模式将对象的创建与管理和对象的使用解耦。
遵循工厂模式,开发可以分成两步:1.定义对象和它们的协作方式;2.编写为相应情况实例化对象并在对象共享时管理已有对象的工厂。Singleton模式
定义 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
意图 希望对象只有一个实例,但没有控制对象实例化的全局对象。还希望确保所有实体使用该对象相同的实例,而无需将引用传给它们。
问题 几个不同的客户对象需要引用同一对象,而且希望确保这种类型对象恕不不超过一个。
解决方案 保证一个实例。
参与者与协作者 Client对象只能通过getInstance方法创建Singleton实例。
效果 Client对象无需操心是否已存在Singleton实例。这是有Singleton自己控制的。
实现 1.添加一个类的私有的静态成员变量,引用所需的对象;2.添加一个静态方法,它在成员变量为null时实例化这个类,然后返回该成员变量的值;3.将构造函数的状态设置为保护或私有,从而防止任何人直接实例化这个类,绕过静态构造函数的机制。
Double-Checked Locking模式 Singleton的多线程版本。Object Pool模式
定义 在存在共享资源而且与该资源单点联系较为有益时,通过封装这些职责,使用这些对象的客户不仅免于这些职责,而且还可以与这些职责相关的修改隔离开来。
意图 在创建对象比较昂贵,或者对于特定类型能够创建的对象数目有限制时,管理对象的重用。
问题 对象的创建或管理必须遵守一组定义明确的规则集。通常这些规则都与如何创建对象、能够创建多少个对象和在已有对象完成当前任务时如何重用它们等等相关。
解决方案 在需要一个Reusable对象时,Client调用ReusablePool的acquireReusable方法。如果池是空的,那么acquireReusable方法创建一个Reusable对象,否则,就等待直到有Reusable对象返回集合。
参与者与协作者 ReusablePool管理者Client所有的Reusable对象的可用性。Client然后在一个有限的时间段使用Reusable对象的实例,ReusablePool包含所有Reusable对象,这样就可以对其以统一的方式进行管理。
效果 最使用于对对象的需求一直非常稳定的时候,需求变化太大会带来性能问题。Object Pool中为了解决这一问题,限制了能够创建的对象的数量。使管理实例创建的逻辑与实例被使用的类分离,可以得到内聚更好的设计。
实现 如果可以创建对象的数量有限制,或者池的大小有限制,可以使用一个简单的数组来实现池。否则,使用vector对象,负责管理对象池的对象必须是唯一能够创建这些对象的对象。ReusablePool使用Singleton模式实现。另一种变体是在Reusable对象中加一个释放方法,让它自己返回到池。
应用 应用服务器与数据服务器之间的TCP/IP连接对象。Factory Method模式
定义 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使实例化延迟到其子类。
意图 定义一个用于创建对象的接口,让子类决定实例化哪一个类。将实例化推迟到子类。
问题 一个类需要实例化另一个类的派生类,但不知道是哪一个。Factory Method允许派生类进行决策。
解决方案 派生类对实例化哪个类和如何实例化做出决策。
参与者与协作者 Product是工厂所创建的对象类型的接口。Creator是定义工厂方法的接口。
效果 客户讲需要派生Creator,以创建一个特定的ConcreteProduct对象。
实现 在抽象类中使用一个抽象方法。需要实例化一个被包含对象的时候,抽象类的代码将引用此方法,但是不知道需要的对象是哪一个。
Abstract Factory Abstract Factory模式中,有一个抽象类定义创建一系列对象的方法。为每个可能存在的对象序列都派生一个类。所有定义在抽象类中,然后在派生类中覆盖的方法都遵循了Factory Method模式。
举例 容器的迭代器。
工厂总结 对于系统中的两个实体,应该将它们的关系限制为A使用B,或者A创建/管理B,但是两中关系永远不要同时存在。
总结
- 面向对象的设计原则
- 对象是具有明确定义的责任的事物
- 对象对自己负责
- 封装指任何形式的隐藏:
1)数据隐藏
2)实现隐藏
3)类隐藏
4)设计隐藏
5)实例化隐藏 - 使用共性和可变性抽象出行为和数据中的变化
- 按接口设计
- 将继承看成一种将变化概念化的方法,而不是创建已有对象的特殊情形。
- 将变化放入一个类中,并与该类中的其它变化解耦
- 力求松耦合
- 力求强内聚
- 将使用一个对象的代码与创建该对象的代码分离
- 通过“按意图编程”,使用反映意图的名字,确保代码的可读性
- 在编程之间就考虑代码的可测试性
在最后阶段,模式不再重要,重要的是模式教会我们对真实的感悟力~