目录
0.单一职责模式
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任
典型:
- 装饰模式(Decorator)
- 桥接模式(Bridge)
1.基本介绍
定义:
- 动态(组合)地给一个对象增加一些额外的职责,就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)
装饰者模式体现了开放封闭原则(OCP原则)
2.动机
- 在某些情况下我们可能会“过度地使用继承来扩展对象地功能”,由于继承为类型引入的静态特质(比如下述案例1中CryptoFIleStream类中读文件流、定位文件流、写文件流都是固定的代码,不会改变),使得这种扩展方式缺乏灵活性;并且随着子列的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀
- 如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响降为最低?
3.类图
原理:
装饰者模式总是包含主体和包装(即在主体上的扩展功能),用打包一个快递解释
- 主体:比如我们要打包的衣服、陶瓷(被装饰者ConcreteComponent)
- 包装:包装主体的方式,比如报纸填充,塑料泡沫、纸板(装饰者Decorator)
装饰者模式的核心:
- 装饰者既继承了主体的接口,又组合了主体接口,
- 这样他就能重写主体的方法(因为继承),并且在重写的方法上调用主体对应方法(因为组合),
- 这样重写的同时在调用主体方法的前后都可以灵活地添加自己的代码
注意:
- 在如图的Component与ConcreteComponent之间,如果ConcreteComponent类很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象是一个类
4.案例
案例1:IO流操作
普通实现:
package cn.cqu.decorator;
public interface Stream {
char read(int number);
void seek(int position);
void write(char data);
}
package cn.cqu.decorator;
public class FileStream implements Stream{
@Override
public char read(int number) {
//读文件流
return 0;
}
@Override
public void seek(int position) {
//定位文件流
}
@Override
public void write(char data) {
//写文件流
}
}
package cn.cqu.decorator;
public class NetworkStream implements Stream{
@Override
public char read(int number) {
//读网络流
return 0;
}
@Override
public void seek(int position) {
//定位网络流
}
@Override
public void write(char data) {
//写网络流
}
}
package cn.cqu.decorator;
public class CryptoFIleStream extends FileStream{
@Override
public char read(int number) {
//额外的加密操作
super.read(number);//读文件流
return 0;
}
@Override
public void seek(int position) {
//额外的加密操作
super.seek(position);//定位文件流
//额外的加密操作
}
@Override
public void write(char data) {
//额外的加密操作
super.write(data);//写文件流
//额外的加密操作
}
}
package cn.cqu.decorator;
public class CryptoNetworkStream extends NetworkStream{
@Override
public char read(int number) {
//额外的加密操作
super.read(number);//读网络流
return 0;
}
@Override
public void seek(int position) {
//额外的加密操作
super.seek(position);//定位网络流
//额外的加密操作
}
@Override
public void write(char data) {
//额外的加密操作
super.write(data);//写网络流
//额外的加密操作
}
}
package cn.cqu.decorator;
public class CryptoBufferedFIleStream extends FileStream{
@Override
public char read(int number) {
//额外的缓冲操作
//额外的加密操作
super.read(number);//读文件流
return 0;
}
@Override
public void seek(int position) {
//额外的缓冲操作
//额外的加密操作
super.seek(position);
//额外的加密操作
}
@Override
public void write(char data) {
//额外的缓冲操作
//额外的加密操作
super.write(data);
//额外的加密操作
}
}
package cn.cqu.decorator;
public class CryptoBufferedNetworkStream extends FileStream{
@Override
public char read(int number) {
//额外的缓冲操作
//额外的加密操作
super.read(number);//读网络流
return 0;
}
@Override
public void seek(int position) {
//额外的缓冲操作
//额外的加密操作
super.seek(position);//定位网络流
//额外的加密操作
}
@Override
public void write(char data) {
//额外的缓冲操作
//额外的加密操作
super.write(data);//写网络流
//额外的加密操作
}
}
上述代码大致继承体系:
n表示主体类的数量,m表示装饰者的数量
分析问题:
- 1.因为部分扩展功能,整个继承体系变得异常庞大
- 2.代码重复性很高,比如CryptoFIleStream中的加密操作都是相同的代码,CryptoBufferedFIleStream中的缓冲操作和加密操作在read、seek、write方法中都是相同的代码
装饰模式重构上述代码实现:
Stream.java
package cn.cqu.decorator;
public abstract class Stream {
abstract char read(int number);
abstract void seek(int position);
abstract void write(char data);
}
主体类
FileStream.java
package cn.cqu.decorator;
public class FileStream extends Stream{
@Override
public char read(int number) {
//读文件流
return 0;
}
@Override
public void seek(int position) {
//定位文件流
}
@Override
public void write(char data) {
//写文件流
}
}
NetworkStream.java
package cn.cqu.decorator;
public class NetworkStream extends Stream{
@Override
public char read(int number) {
//读网络流
return 0;
}
@Override
public void seek(int position) {
//定位网络流
}
@Override
public void write(char data) {
//写网络流
}
}
装饰类
DecoratorStream.java
package cn.cqu.decorator;
public abstract class DecoratorStream extends Stream{
protected Stream stream;
}
CryptoStream.java
package cn.cqu.decorator;
public class CryptoStream extends DecoratorStream{
public CryptoStream(Stream stream) {
this.stream = stream;
}
@Override
public char read(int number) {
//额外的加密操作
stream.read(number);//读文件流
return 0;
}
@Override
public void seek(int position) {
//额外的加密操作
stream.seek(position);
//额外的加密操作
}
@Override
public void write(char data) {
//额外的加密操作
stream.write(data);
//额外的加密操作
}
}
BufferedStream.java
package cn.cqu.decorator;
public class BufferedStream extends DecoratorStream{
public BufferedStream(Stream stream){
this.stream = stream;
}
@Override
public char read(int number) {
//额外的缓冲操作
stream.read(number);//读文件流
return 0;
}
@Override
public void seek(int position) {
//额外的缓冲操作
stream.seek(position);
}
@Override
public void write(char data) {
//额外的缓冲操作
stream.write(data);
}
}
客户端:
package cn.cqu.decorator;
public class Client {
public static void main(String[] args) {
//定义一个文件流
FileStream fileStream = new FileStream();
//加密文件流(对于扩展操作即装饰,需要传入一个被装饰者即主体)
CryptoStream cryptoStream = new CryptoStream(fileStream);
//给加密后的文件流加入缓冲操作
BufferedStream bufferedStream01 = new BufferedStream(fileStream);
//加密缓冲文件流
BufferedStream bufferedStream02 = new BufferedStream(cryptoStream);
}
}
上述代码大致继承体系:
案例2:星巴克咖啡订单项目
- 1.咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decat(无因咖啡)
- 2.调料:Milk、Soya、Chocolate
- 3.要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
- 4.使用OO的来计算不同种类咖啡的费用:客户可以点单品咖啡,也可以点单品咖啡+调料组合
方案1:
解释:
- 1.Drink是一个抽象类,表示饮料
- 2.des就是对咖啡的描述,比如咖啡的名字
- 3.cost()方法就是计算费用,Drink类中做成一个抽象方法
- 4.Decaf就是单品咖啡,继承Drink
- 5.Espress&&Milk就是单品咖啡+调料,这个组合很多
问题分析:
- 这样设计,会有很多类,当我们增加一个单品咖啡或者一个新的调料时,类的数量就会倍增,就会出现类爆炸
方案2:
- 方案1中因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到Drink类,这样就不会造成类数量过多,从而提高项目的维护性
可以将milk、soy、chocolate设计成int,代表要加几份对应的调料
分析问题:
虽然使类的数量降低,但是还是存在问题
- 在子类中添加或删除种类时,每个都需要去写是否需要添加每种调料的代码,代码的维护量很大(每个子类都需要维护,我们又“闻到了重复代码的味道”)
- 增加新的调料种类时,既要修改父类,又要修改子类,违背了开放封闭原则
方案3:使用装饰者模式进行重构
代码实现:
Drink.java
package cn.cqu.decorator2;
public abstract class Drink {
public String des;//描述
private float price = 0.0f;
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
/**
* 计算费用的抽象方法
* 子类来实现
*/
public abstract float cost();
}
coffee包下:
Coffee.java
package cn.cqu.decorator2.coffee;
import cn.cqu.decorator2.Drink;
public class Coffee extends Drink {
@Override
public float cost() {
return super.getPrice();
}
}
Espresso.java
package cn.cqu.decorator2.coffee;
public class Espresso extends Coffee {
public Espresso(){
setDes("意大利咖啡");
setPrice(16.0f);
}
}
LongBlack.java
package cn.cqu.decorator2.coffee;
public class LongBlack extends Coffee {
public LongBlack(){
setDes(" longblack ");
setPrice(15.0f);
}
}
ShortBlack.java
package cn.cqu.decorator2.coffee;
public class ShortBlack extends Coffee {
public ShortBlack() {
setDes(" shortblack ");
setPrice(14.0f);
}
}
decorator包下:
Decorator.java
package cn.cqu.decorator2.decorator;
import cn.cqu.decorator2.Drink;
public class Decorator extends Drink {
private Drink drink;
public Decorator(Drink drink){
this.drink = drink;
}
@Override
public float cost() {
return super.getPrice()+drink.cost();
}
//输出被装饰者的描述信息
@Override
public String getDes(){
return super.des + " " + super.getPrice() + "&&" +drink.getDes();
}
}
Chocolate.java
package cn.cqu.decorator2.decorator;
import cn.cqu.decorator2.Drink;
public class Chocolate extends Decorator {
public Chocolate(Drink drink) {
super(drink);
setDes("巧克力");
setPrice(3.0f);//调味品的价格
}
}
Milk.java
package cn.cqu.decorator2.decorator;
import cn.cqu.decorator2.Drink;
public class Milk extends Decorator {
public Milk(Drink drink) {
super(drink);
setDes("牛奶");
setPrice(2.0f);
}
}
Soy.java
package cn.cqu.decorator2.decorator;
import cn.cqu.decorator2.Drink;
public class Soy extends Decorator {
public Soy(Drink drink) {
super(drink);
setDes("豆浆");
setPrice(1.5f);
}
}
客户端:
CoffeeBar.java
package cn.cqu.decorator2;
import cn.cqu.decorator2.coffee.LongBlack;
import cn.cqu.decorator2.decorator.Chocolate;
import cn.cqu.decorator2.decorator.Milk;
public class CoffeeBar {
public static void main(String[] args) {
System.out.println("========================");
//1.点一份longback
Drink order = new LongBlack();
System.out.println("点一份longback费用:"+order.cost());
System.out.println("点一份longback描述="+order.getDes());
System.out.println("========================");
//2.加入一份牛奶
order = new Milk(order);
System.out.println("加入一份牛奶费用:"+order.cost());
System.out.println("加入一份牛奶描述="+order.getDes());
System.out.println("========================");
//3.加入一份巧克力
order = new Chocolate(order);
System.out.println("加入一份巧克力费用:"+order.cost());
System.out.println("加入一份巧克力描述="+order.getDes());
System.out.println("========================");
//4.加入第二份巧克力
order = new Chocolate(order);
System.out.println("加入第二份巧克力费用:"+order.cost());
System.out.println("加入第二份巧克力描述="+order.getDes());
System.out.println("========================");
}
}
这种实现方式,不管我们是要新增coffee(主体)还是配料(装饰者)都只需要添加类,其他代码根本不需要去修改,符合开放封闭原则
5.源码分析
- InputStream是抽象类,相当于我们上述案例2中的Drink
- FileStream继承了InputStream,是主体对象(被装饰者)
- FilterInputStream既继承了InputStream,也组合了InputStream,是装饰者,类似于案例2中的Decorator
- DataInputStream是FilterInputStream的子类,具体的修饰者,类似于案例中的Milk、Soy等
6.总结
- 1.通过采用组合而非继承的手法,Decorator模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能,避免了使用继承带来的“灵活性差”和“多子类衍生问题”
- 2.Decorator类在接口上表现为is-a Component的继承关系,即Decorator继承了Component类的所有接口,但在实现上有表现为has-a Component的组合关系,即Decorator类又使用了另一个Component类
- 3.Decorator模式的目的并非解决“多子类衍生的多继承问题”,Decorator模式应用的要点在于解决“主体类在多个方向上的扩展功能”——即为“装饰”的含义