目录
1、什么是设计模式?设计模式有什么用?
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。
2、设计模式应该遵循的面向对象设计原则
1994年,在由设计模式四人帮GOF出版的著作Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素)中提出,设计模式应该遵循以下两条面向对象设计原则:
- 对接口编程而不是对实现编程
- 优先使用对象组合而不是继承
“对接口编程而不是对实现编程”,对于这句话我的理解就是:要善于使用多态。变量的声明尽量使用超类型(父类),而不是某个具体的子类,超类型中的各个具体方法的实现都是写在不同的子类中。程序在执行时能够根据不同的情况来调用到不同的子类方法,这样做更加灵活,并且我们在声明一个变量时无需关心以后执行时的真正的数据类型是哪种(某个子类类型),这是种解耦合(松耦合)的思想。实例代码如下:
package Test;
public interface Animal {
public void makenoise();
}
package Test;
public class Dog implements Animal {
@Override
public void makenoise() {
System.out.println("汪汪汪!");
}
}
class Cat implements Animal{
@Override
public void makenoise() {
System.out.println("喵喵喵!");
package Test;
public class AnimalTest {
public static void hearnoise(Animal animal){
animal.makenoise();
}
public static void main(String[] args) {
AnimalTest.hearnoise(new Dog());
AnimalTest.hearnoise(new Cat());
}
}
执行结果:
汪汪汪!
喵喵喵!
3、设计模式的六大原则
- 开闭原则(Open Close Prinprinciple),开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
- 里氏代换原则(Liskov Substitution Principle),里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范(LSP我曾经在另一篇文章重新思考接口和抽象类中举了一个例子)。
- 依赖倒转原则(Dependence Inversion Principle),这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
- 接口隔离原则(Interface Segregation Principle),使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
- 迪米特法则,又称最少知道原则(Demeter Principle),一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
- 合成复用原则(Composite Reuse Principle),尽量使用合成/聚合的方式,而不是使用继承。
4、设计模式的四种类型(包括J2EE设计模式)
创建型模式,这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。创建者模式包括以下几种设计模式:
- 工厂模式
- 抽象工厂模式
- 单例模式
- 建造者模式
- 原型模式
结构型模式,这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。结果型模式包括以下几种设计模式:
- 适配器模式
- 桥接模式
- 过滤器模式
- 组合模式
- 装饰器模式
- 外观模式
- 享元模式
- 代理模式
行为型模式,这些设计模式特别关注对象之间的通信。行为型模式包括以下几种设计模式:
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 空对象模式
- 策略模式
- 模板模式
- 访问者模式
J2EE模式,这些设计模式特别关注表现层,这些模式是由Sun Java Center鉴定的。J2EE模式包括以下几种设计模式:
- MVC模式
- 业务代表模式
- 组合实体模式
- 数据访问对象模式
- 前端控制器模式
- 拦截过滤器模式
- 服务器定位器模式
- 传输对象模式
5、几种常见的设计模式
5.1、工厂模式
工厂模式(Factory Pattern)是Java中最常见的设计模式之一,属于创建者模式。顾名思义,它的思路是设计一个对象生产工厂,它提供了一种绝佳的创建对象的方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过一个共同的接口来创建对象。
主要解决的问题:解决接口选择的问题。定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
优点:
- 一个调用者想创建一个实例对象,只需要知道其名字就行。
- 扩展性高,如果想增加一个产品,只要扩展工厂类就行。
- 屏蔽了产品的具体实现,调用者只关心产品的接口。
缺点:
- 每次增加一个产品,都需要增加一个具体实现类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂性,同时也增加了系统具体类的依赖,这并不是什么好事。
实现示例:就以一家生产多个不同品牌的汽车生产厂为例,先创建一个Car接口和三个实现类BENZ、BMW、TOYOTA,再定义一个工厂类CarFactory。我们使用这个工厂类CarFactory来生产不同品牌的汽车。
package FactoryDemo;
public interface Car {
public void Brand();
}
public class BENZ implements Car{
@Override
public void Brand() {
System.out.println("生产一辆奔驰");
}
}
public class BMW implements Car{
@Override
public void Brand() {
System.out.println("生产一辆宝马");
}
}
public class TOYOTA implements Car{
@Override
public void Brand() {
System.out.println("生产一辆丰田");
}
}
//用来生成汽车的工厂类
//equalsIgnoreCase()方法只能比较字符串,equals()可以比较字符串和对象,且equalsIgnoreCase()
//中不区别大小写,A-Z和a-z是一样的
public class CarFactory {
public Car getcar(String carbrand) {
if (carbrand.equalsIgnoreCase("BENZ")) {
return new BENZ();
} else if (carbrand.equalsIgnoreCase("BMW")) {
return new BMW();
} else if (carbrand.equalsIgnoreCase("TOYOTA")) {
return new TOYOTA();
} else
System.out.println("对不起我们不生产这辆车");
return null;
}
}
//实例化工厂类
public class CarFactoryTest {
public static void main(String[] args) {
CarFactory carfactory = new CarFactory();
Car car1 = carfactory.getcar("Benz");
car1.Brand();
Car car2 = carfactory.getcar("Bmw");
car2.Brand();
Car car3 = carfactory.getcar("toyota");
car3.Brand();
}
}
执行结果:
生产一辆奔驰
生产一辆宝马
生产一辆丰田
5.2、抽象工厂模式
抽象工厂模式是围绕一个超级工厂创建其他工厂,这个超级工厂是生产其他工厂的工厂。这种设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式地指定它们的类。每个生成的工厂都能按照工厂模式提供对象。
主要解决的问题:主要解决接口选择的问题。系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
优点:当一个产品族中的多个对象被设计在一起工作时,它能保证客户端始终只使用一个产品族中的对象。
缺点:产品族的扩展非常困难,要增加一个系列的某个产品,不但要在创造工厂里新增大量代码,还要在具体实现里加代码,产品族难以扩展。
实现示例:还是以上一个实例为基础来说明抽象工厂模式。一家汽车集团公司,旗下有两个工厂,一家是生产国外品牌车(BENZ、BMW、TOYATA),另一家是生产国内品牌车(JAC、BYD、ROEWE)(这就相当于两个产品族)。集团有一家自营销售门店,采取顾客下单后再生产的经营策略(这就相当于系统只在一个时刻消费某一族的产品)。有两个接口:ForeignCar(国外品牌车)、DomesticCar(国内品牌车),汽车实现类:Benz、Bmw、Toyota、Jac、Byd、Roewe。一个工厂抽象类AbstractFactory,两个工厂类继承自这个抽象类:ForeigncarFactory、DomesticcarFactory。最后还有一个工厂生产者FactoryProducer和一个测试类AbstractFactoryTest。具体代码实现如下:
国外品牌车接口及实现类
public interface ForeignCar {
public void Brand();
}
public class BNEZ implements ForeignCar{
@Override
public void Brand() {
System.out.println("生产一辆奔驰");
}
}
public class BMW implements ForeignCar{
@Override
public void Brand() {
System.out.println("生产一辆宝马");
}
}
public class TOYOTA implements ForeignCar{
@Override
public void Brand() {
System.out.println("生产一辆丰田");
}
}
国内品牌车接口及实现类
public interface DomesticCar {
public void Brand();
}
public class BYD implements DomesticCar{
@Override
public void Brand() {
System.out.println("生产一辆比亚迪");
}
}
public class JAC implements DomesticCar{
@Override
public void Brand() {
System.out.println("生产一辆江淮");
}
}
public class ROEWE implements DomesticCar{
@Override
public void Brand() {
System.out.println("生产一辆荣威");
}
}
工厂的抽象类及两个工厂实现类
public abstract class AbstractFactory {
public abstract ForeignCar getforeigncar(String brand);
public abstract DomesticCar getdomesticcar(String brand);
}
public class ForeigincarFactory extends AbstractFactory{
@Override
public ForeignCar getforeigncar(String brand) {
if (brand.equalsIgnoreCase("Benz")){
return new BNEZ();
}
else if (brand.equalsIgnoreCase("Bmw")){
return new BMW();
}
else if (brand.equalsIgnoreCase("Toyota")){
return new TOYOTA();
}
else
System.out.println("我们没有这个品牌授权");
return null;
}
@Override
public DomesticCar getdomesticcar(String brand) {
return null;
}
}
public class DomesticcarFactory extends AbstractFactory{
@Override
public ForeignCar getforeigncar(String brand) {
return null;
}
@Override
public DomesticCar getdomesticcar(String brand) {
if (brand.equalsIgnoreCase("Jac")){
return new JAC();
}
else if (brand.equalsIgnoreCase("Byd")){
return new BYD();
}
else if (brand.equalsIgnoreCase("Roewe")){
return new ROEWE();
}
else
System.out.println("我们没有这个品牌授权");
return null;
}
}
工厂生产者及测试类
public class FactoryProducer {
public static AbstractFactory CreateFactory(String choice){
if (choice.equalsIgnoreCase("ForeignCar")){
return new ForeigincarFactory();
}
else if (choice.equalsIgnoreCase("DomesticCar")){
return new DomesticcarFactory();
}
else
System.out.println("我们没有这个工厂");
return null;
}
}
public class AbstractFactoryTest {
public static void main(String[] args) {
AbstractFactory abstractFactory1 = FactoryProducer.CreateFactory("ForeignCar");
ForeignCar car1 = abstractFactory1.getforeigncar("toyota");
car1.Brand();
AbstractFactory abstractFactory2 = FactoryProducer.CreateFactory("DomesticCar");
DomesticCar car2 = abstractFactory2.getdomesticcar("byd");
car2.Brand();
}
}
执行结果
生产一辆丰田
生产一辆比亚迪
5.3、单例模式
单例模式(Siningleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。这种模式设计到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。单例模式需要注意一下三点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
主要解决的问题:一个全局使用的类频繁地创建和销毁。当使用者想控制实例的数量,节省系统资源的时候就可以使用单例模式。
优点:
- 在内存中只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例。
- 避免对资源的多重占用(比如写文件操作)。
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关系内部实现逻辑,而不应该关注外部怎么来实例化。
常见使用场景:
- 要求生产唯一序列号。
- WEB中的计数器,不必每一次刷新都在数据库中加1,可以用单例先缓存起来。
- 创建一个对象需要消耗的资源过多,比如I/O与数据库的连接等。
实现示例:SingletonObject是一个单例类,SingletonPatternTest是测试类。
package SingletonDemo;
public class SingletonObject {
//创建这个类的唯一对象
private static SingletonObject instance = new SingletonObject();
//让构造函数私有化,确保外部不能调用这个类的构造器来实例化对象
private SingletonObject(){
}
//获取这个唯一可用对象
public static SingletonObject getInstance(){
return instance;
}
public void showmessage(){
System.out.println("这是单例模式");
}
}
package SingletonDemo;
public class SingletonPatternTest {
public static void main(String[] args) {
SingletonObject object = SingletonObject.getInstance();
object.showmessage();
}
}
单例模式的实现有多种方式(懒汉式和饿汉式有什么区别?为什么要这么叫?区别就在于创建对象的时机不同,懒汉式是当你需要时才去创建对象,而饿汉式是不管你需不需要,一开始就会创建对象):
- 懒汉式(线程不安全)
- 懒汉式(线程安全)
- 饿汉式
- 双检锁/双重校验锁(DCL,即double-checked locking)
- 登记式/静态内部类
- 枚举
懒汉式(线程不安全)
这种方式是最基本的实现方式,它最大的问题就是多线程不安全。原因是没有加锁synchronized,所以严格意义上来说它不属于单例模式,这种方式lazy loading(延迟加载)很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
懒汉式(线程安全)
这种方式具备很好的lazy loading,能够在多线程中很好的工作,但是效率很低,99%的情况下不需要同步。第一次调用才初始化,避免了内存浪费。但是它必须加锁才能够保证单例,加锁必定会影响效率。
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
饿汉式(线程安全)
它是基于classloader机制避免了多线程的同步问题,不过instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中都是调用getInstance()方法,但是也不能确定有其他的方式导致类装载,这是初始化显然没有达到lazy loading的效果。这种方式比较常用,但是容易产生垃圾对象。优点是没有加锁,执行效率高。缺点是类加载时就初始化,浪费内存。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
双检锁/双重校验锁(DCL,即double-checked locking)
采用双检锁的方式,安全且多线程情况下能保持高性能。这种方式中getInstance()的性能对应用程序很关键。
public class Singleton {
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
/**
*为什么instance要使用volatile关键字修饰?
*主要是为了防止发生指令重排
*在 instance = new Singleton() 中主要有三步:
*1、为Singleton()对象分配内存空间
*2、初始化Singleton()对象
*3、将instance指向Singleton()对象的内存地址
*如果cpu进行优化,发生指令重排的话,那么步骤可能就会变成132
*就有可能出现线程中A指令执行完1和3两步,另一个线程B发现instance不为空,于是直接拿去用,
*但是这时Singleton()对象还没有初始化
**/
还有两种实现方式就不一一列出了,当明确要求实现lazy loading效果时,要使用“登记式/静态内部类”方式;如果涉及到反序列化创建对象时,可以尝试使用枚举。第一种和第二种懒汉式不推荐使用,一般都是使用第三种饿汉式。
5.4、适配器模式
适配器模式是作为两个不兼容的接口之间的桥梁,这种设计模式属于结构性模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实例子,读卡器是作为内存卡和笔记本之间的适配器,将内存卡插入读卡器,再将读卡器插入笔记本,这样就能通过笔记本来读取内存卡了。特别注意:适配器模式不是在详细设计时添加的,而是针对解决正在服役的项目的问题。
主要解决:主要解决在软件系统中,常常要将一些“现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。
使用场景:
- 系统需要使用现有的类,而此类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
- 通过接口转换,将一个类插入另一个类系中(比如老虎和飞禽,现在要增加一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容老虎对象,实现飞的接口)。
优点:
- 可以让两个没有任何关联的类一起运行。
- 提高了类的复用。
- 增加了类的透明度。
- 灵活性好。
缺点:
- 过多的使用适配器会让系统非常的零乱,不易整体把控。比如,明明看到的是A接口,内部被适配成了B接口的实现,一个系统如果出现太多这样的问题,那么无异于异常灾难。因此如果不是特别有必要的话,可以不使用适配器,而是对系统直接进行重构。
- 由于Java至多只能继承一个类,所以至多只能适配一个适配器类,并且目标类必须是抽象类。
实现示例:我们有一个Mp3接口和一个实现了该接口的实现类Mp3Player,默认情况下Mp3Player可以播放mp3格式的音频文件;还有另一个MediaPlayer接口和实现了该接口的实现类Mp4Player及VlcPlayer,这两个实现类分别可以播放vlc和mp4格式的文件。我们想让Mp3Player来播放vlc和mp4格式的音频文件,那么就需要创建一个实现了Mp3接口的适配器类MediaAdapter,并且使用Mp4Player对象来播放所需要的格式音频文件。
Mp3Player使用适配器类MediaAdapter传递所需的音频类型,不需要知道能播放所需音频格式的实际类。我们在演示类AdapterPatternDemo来使用Mp3Player来播放各种音频格式。
Mp3接口及MediaPlayer接口:
public interface Mp3 {
//传入参数:音频类型,音频文件名
public void play(String audioType, String filename);
}
public interface MediaPlayer {
public void playVlc(String filename);
public void playMp4(String filename);
}
MediaPlayer接口的实现类Mp4Player、VlcPlayer:
public class Mp4Player implements MediaPlayer{
@Override
public void playVlc(String filename) {
}
@Override
public void playMp4(String filename) {
System.out.println("Playing the MP4 file,filename: " + filename);
}
}
public class VlcPlayer implements MediaPlayer {
@Override
public void playVlc(String filename) {
System.out.println("Playing the Vlc file,filename: " + filename);
}
@Override
public void playMp4(String filename) {
}
}
适配器类MediaAdapter:
public class MediaAdapter implements Mp3{
//定义一个接口类型的引用变量来引来实现了该接口的实现类实例对象
MediaPlayer mediaPlayer;
public MediaAdapter(String audiotype){
if (audiotype.equalsIgnoreCase("vlc")){
mediaPlayer = new VlcPlayer();
}else if (audiotype.equalsIgnoreCase("mp4")){
mediaPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("vlc")){
mediaPlayer.playVlc(filename);
}else if (audioType.equalsIgnoreCase("mp4")){
mediaPlayer.playMp4(filename);
}
}
}
Mp3接口的实现类Mp3Player:
public class Mp3Player implements Mp3{
//定义一个接口类型的引用变量来引来实现了该接口的实现类实例对象
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("mp3")){
System.out.println("Playing the MP3 file,filename: " + filename);
}else if (audioType.equalsIgnoreCase("mp4") || audioType.equalsIgnoreCase("vlc")){
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType,filename);
}else{
System.out.println("该设备不支持这种音频格式播放!");
}
}
}
测试类AdapterPatternDemo:
public class AdapterPatternDemo {
public static void main(String[] args) {
Mp3Player mp3Player = new Mp3Player();
mp3Player.play("vlc","vlcfile1");
mp3Player.play("mp4","mp4file1");
mp3Player.play("mp3","mp3file1");
mp3Player.play("xml","xmlfile1");
}
}
执行结果:
Playing the Vlc file,filename: vlcfile1
Playing the MP4 file,filename: mp4file1
Playing the MP3 file,filename: mp3file1
该设备不支持这种音频格式播放!
5.5、装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种设计模式属于结构型模式,它是作为现有类的一个包装。装饰器模式创建了一个装饰类,用来包装原有的类,并且在保持类方法签名完整性的前提下,提供额外的功能。
主要解决:一般来说,我们为扩展一个类经常使用继承的方式实现,但由于继承为类引入了静态特性,并且随着扩展功能的增多,子类会很膨胀。所以当我们不想增加很多子类时我们就可以使用装饰器模式。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
使用场景:
- 扩展一个类的功能。
- 动态增加功能、动态撤销。
实现示例:把一个形状装饰上不同的颜色,同时又不改变形状类。创建一个Shape接口和实现了该接口的实体类,然后创建一个实现了Shape接口的抽象装饰类ShapeDecorator,并把Shape对象作为它的实例变量。RedShapeDecorator是实现了ShapeDecorator的实体类。我们在演示类DecoratorPatternDemo中使用RedShapeDecorator来装饰Shape对象。
Shape接口及实现类Circle、Rectangle:
public interface Shape {
public void draw();
}
public class Circle implements Shape{
@Override
public void draw() {
System.out.println("这是一个圆形!");
}
}
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("这是一个长方形!");
}
}
抽象的装饰器类ShapeDecorator:
public abstract class ShapeDecorator implements Shape{
protected Shape decoratorShape;
public ShapeDecorator(Shape decoratorShape){
this.decoratorShape = decoratorShape;
}
public void draw(){
decoratorShape.draw();
}
}
抽象类的实现类RedShapeDecorator:
public class RedShapeDecorator extends ShapeDecorator{
public RedShapeDecorator(Shape decoratorShape) {
super(decoratorShape);
}
@Override
public void draw(){
decoratorShape.draw();
setRedBorder(decoratorShape);
}
public void setRedBorder(Shape decoratedShape){
System.out.println("边框颜色:红色!");
}
}
测试类DecoratorPatternDemo:
public class DecoratoePatternDemo {
public static void main(String[] args) {
Shape circle = new Circle();
Shape redcircle = new RedShapeDecorator(new Circle());
Shape redrectangle = new RedShapeDecorator(new Rectangle());
System.out.println("圆形边框颜色正常!");
circle.draw();
System.out.println("\n红色边框的圆形");
redcircle.draw();
System.out.println("\n红色边框的长方形");
redrectangle.draw();
}
}
执行结果:
圆形边框颜色正常!
这是一个圆形!
红色边框的圆形
这是一个圆形!
边框颜色:红色!
红色边框的长方形
这是一个长方形!
边框颜色:红色!
5.6、代理模式
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种设计模式属于结构型模式。在代理模式中我们创建具有现有对象的对象,以便向外界提供功能接口。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。主要解决思路就是加上一个中间层。
何时使用:想在访问一个类时做一些控制。
优点:
- 职责清晰。
- 高扩展性。
- 智能化。
缺点:
- 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
常见使用场景:
- 远程代理、虚拟代理、Copy-on-Write代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
- Spring aop。
注意事项:
- 和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
- 和装饰器模式的区别:装饰器模式是为了增强功能,而代理模式是为了加以控制。
实现示例:经纪人和明星就是一个很明显的代理关系。一家公司想请明星来公司参加开业活动,那么公司一定是和经纪人取得联系,通过经纪人来安排明星出席开业活动,就相当于通过经纪人来控制明星的行为。定义一个接口Activity,明星类Star和经纪人Proxy都实现了这个接口,然后在演示类ProxyPatternDemo中来使用Proxy对象控制Star对象(经纪人接下这个活动实际是由明星来参加)。
Activity接口及实现类Star、Proxy:
public interface Activity {
public void campaign();
}
public class Star implements Activity{
private String activityname;
public Star(String activityname){
this.activityname = activityname;
}
@Override
public void campaign() {
System.out.println("我是明星,我来参加" + activityname +"活动!");
}
}
public class Proxy implements Activity{
private Star star;
private String activityname;
public Proxy(String activityname){
this.activityname = activityname;
}
@Override
public void campaign() {
if (star == null){
star = new Star(activityname);
}
star.campaign();
}
}
演示类ProxyPatternDemo:
public class ProxyPatternDemo {
public static void main(String[] args) {
Activity activity = new Proxy("九九隆开业庆典");
//经纪人接下这个活动,由明星来参加
activity.campaign();
}
}
执行结果:
我是明星,我来参加九九隆开业庆典活动!
5.7、策略模式
在策略模式(Strategy Pattern)中,一个类的行为或算法可以在运行时更改。这种设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的context对象,策略对象改变context对象的执行算法。
主要解决:在有多种算法相似的情况下,使用if...else所带来的复杂和难以维护。
何时使用:一个系统中有很多很多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。关键是要实现同一个接口。
优点:
- 算法可以自由切换。
- 避免使用多重条件判断。
- 扩展性良好。
缺点:
- 策略类会增多。
- 策略类会对外暴露。
常见使用场景:
- 旅游出行的交通方式,选择骑自行车、坐汽车、搭乘飞机,每一种出行方式就是一种策略。
- 如果在一个系统中有许多类,它们之间的却别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多种行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。
- 如果一个对象有很多行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于4个,就需要考虑使用混合模式,解决策略类膨胀的问题。
实现示例:定义一个数字运算接口Strategy,以及三个实现了该接口的实现类OperationAdd、OperationSub、OperationMul,这三个实现类代表了三个不同的算法策略。定义一个Context类来选择执行策略,并在StrategyPatternDemo演示类中进行演示。
Strategy接口以及三个实现类:
public interface Strategy {
public int doOperation(int num1, int num2);
}
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSub implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMul implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
策略选择类Context:
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
演示类StrategyPatternDemo:
public class StrategyPatternDemo {
public static void main(String[] args) {
Context context = new Context(new OperationAdd());
System.out.println("3 + 8 = " + context.executeStrategy(3, 8));
context = new Context(new OperationSub());
System.out.println("3 - 8 = " + context.executeStrategy(3, 8));
context = new Context(new OperationMul());
System.out.println("3 * 8 = " + context.executeStrategy(3, 8));
}
}
执行结果:
3 + 8 = 11
3 - 8 = -5
3 * 8 = 24
5.8、观察者模式
当对象存在一对多关系时,则使用观察者模式(Observer Pattern)。比如当一个对象被修改时,则自动通知它的依赖对象。观察者模式属于行为型模式(定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新)。比如说在拍卖时,拍卖师要观察出价最高的人,然后通知给其他参与竞价的人。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
关键代码:在抽象类中有一个ArrayList存放观察者们。
优点:
- 观察者和被观察者是抽象耦合的。
- 建立一套触发机制。
缺点:
- 如果一个被观察者有很多直接或间接的观察者的话,将所有观察者都通知到是一件很麻烦的事,要花费很多时间。
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间的循环调用,可能调至系统崩溃。
- 观察者模式没有相应的机制让观察者知道观察目标是如何发生变化的,而仅仅是知道观察目标发生了变化。
常见使用场景:
- 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们独立地改变和复用。
- 一个对象的改变将导致其他一个或多个对象也发生变化,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
- 一个对象必须通知其他对象,而不知道这些对象是谁。
注意事项:
- Java中已经有了对观察者模式的支持类。
- 避免循环引用。
- 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
实现示例:观察者模式使用三个类Subject、Observer、Client。Subject对象带有绑定观察者到Clinet对象和从Client对象解绑观察者的方法。我们创建Subject类、Observer抽象类和扩展了抽象类Observer的实体类。在演示类ObserverPatternDemo中使用Subject和实体类对象来演示观察者模式。
观察目标Subject类:
import java.util.ArrayList;
import java.util.List;
//Subject是观察目标,当subject的私有属性值state发生改变时,它的观察者们也做出相应的改变
public class Subject {
//将观察者们添加到ArrayList集合中
private List<Observer> observers = new ArrayList<Observer>();
//表示要改变的值
private int state;
public int getState(){
return state;
}
public void setState(int state){
this.state = state;
notifyAllObservers();
}
//增加观察者对象
public void attach(Observer observer){
observers.add(observer);
}
//遍历ArrayList中的观察者对象并执行观察者们的update()方法
public void notifyAllObservers(){
for (Observer observer : observers){
observer.update();
}
}
}
观察者模板,抽象类Observer:
//构建观察者模板
public abstract class Observer {
//这是观察目标
protected Subject subject;
//更新方法
public abstract void update();
}
观察者BinaryObserver类:
public class BinaryObserver extends Observer{
//构造函数中传入观察目标,并将自己添加到观察目标subject保存的观察者集合中
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toBinaryString()就是将输入的数字转换成二进制数,但是转换输出的是String类型的字符串
@Override
public void update() {
System.out.println("转成二进制数是 :" + Integer.toBinaryString(subject.getState()));
}
}
观察者OctalObserver类:
public class OctalObserver extends Observer{
public OctalObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toOctalString()是将十进制数转换成八进制数并以字符串的类型输出
@Override
public void update() {
System.out.println("转成八进制数是 :" + Integer.toOctalString(subject.getState()));
}
}
观察者HexaObserver类:
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
//Integer.toHexString()是将十进制数转成十六进制并以字符串类型输出
//toUpperCase()是将小写字符转换成大写字符
@Override
public void update() {
System.out.println("转成十六进制数是 :" + Integer.toHexString(subject.getState()).toUpperCase());
}
}
演示类ObserverPatternDemo:
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new BinaryObserver(subject);
new OctalObserver(subject);
new HexaObserver(subject);
System.out.println("第一次state值改变为: 15");
subject.setState(15);
System.out.println("\n第二次state值改变为: 10");
subject.setState(10);
}
}
执行结果:
第一次state值改变为: 15
转成二进制数是 :1111
转成八进制数是 :17
转成十六进制数是 :F
第二次state值改变为: 10
转成二进制数是 :1010
转成八进制数是 :12
转成十六进制数是 :A