1.设计模式概念
设计模式(Design Pattern)是人们在长期的软件开发中对一些经验的总结,是对某些特定问题经过实践检验的特定解决方法。就像兵法中的三十六计,总结了 36 种对于战争中某些场合的可行性计谋战术--"围魏救赵""声东击西”"走为上计"等,可以说三十六计中的每一计都是一种模式。
设计模式使人们可以更加简单方便地复用成功的设计和体系结构。将已证实的技术方案总结成设计模式,也会使其他开发者更加容易理解其设计思路。设计模式是可复用的面向对象软件的基础,帮助开发者做出有利于系统复用的选择,避免损害系统复用性的设计。简言之,设计模式可以帮助设计者更快 更好地完成系统设计。
目前所说的设计模式通常是指 GoF设计模式。GoF(Gang of Four,四人组)指的是DesignPatterns: Elements of Reusable objet-Oriented software 这本书的4位作者:
Gamma、Helm、Johnson 和 Vissides,书中总结了 23 种经典的设计模式,因此也被称为 GoF 设计模式。
这23种设计模式有俩种分类方式。
(1)根据目的划分,即根据设计模式是用于完成何种工作来划分。这种方式可分为创建型模式、结构型模式和行为型模式3种。
创建型模式:用于描述"如何创建对象",其主要特点是"将对象的创建与使用分离”
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。
行为型模式:用于描述类或对象之间如何相互协作,共同完成单个对象无法独立完的任务,以及如何分配职责。
(2)根据作用范围划分,即根据设计模式主要作用于类上还是主要作用于对象上来划分这种方式可分为类模式和对象模式两种。
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运时是可以变化的,更具动态性。
注意:
适配器模式同属于类模式和对象模式,可分为类适配器和对象适配器两种形式 |
下面是GoF设计模式分分类。
1.2软件可复用问题和面向对象设计原则
众所周知,在软件开发和使用的过程中,需求是经常变化的。面对这些变化,设计不足的软件往往难以修改甚至要重新设计。
大多数的软件应用是由多个类通过彼此合作才能实现完整的功能。对于组件化开发的软件来说,组件之间会存在各种依赖关系。例如,在A类的方法中,调用了B类对象的方法以完成特定的功能,我们就说 A类依赖于B类。类与类之间的依赖关系增加了程序开发的复杂程度一个类的变更,可能会对正在使用该类的所有类产生影响。以常见的业务层调用数据访问层的操作为例,代码通常如下所示。
在 src 下创建 pojo 包,在 pojo 包下创建 News 实体类。
public class News {
private String ntitle;
private String ncontent;
public News(){}
public News(String ntitle, String ncontent) {
this.ntitle = ntitle;
this.ncontent = ncontent;
}
@Override
public String toString() {
return "News{" +
"ntitle='" + ntitle + '\'' +
", ncontent='" + ncontent + '\'' +
'}';
}
public String getNtitle() {
return ntitle;
}
public void setNtitle(String ntitle) {
this.ntitle = ntitle;
}
public String getNcontent() {
return ncontent;
}
public void setNcontent(String ncontent) {
this.ncontent = ncontent;
}
}
在src下创建dao包,之后再dao包下创建NewsDao新闻模块的DAO接口。
public interface NewsDao {
/**
* 新闻模式的DAO接口
* @param news
*/
public void save(News news);
}
在src下创建dao.Impl包,之后再dao.Impl包下创建新闻模块的DAO接口实现类。
public class NewsDaoImpl extends BaseDao implements NewsDao {
/**
* 新闻模块的DAO实现类
*/
private Logger logger = Logger.getLogger(NewsDaoImpl.class);
@Override
public void save(News news) {
logger.debug("保存新闻信息到数据库");
String sql = "insert news values(?,?)";
Object [] pam = {news.getNtitle(),news.getNcontent()};
int paw= super.executeUpdate(sql,pam);
if (paw>0){
System.out.println("添加成功");
}
}
}
public interface NewsServer {
/**
* 新闻模块的server接口
* @param news
*/
public void save(News news);
}
public class NewsServiceImpl implements NewsServer {
//调用数据访问层接口的save方法
private NewsDao dao = SimpleDaoFactory.getInstance();
@Override
public void save(News news) {
//调用NewsDao接口中的save方法完成保存新闻信息操作
newsDao.save(news);
}
}
在以上代码中,NewsServicelmpl对NewsDao接口存在依赖关系,并且与其实现类NewsDaoimpl 耦合在一起。此类常见的代码其实存在一个严重问题,即如果因为需求变化需要替换 NewsDao的实现类将导致Newssenicelmp!中的代码也要进行修改。由此不难想象,如果程序中比较基础的模块发生变化将导致该模块的所有调用者都要修改代码,影响了其他模块的重用。如此,程序将难以扩展和维护,甚至难以开发、测试。
对于如何设计易于维护和扩展的软件系统,面向对象的原则。这些原则可以用来检验软件系统设计的合理性,也被设计模式所遵循。
1.单一责任原则
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,简单来说,一个类应该只负责一个职责;否则,类应该被拆分。
该原则提出一个类不应该承担太多职责。如果一个类承担了太多的职责,至少存在以下两个缺点。
(1)一个职责的变化可能会影响这个类实现其他职责的能力,或者引发其他职责故障。
(2)当客户需要该类的某一个职责时,不得不将其他不需要的职责全部包含进来,从面造成冗余或风险。
2.开闭原则
开闭原则是面向对象设计中最基础的设计原则,开闭原则规定一个软件实体,如类、模块和函数,应该对扩展开放,对修改关闭(在不修改原有代码的情况下增加新的功能)。其意思是,在程序需要进行拓展的时候,不能通过修改已有的代码实现变化,而应该通过扩展软件实体的方式实现,如根据需求重新派生一个实现类。想要达到这样的效果,这就需要使用接口,面向接口编程。(USB2.0与 USB3.0)
在软件的生命周期内,因为变化、升级和维护等原因而对软件原有代码进行修改,可能会向原有代码中引入错误,也可能不得不对原有代码整个进行重构,并且原有代码修改后还要重新进行测试。
3.里氏替换原则
里氏替换原则是面向对象设计的基本原则之一,是继承复用的基石。该原则规定所有引用基类的地方必须透明地使用其子类的对象。简单来说,所有使用基类代码的地方,如果换成子类对象还能够正确运行,则满足这个原则;否则就是继承关系有问题,应该取消原来的继承关系,重新设计它们之间的关系。这个原则可以用来判断继承关系是否合理。
4.依赖倒置原则
依赖倒置原则的核心思想是:依赖于约定面不依赖于具体实现,即面向接口编程。对象的依赖关系有3种传递方式。(Spring中将依赖对象通过构造注入和setter注入)(1)通过构造方法传递依赖对象,即构造方法的参数是需要依赖的接口类型。(2)通过 setter方法传递依赖对象,即setter方法的参数是需要依赖的接口类型。女(3)接口声明依赖,即接口方法的参数是需要依赖的接口类型。
如果开闭原则是面向对象设计的目标,那么依赖倒置原则就是实现开闭原则的重要途径之一它降低了客户与实现模块之间的耦合。
5.接口隔离原则
接口隔离原则要求尽量将庞大臃肿的接口拆分成更小、更具体的接口,让接口中只包含客户感兴趣的方法。客户不应该被迫去依赖他不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用;否则,很多实现类被迫去实现它们不需要的方法。
接口隔离原则和单一职责原则都是为了提高类的内聚性,降低它们之间的耦合度,但两者
是不同的。
单一职责原则注重的是职责,而接口隔离原则注重的接口依赖的隔离。
单一职责原则主要是约束类,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口主要针对抽象和程序整体框架的构建。
6.迪米特法则
迪米特法则又称为最少知道原则,是指一个软件实体应当尽可能少地与其他实体发生相互作用、具体来讲,被依赖的类应尽量将复杂逻辑封装在类的内部,不对外露任何中间信息,使客户对中间过程中的其他实体保持最少的了解,从而减少不必要的依赖,降低耦合(类似于电脑的 USB 接口,其具体的实现会封装到电脑的内部,提供一个简单的接口供外部实体去调用如此在对 USB 接口的具体实现进行修改时,不会影响其他实体类的调用)。
7.合成复用原则
合成复用原则是指:尽量使用组件/聚合的方式,而不是继承关系达到软件复用的目的。继承复用是类型的复用,必须具备-a关系才可通过继承方式进行复用,且从基类继承而来的实现是静态的,不可能在运行期间发生变化,因此没有足够的灵活性。
而合成复用是 has-a 关系,将已有对象纳入到新对象中使之成为新对象的一部分,因此新对象可以调用已有对象的功能。使用合成复用方式,新对象可以在运行期间动态地引用与成分对象类型相同的实现。
2.设计模式的应用
2.1工厂方法模式
症结在于NewsServicelmpl的代码中明确创建了NewsDao 接口的具体实现类NewsDaolmpl的实例,导致NewsServicelmpl类和 NewsDaoimpl类紧密耦合在一起丧失了使用灵活性。而解决思路是避免在ewsServicelmpl中创建具体的NewsDao 实现类,将创建工作转移出来,避免Newsservicelmpl和任何一个 NewsDao 实现类耦台。
NewsServicelmpl需要一个NewsDao的实例。如果把这个实例(NewsDaolmpl对象)看作产品那么负责创建这个实例的组件就可以理解为生产这个产品的工厂。
在 src 下创建 factory包,在此包中定义负责创建 NewsDao 实例的工厂类。关键代码如下:
public class SimpleDaoFactory {
/**
* 创建NewsDao的工厂类
* @param key
* @return
*/
public static NewsDao getInstance(){
return new NewsDaoImpl();
}
}
展示的是简单工厂模式,又叫做静态工厂方法模式,它不属于 GoF 的 23 种设计模式之一,简单工厂模式是工厂模式家族中最简单的一种设计模式,可以理解为工厂模式的一个特殊实现。
在 NewsServicelmpl中,使用工厂类解耦合。
在 NewsServicelmpl 中通过 SimpleDaoFactory与 NewsDaolmpl 解耦合,代码如下:
public class NewsServiceImpl implements NewsServer {
//所依赖的NewDao对象
private NewsDao dao = SimpleDaoFactory.getInstance();
public void addNews(News news) {
//调用NewsDao对象
dao.save(news);
}
或者根据依赖倒置原则,使用setter方法传递依赖关系。
根据依赖倒置原则,使用 setter方法传递依赖关系。关键代码如下:
(1)修改NewsSericelmpl类,为NewsDao 属性添加 setter方法。
public class NewsServiceImpl implements NewsServer {
private NewsDao dao;
public void setDao(NewsDao dao) {
this.dao = dao;
}
public void addNews(News news){
dao.save(news);
}
}
在测试方法中,通过Newssericelmp!类的seter方法传递依赖关系。
public class NewsServiceImplTest {
@Test
public void addNews() throws Exception {
NewsDao dao = SimpleDaoFactory.getInstance();
NewsServiceImpl service = new NewsServiceImpl();
service.setDao(dao);
News news = new News();
news.setNtitle("测试标题4");
news.setNcontent("测试内容4");
service.addNews(news);
}
}
简单工厂模式也可以根据不同的参数返回不同的实例,被创建的实例应具有共同的父类。
在简单工厂模式中,根据 String 类型参数创建 NewsDao 接口的不同实现类。关键代码:
1.创建NewsDao接口不同实现类略
2.修改工厂方法,根据参数值返回NewsDao的不同实例。
public class SimpleDaoFactory {
public static NewsDao getInstance(String key){
switch (key){
case "mysql":
return new NewsDaoMysqlImpl();
case "oracle":
return new NewsDaoOracleImpl();
case "redis":
return new NewsDaoRedisImpl();
default:
throw new RuntimeException("无效的数据库类型:"+key+",Dao获取失败");
}
}
}
3.通过key获取指定NewsDao实例,改变key值可获得不同的NewsDao实例。
public class NewsServiceImplTest {
@Test
public void addNews() throws Exception {
NewsDao dao = SimpleDaoFactory.getInstance("mysql");
AbstractFactory factory = new MySqlDaoFactory();
NewsServiceImpl service = new NewsServiceImpl();
service.setDao(dao);
News news = new News();
news.setNtitle("测试标题4");
news.setNcontent("测试内容4");
service.addNews(news);
}
}
以上示例可以看出,简单工厂模式包含如下角色。
工厂(Factory):简单工厂模式的核心,负责实现创建所有实例的逻辑。工厂类提供静态方法,根据传入的参数创建所需的产品实例。(示例中的SimpleDaoFactony)
抽象产品(Product):工厂创建的所有实例的父类型,是负责描述所有产品的公共接口。可以是接口或抽象类(示例中的 NewsDao 接口)。
具体产品(Concrete product):抽象产品的实现类,是工厂的创建目标,工厂所创建的实例就是某个具体产品类的实例(示例中的 NewsDao 接口的众多实现类)。
客户程序 NewsServicelmpl只需要知道工厂和抽象的父类产品(NewsDao 接口),不需要关心具体的产品如何创建(不需要知道接口的具体实现),内部如何变化。具体产品(接口实现类)被父类型(接口)包装,与客户程序解耦合,不影响客户程序(service 接口实现类)的复用。
简单工厂类图
对于要创建的产品不多且逻辑不复杂的情况,可以考虑简单工厂模式,需要增加产品类别时修改工厂方法即可实现。但也正因如此,简单工厂模式不适合创建逻辑比较复杂的情况,杂的产品逻辑会导致工厂方法难以维护,并且增加新的产品就需要修改工厂方法判断逻辑,这与开闭原则相违背。而工厂方法模式是对简单工厂模式的进一步抽象化,工厂方法模式的主要角色如下(对工厂进一步抽象)。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能(Dao 接口)。
抽象工厂(Abstract Factory):提供了创建产品的接口,声明创建方法,该方法返回值为抽象产品类型,调用者通过抽象工厂接口访问具体工厂的方法来创建产品(提供创建Da0接口实现类实例的接口)。
具体产品(Concrete Product):实现了抽象产品所定义的接口,由具体工厂创建(Dao 接口实现类)。
具体工厂(Concrete Factory):实现抽象工厂中的抽象创建方法,完成某个具体产品的创建
具体工厂和具体产品之间存在对应关系(负责创建 Dao接口实现类的实例)。
工厂方法模式的类图;
工厂方法模式通过定义一个抽象工厂接口,将产品对象的实例创建工作推迟到具体工厂实现类中。工厂方法模式的主要优点如下:
1.客户只需要知道具体工厂的名称就可以得到所要的产品,无须知道产品的具体创建过程。
2.基于多态,便于对复杂逻辑进行封装管理,并且在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任务修改,满足开闭原则。
缺点如下:
每增加一个产品就需要增加一个具体产品类和对应的一个具体工厂类,这增加了系统的复杂度。
使用工厂方法模式管理不同 NewsDao 实现的创建工厂
1.创建抽象工厂接口
public interface AbstractFactory {
public NewsDao getInstance();
}
2.为不同的NewsDao实现创建相对应的具体工厂,以生产NewsDaoMysqlImpl实例的工厂为例。
public class MySqlDaoFactory implements AbstractFactory {
@Override
public NewsDao getInstance() {
return new NewsDaoMysqlImpl();
}
}
3.测试方法
public class NewsServiceImplTest {
@Test
public void addNews() throws Exception {
// NewsDao dao = SimpleDaoFactory.getInstance("mysql");
AbstractFactory factory = new MySqlDaoFactory();
NewsDao dao = factory.getInstance();
NewsServiceImpl service = new NewsServiceImpl();
service.setDao(dao);
News news = new News();
news.setNtitle("测试标题4");
news.setNcontent("测试内容4");
service.addNews(news);
}
}
3.代理模式
在生活中,我们经常听说房产中介、婚介、经纪人等社会角色,这些都是代理模式的实现体现。这种模式其实也是单一职责原则的体现,就好像一个人要买房,中间会涉及很多的环节,部分流程复杂而且专业。如果都由买家独自完成,也许很难做好,事情多影响本职工作不说,还不够专业很容易出现问题。这时候,房产中介就可以发挥他的作用了。通过房产中介,买家专注看房、签合同。而中介则联系房源、推进流程。这样做可以分工明确,合作共赢。这就是代理模式典型的任务场景。
代理模式包含如下角色。
抽象主题(Subject):通过接口或抽象类声明业务方法(NewsDao接口)。
真实主题(Real Subject):实现了抽象主题中的具体业务,是实施代理的目标对象,即代理对象所代表的真实对象,是最终要引用的对象(NewsDao 接口的实现类)。
代理(Proxy):提供了与真实主题相同的接口,其内部含有对真实主题的引用,可以访问、控制或扩展真实主题的功能。(
在接口中定义一个买房的方法,真实对象张三去实现买房操作)
实现代理模式有多种方法,总体上分为静态代理和动态代理两种。静态代理由开发者针对抽象主题编写相关的代理类实现,编译之后生成代理类的 class 文件。
静态代理在编译时就已经实现,代理关系在编译期就已经绑定,编译完成后代理类是一个实际的 class 文件。
动态代理是在运行时动态生成的,即编译完成后没有实际的代理类 cass文件,而是在运行时动态生成代理类字节码,并加载到JVM 中。
接下来,通过房产中介的例子介绍如何实现代理模式。首先关注静态代理方法。
1.定义抽象主题:买家业务接口。
public interface Buyer {
/**
* 查看反馈
* @return
*/
public String havealook();
}
2.定义真实主题,买家业务接口实现类。
public class RealBuyer implements Buyer {
private Logger logger = Logger.getLogger(RealBuyer.class);
@Override
public String havealook() {
logger.debug("实地查看一下");
return "一些意见";
}
}
3.定义代理,对Buyer接口的havealook()方法流程进行完善。
public class IntermediaryImpl implements Buyer {
private Logger logger = Logger.getLogger(IntermediaryImpl.class);
private Buyer target;
public IntermediaryImpl(Buyer target){
this.target=target;
}
@Override
public String havealook(){
before();
String feedback = target.havealook();
after();
return "看房记录: 买家反馈”"+feedback+"“";
}
private void before() {
logger.debug("前期准备");
logger.debug("查找房源");
logger.debug("和卖家沟通时间");
}
private void after() {
logger.debug("后期跟踪");
logger.debug("和买家沟通意见");
}
}
4.编写测试类
public class BuyerTest {
private Logger logger = Logger.getLogger(BuyerTest.class);
@Test
public void havealookImpl() throws Exception{
Buyer buyer = new IntermediaryImpl(new RealBuyer());
String result = buyer.havealook();
logger.debug(result);
}
}
对没有实现接口的目标对象实现静态代理,代码如下:
1.继承RealBuyer并重写其中的业务方法,得到代理对象。
public class IntermediarySubclass extends RealBuyer {
private Logger logger = Logger.getLogger(IntermediarySubclass.class);
@Override
public String havealook(){
before();
String feedback = super.havealook();
after();
return "看房记录: 买家反馈”"+feedback+"“";
}
private void before() {
logger.debug("前期准备");
logger.debug("查找房源");
logger.debug("和卖家沟通时间");
}
private void after() {
logger.debug("后期跟踪");
logger.debug("和买家沟通意见");
}
}
2.测试方法
Buyer buyer = new IntermediaryImpl();
String result = buyer.havealook();
logger.debug(result);
}
静态代理的实现方式虽然简单直观,但是需要手工编写代理对象。如果被代理的目标对象中方法出现调整,如添加方法,要对新添加的方法也进行代理就需要同时修改代理对象的定义,目标对象删除方法亦然,这违反了开闭原则。而对于没有实现接口的目标对象,如果项目中有多个类需要代理,就要通过继承的方式为每个类定义代理对象。而很多情况下,项目中都需要为多个类扩展同样的功能,如日志记录,项目中很多地方都需要记录日志,这样会非常麻(Spring中的声明式事务管理)。
针对以上静态代理存在的问题,可以通过动态代理加以解决。动态代理(Dynamic Proxy API)是从 JDK1.3 版本就已经引入的特性,它利用反射机制在运行时生成代理类的字节码,为Java平台带来了运行时动态扩展行为的能力。JDK动态代理的核心APl是java.lang.refect 包下的InvocationHandler接口和 Proxy 类。
InvocationHander接口是代理方法的调用处理程序,负责为代理方法提供业务逻辑,接口中的方法声明如下。
Object invoke(Object proxy,Method method,Object[]args):用于在代理实例上处理方法少调用并返回结果。代理对象实现自抽象主题接口,调用代理对象中的接口方法时会执行 InvocationHandler 的 invoke()方法,故 invoke()方法中定义的即为代理业务的逻辑。参数 proxy 是正在执行方法的代理对象(中介),参数method 是正在被调用的接口方法(被代理对象中的目标方法),参数args是传递给接口方法的参数。
Proxy类负责动态创建代理类及其实例,主要方法如下。
static ObjectnewProxyInstance(ClassLoaderloader Class<?>[]interfacesInvocationHandlerh):返回一个实现了指定接口的代理类实例,对接口方法的调用 会被指派到指定的调用处理程序。参数interfaces 是需要进行代理的接口类型,参数h是接口代理方法的调用处理程序,类加载器loader 用于加载动态生成的代理类。
3.1使用JDK动态代理
JDK动态代理只能针对接口实现代理,故目标对象必须通过抽象主题接口实现。
代码如下:
public class RealBuyer implements Buyer {
private Logger logger = Logger.getLogger(RealBuyer.class);
@Override
public String havealook() {
logger.debug("实地查看一下");
return "一些意见";
}
}
2.通过IntermediarInvocationHandler接口定义房产中介代理业务规则。
public class IntermediarInvocationHandler implements InvocationHandler {
private Logger logger = Logger.getLogger(IntermediarInvocationHandler.class);
public void setTarget(Object target) {
this.target = target;
}
/**
* 被代理的目标对象,及其内部含有对真实主题的引用
* @param proxy the proxy instance that the method was invoked on
*
* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance. The declaring
* class of the {@code Method} object will be the interface that
* the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.
*
* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
* Arguments of primitive types are wrapped in instances of the
* appropriate primitive wrapper class, such as
* {@code java.lang.Integer} or {@code java.lang.Boolean}.
*
* @return
* @throws Throwable
*/
private Object target;
public void SetTarget(Object target){
this.target=target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object feedback = method.invoke(target,args);
after();
return "看房记录: 买家反馈”"+feedback+"“";
}
private void before() {
logger.debug("前期准备");
logger.debug("查找房源");
logger.debug("和卖家沟通时间");
}
private void after() {
logger.debug("后期跟踪");
logger.debug("和买家沟通意见");
}
}
3.定义代理工厂,使用Proxy类基于前文定义的代理业务规则指定接口动态生成代理对象。
public class IntermediaryCglibProxyFactory {
private static IntermediaryMethodInterceptor callback = new IntermediaryMethodInterceptor();
public static <T> T create(Class<T> target){
Enhancer enhancer = new Enhancer();
enhancer.setCallback(callback);
enhancer.setSuperclass(target);
return (T) enhancer.create();
}
}
4.测试代码
@Test
public void havealookJdkProxy() throws Exception{
Buyer buyer = IntermediaryJdkProxyFactory.create(new RealBuyer());
String result = buyer.havealook();
logger.debug(result);
}
JDK 动志代理是面向接口的代理实现,所以要求被代理的目标对象必须通过抽象主题接口进行定义,否则无法实施代理。虽然面向接口编程是非常推荐的做法,但实际开发中的情况更加复杂。如果需要代理的目标对象并非通过接口定义,可以使用CGLIB实现动态代理。