本章学习面向对象的一些高级应用——设计模式。在学习过程中会了解到一些软件设计思想和原则,作为一名软件开发者,不仅要能开发高质量的代码,还要能够做出优秀的设计。设计模式被广泛运用在Java 框架技术中,学习设计模式对于理解框架的工作原理会有所帮助。
学习方法:
设计模式虽有很多种,但总是可以从解锅台、提高复用性这些方向来理解。
首先要明确每种设计模式的使用场景,明确其要解决的问题,进而理解其解决该问题的思路。
设计模式(Design Pattern)是人们在长期的软件开发中对一些经验的总结,是对某些特定问题经过实践检验的特定解决方法。就像兵法中的三十六计,总结了 36 种对于战争中某些场合的可行性计谋战术——"围魏救赵" "声东击西" "走为上"等,可以说三十六计中的每一计都是种模式。
设计模式使人们可以更加简单方便地复用成功的设计和体系结构。将已证实的技术方案总结成设计模式,也会使其他开发者更加容易理解其设计思路。设计模式是可复用的面向对象软件的基础,帮助开发者做出有利于系统复用的选择,避免损害系统复用性的设计。简言之,设计模式可以帮助设计者更快 更好地完成系统设计。
目前所说的设计模式通常是指 GoF 设计模式。GoF(Gang of Four,四人组)指的是 DesignPatterns: Elements of Reusable Object-Oriented Software 这本书的4位作者:Gamma、Helm、Johnson 和 Vlissides,书中总结了 23 种经典的设计模式,因此也被称为 GoF 设计模式。
这 23 种设计模式有两种分类方式。
根据目的划分,即根据设计模式是用于完成何种工作来划分。这种方式可分为创建型模式、结构型模式和行为型模式 3种。
根据作用范围划分,即根据设计模式主要作用于类上还是主要作用于对象上来划分这种方式可分为类模式和对象模式两种。
注意:适配器模式同属于类模式和对象模式,可分为类适配器和对象适配器两种形式。
GoF 设计模式的分类。
范围/目的 | 创建型模式 | 结构型模式 | 行为型模式 |
---|---|---|---|
类模式 | 工厂方法 | (类)适配器 | 模板方法 解释器 |
对象模式 | 单例 原型 建造者 |
代理 桥接 装饰 外观 享元 组合 |
策略 职责链 观察者 中介者 迭代器 访问者 备忘录 |
众所周知,在软件开发和使用的过程中,需求是经常变化的。面对这些变化,设计不足的软件往往难以修改甚至要重新设计。大多数的软件应用是由多个类通过彼此合作才能实现完整的功能。对于组件化开发的软件来说,组件之间会存在各种依赖关系。例如,在A类的方法中,调用了B类对象的方法以完成特定的功能,我们就说A类依赖于B类。类与类之间的依赖关系增加了程序开发的复杂程度个类的变更,可能会对正在使用该类的所有类产生影响。
以常见的业务层调用数据访问层的操作为例,代码通常如下所示。
在 src 下创建 pojo 包,在 pojo 包下创建 News 实体类:
package pojo;
/**
* 新闻模块的实体列
*/
public class News {
private String ntitle;// 新闻标题
private String ncontent;// 新闻内容
//省略了 getter/setter
}
在 src 下创建 dao 包,之后在 dao 包下创建 NewsDao 新闻模块的 DAO 接口:
package dao;
import pojo.News;
/**
* 新闻模块的 DAO 接口
*/
public interface NewsDao {
/**
* 保存新闻信息的方法
*/
public void save(News news);
}
在 sre 下创建 dao.Impl 包,之后在 dao.Impl 包下创建新闻模块的 DAO 接口实现类
package dao.impl;
impert dao.NewsDao;
import org.apache.1og4j.Logger;
import pojo.News;
/**
* 新闻模块的 DAO 实现类
*
* @param news
*/
public class NewsDaoImpl extends BaseDao implements NewsDao {
private static final Logger logger = Logger.getLogger(NewsDaoImpl.class);
public void save(News news) {
logger.debug("保存新闻信息到数据库");
String sql = "insert into news values(?,?)";
Object[] prarm = {news.getNtitle(), news.getNcontent()};
super.executeUpdate(sql, prarm);
}
}
/**
* 新闻模块业务类
*/
public class NewsServiceImpl implements NewsService {
// 所依赖的NewsDao对象
private NewsDao dao;
public void setDao(NewsDao dao) {
this.dao = dao;
}
public void addNews(News news) {
// 调用NewDao的方法保存新闻信息
dao.save(news);
}
/**
* 保存新闻信息的方法
*
* @param news
*/
@Override
public void save(News news) {
// 调用NewsDao接口中的save方法完成保存新闻信息操作
dao.save(news);
}
}
在以上代码中,NewsServicelmpl对NewsDao接口存在依赖关系,并且与其实现类NewsDaolmp!耦合在一起。此类常见的代码其实存在一个严重问题,即如果因为需求变化需要替换 NewsDao 的实现类将导致 NewsServicelmp! 中的代码也要进行修改。由此不难想象,如果程序中比较基础的模块发生变化将导致该模块的所有调用者都要修改代码,影响了其他模块的重用。如此,程序将难以扩展和维护,甚至难以开发、测试。
对于如何设计易于维护和扩展的软件系统,面向对象的原则。这些原则可以用来检验软件系统设计的合理性,也被设计模式所遵循。
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,简单来说,一个类应该只负责一个职责;否则,类应该被拆分。
该原则提出一个类不应该承担太多职责。如果一个类承担了太多的职责,至少存在以下两个缺点。
开闭原则是面向对象设计中最基础的设计原则,开闭原则规定一个软件实体,如类、模块和函数,应该对扩展开放,对修改关闭(在不修改原有代码的情况下增加新的功能)。其意思是,在程序需要进行拓展的时候,不能通过修改已有的代码实现变化,而应该通过扩展软件实体的方式实现,如根据需求重新派生一个实现类。想要达到这样的效果,这就需要使用接口,面向接口编程。(USB2.0与 USB3.0)
在软件的生命周期内,因为变化、升级和维护等原因而对软件原有代码进行修改,可能会向原有代码中引入错误,也可能不得不对原有代码整个进行重构,并且原有代码修改后还要重新进行测试。
里氏替换原则是面向对象设计的基本原则之一,是继承复用的基石。该原则规定所有引用基类的地方必须透明地使用其子类的对象。简单来说,所有使用基类代码的地方,如果换成子类对象还能够正确运行,则满足这个原则;否则就是继承关系有问题,应该取消原来的继承关系,重新设计它们之间的关系。这个原则可以用来判断继承关系是否合理。
依赖倒置原则的核心思想是:依赖于约定而不依赖于具体实现,即面向接口编程。对象的依赖关系有3种传递方式。(Spring中将依赖对象通过构造注入和setter注入)
如果开闭原则是面向对象设计的目标,那么依赖倒置原则就是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
接口隔离原则要求尽量将庞大臃肿的接口拆分成更小、更具体的接口,让接口中只包含客户感兴趣的方法。客户不应该被迫去依赖他不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用;否则,很多实现类被迫去实现它们不需要的方法。
接口隔离原则和单一职责原则都是为了提高类的内聚性,降低它们之间的耦合度,但两者是不同的。
迪米特法则又称为最少知道原则,是指一个软件实体应当尽可能少地与其他实体发生相互作用。具体来讲,被依赖的类应尽量将复杂逻辑封装在类的内部,不对外泄露任中间信息,使客户对中间过程中的其他实体保持最少的了解,从而减少不必要的依赖,降低合(类似于电脑的 USB 接口,其具体的实现会封装到电脑的内部,提供一个简单的接口供外部主体去调用,如此在对 USB 接口的具体实现进行修改时,不会影响其他实体类的调用).
合成复用原则是指:尽量使用组件/聚合的方式,而不是继承关系达到软件复用的目的。
继承复用是类型的复用,必须具备is-a关系才可通过继承方式进行复用,且从基类继承而来的实现是静态的,不可能在运行期间发生变化,因此没有足够的灵活性。
而合成复用是 has-a 关系,将已有对象纳入到新对象中使之成为新对象的一部分,因此新对象可以调用已有对象的功能。使用合成复用方式,新对象可以在运行期间动态地引用与成分对象类型相同的实现。