文章目录
Leetcode-设计模式-结构型模式
一、适配器模式
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用:
- 1、系统需要使用现有的类,而此类的接口不符合系统的需要。
- 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
- 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
如何解决:继承或依赖(推荐)。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
菜鸟教程适配器模式
二、桥接模式(将两个独立的结构联系起来,两个被联系起来的结构是可以独立变化的,如果使用继承会使类变的特别多)
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展(比如咖啡的大杯中杯、小杯,加糖、加奶、加椰果等两个维度)。
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
桥梁模式所涉及的角色有:
- 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用。
- 修正抽象化(RefinedAbstraction)角色:扩展抽象化角色,改变和修正父类对抽象化的定义。
- 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
- 具体实现化(ConcreteImplementor)角色:这个角色给出实现化角色接口的具体实现。
例子
第一步:创建抽象化部分:
//抽象化Abstraction
public abstract class Coffee {
protected ICoffeeAdditives additives;
public Coffee(ICoffeeAdditives additives){
this.additives=additives;
}
public abstract void orderCoffee(int count);
}
我们可以看到,Coffee持有了ICoffeeAdditives 引用,ICoffeeAdditives 的实例是通过构造函数注入的,这个过程就是我们所说的桥接过程。我们通过这个引用就可以调用ICoffeeAdditives的方法,进而将Coffee的行为与ICoffeeAdditives的行为通过orderCoffee()方法而组合起来。
下面是一个对抽象化修正的一个类,里面增加了一个品控的方法
//RefinedAbstraction
public abstract class RefinedCoffee extends Coffee {
public RefinedCoffee(ICoffeeAdditives additives) {
super(additives);
}
public void checkQuality(){
Random ran=new Random();
System.out.println(String.format("%s 添加%s",additives.getClass().getSimpleName(),ran.nextBoolean()?"太多":"正常"));
}
}
第二步:创建实现化部分
public interface ICoffeeAdditives {
void addSomething();
}
//加奶
public class Milk implements ICoffeeAdditives {
@Override
public void addSomething() {
System.out.println("加奶");
}
}
//加糖
public class Sugar implements ICoffeeAdditives {
@Override
public void addSomething() {
System.out.println("加糖");
}
}
第三步:客户端调用
public static void main(String[] args) {
//点两杯加奶的大杯咖啡
RefinedCoffee largeWithMilk=new LargeCoffee(new Milk());
largeWithMilk.orderCoffee(2);
largeWithMilk.checkQuality();
}
输出结果:
加奶
大杯咖啡2杯
Milk 添加太多
三、组合模式(部分整体模式。整体与部分的结构,组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件)
事实上组合模式和桥接模式的组合完全不一样。组合模式用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:
- 文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
- 总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
- 树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。
在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。
- 组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类
例子
组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类:
public abstract class Component {
// 职位
private String position;
// 工作内容
private String job;
public Component(String position, String job) {
this.position = position;
this.job = job;
}
// 做自己的本职工作
public void work() {
System.out.println("我是" + position + ",我正在" + job);
}
abstract void addComponent(Component component);
abstract void removeComponent(Component component);
abstract void check();
}
管理者继承自此抽象类:
Java
public class Manager extends Component {
// 管理的组件
private List<Component> components = new ArrayList<>();
public Manager(String position, String job) {
super(position, job);
}
@Override
public void addComponent(Component component) {
components.add(component);
}
@Override
void removeComponent(Component component) {
components.remove(component);
}
// 检查下属
@Override
public void check() {
work();
for (Component component : components) {
component.check();
}
}
}
职员同样继承自此抽象类:
public class Employee extends Component {
public Employee(String position, String job) {
super(position, job);
}
@Override
void addComponent(Component component) {
System.out.println("职员没有管理权限");
}
@Override
void removeComponent(Component component) {
System.out.println("职员没有管理权限");
}
@Override
void check() {
work();
}
}
修改客户端如下:
public class Client {
@Test
public void test(){
Component boss = new Manager("老板", "唱怒放的生命");
Component HR = new Employee("人力资源", "聊微信");
Component PM = new Manager("产品经理", "不知道干啥");
Component CFO = new Manager("财务主管", "看剧");
Component CTO = new Manager("技术主管", "划水");
Component UI = new Employee("设计师", "画画");
Component operator = new Employee("运营人员", "兼职客服");
Component webProgrammer = new Employee("程序员", "学习设计模式");
Component backgroundProgrammer = new Employee("后台程序员", "CRUD");
Component accountant = new Employee("会计", "背九九乘法表");
Component clerk = new Employee("文员", "给老板递麦克风");
boss.addComponent(HR);
boss.addComponent(PM);
boss.addComponent(CFO);
PM.addComponent(UI);
PM.addComponent(CTO);
PM.addComponent(operator);
CTO.addComponent(webProgrammer);
CTO.addComponent(backgroundProgrammer);
CFO.addComponent(accountant);
CFO.addComponent(clerk);
boss.check();
}
}
安全方式和透明方式
读者可能已经注意到了,Employee 类虽然继承了父类的 addComponent 和 removeComponent 方法,但是仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。这样是否违背了接口隔离原则呢?
- 接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
答案是肯定的,这样确实违背了接口隔离原则。这种方式在组合模式中被称作透明方式.
- 透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。
安全模式例子
将抽象类修改为:
public abstract class Component {
// 职位
private String position;
// 工作内容
private String job;
public Component(String position, String job) {
this.position = position;
this.job = job;
}
// 做自己的本职工作
public void work() {
System.out.println("我是" + position + ",我正在" + job);
}
abstract void check();
}
可以看到,我们在父类中去掉了 addComponent 和 removeComponent 这两个抽象方法。
Manager 类修改为:
public class Manager extends Component {
// 管理的组件
private List<Component> components = new ArrayList<>();
public Manager(String position, String job) {
super(position, job);
}
public void addComponent(Component component) {
components.add(component);
}
void removeComponent(Component component) {
components.remove(component);
}
// 检查下属
@Override
public void check() {
work();
for (Component component : components) {
component.check();
}
}
}
Manager 类单独实现了 addComponent 和 removeComponent 这两个方法,去掉了 @Override 注解。
Employee 类修改为:
public class Employee extends Component {
public Employee(String position, String job) {
super(position, job);
}
@Override
void check() {
work();
}
}
客户端建立人员结构关系:
public class Client {
@Test
public void test(){
Manager boss = new Manager("老板", "唱怒放的生命");
Employee HR = new Employee("人力资源", "聊微信");
Manager PM = new Manager("产品经理", "不知道干啥");
Manager CFO = new Manager("财务主管", "看剧");
Manager CTO = new Manager("技术主管", "划水");
Employee UI = new Employee("设计师", "画画");
Employee operator = new Employee("运营人员", "兼职客服");
Employee webProgrammer = new Employee("程序员", "学习设计模式");
Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");
Employee accountant = new Employee("会计", "背九九乘法表");
Employee clerk = new Employee("文员", "给老板递麦克风");
boss.addComponent(HR);
boss.addComponent(PM);
boss.addComponent(CFO);
PM.addComponent(UI);
PM.addComponent(CTO);
PM.addComponent(operator);
CTO.addComponent(webProgrammer);
CTO.addComponent(backgroundProgrammer);
CFO.addComponent(accountant);
CFO.addComponent(clerk);
boss.check();
}
}
组合模式中的安全方式与透明方式有什么区别?
- 答案:透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。
四、装饰者模式
装饰模式的主要作用就是:
- 增强一个类原有的功能
- 为一个类添加新的功能
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
用于增强功能的装饰模式
我们用程序来模拟一下戴上装饰品提高我们颜值的过程:
新建颜值接口:
public interface IBeauty {
int getBeautyValue();
}
新建 Me 类,实现颜值接口:
public class Me implements IBeauty {
@Override
public int getBeautyValue() {
return 100;
}
}
戒指装饰类,将 Me 包装起来:
public class RingDecorator implements IBeauty {
private final IBeauty me;
public RingDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 20;
}
}
客户端测试:
Java
public class Client {
@Test
public void show() {
IBeauty me = new Me();
System.out.println("我原本的颜值:" + me.getBeautyValue());
IBeauty meWithRing = new RingDecorator(me);
System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue());
}
}
运行程序,输出如下:
我原本的颜值:100
戴上了戒指后,我的颜值:120
这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:
耳环装饰类:
public class EarringDecorator implements IBeauty {
private final IBeauty me;
public EarringDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 50;
}
}
项链装饰类:
public class NecklaceDecorator implements IBeauty {
private final IBeauty me;
public NecklaceDecorator(IBeauty me) {
this.me = me;
}
@Override
public int getBeautyValue() {
return me.getBeautyValue() + 80;
}
}
客户端测试:
public class Client {
@Test
public void show() {
IBeauty me = new Me();
System.out.println("我原本的颜值:" + me.getBeautyValue());
// 随意挑选装饰
IBeauty meWithNecklace = new NecklaceDecorator(me);
System.out.println("戴上了项链后,我的颜值:" + meWithNecklace.getBeautyValue());
// 多次装饰
IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));
System.out.println("戴上耳环、戒指、项链后,我的颜值:" + meWithManyDecorators.getBeautyValue());
// 任意搭配装饰
IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new RingDecorator(me));
System.out.println("戴上戒指、项链后,我的颜值:" + meWithNecklaceAndRing.getBeautyValue());
}
}
可以看到,都是实现了IBeauty接口,装饰器也实现了 IBeauty 接口,并且没有添加新的方法,也就是说这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能
用于添加功能的装饰模式
我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:
新建房屋接口:
public interface IHouse {
void live();
}
房屋类:
public class House implements IHouse{
@Override
public void live() {
System.out.println("房屋原有的功能:居住功能");
}
}
新建粘钩装饰器接口,继承自房屋接口:
public interface IStickyHookHouse extends IHouse{
void hangThings();
}
粘钩装饰类:
public class StickyHookDecorator implements IStickyHookHouse {
private final IHouse house;
public StickyHookDecorator(IHouse house) {
this.house = house;
}
@Override
public void live() {
house.live();
}
@Override
public void hangThings() {
System.out.println("有了粘钩后,新增了挂东西功能");
}
}
客户端测试:
public class Client {
@Test
public void show() {
IHouse house = new House();
house.live();
IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house);
stickyHookHouse.live();
stickyHookHouse.hangThings();
}
}
五、外观模式(外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。)
外观模式说白点就是封装的思想,就是暴露简洁的接口,隐藏内部的实现。
新建浏览器类:
public class Browser {
public static void open() {
System.out.println("打开浏览器");
}
public static void close() {
System.out.println("关闭浏览器");
}
}
新建 IDE 类:
public class IDE {
public static void open() {
System.out.println("打开 IDE");
}
public static void close() {
System.out.println("关闭 IDE");
}
}
新建微信类:
public class Wechat {
public static void open() {
System.out.println("打开微信");
}
public static void close() {
System.out.println("关闭微信");
}
}
客户端调用:
public class Client {
@Test
public void test() {
System.out.println("上班:");
Browser.open();
IDE.open();
Wechat.open();
System.out.println("下班:");
Browser.close();
IDE.close();
Wechat.close();
}
}
运行程序,输出如下:
上班:
打开浏览器
打开 IDE
打开微信
下班:
关闭浏览器
关闭 IDE
关闭微信
由于我们每天都要做这几件事,所以我们可以使用外观模式,将这几个子系统封装起来,提供更简洁的接口:
public class Facade {
public void open() {
Browser.open();
IDE.open();
Wechat.open();
}
public void close() {
Browser.close();
IDE.close();
Wechat.close();
}
}
客户端就可以简化代码,只和这个外观类打交道:
public class Client {
@Test
public void test() {
Facade facade = new Facade();
System.out.println("上班:");
facade.open();
System.out.println("下班:");
facade.close();
}
}
六、享元模式(体现的是程序可复用的特点, 简单来说享元模式就是共享对象,提高复用性)
何时使用:
- 1、系统中有大量对象。
- 2、这些对象消耗大量内存。
- 3、这些对象的状态大部分可以外部化。
- 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
- 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
比如用HashMap存储对象,和单例的区别是单例维护一个对象,这个维护很多个。
八、代理模式(给某一个对象提供一个代理,并由代理对象控制对原对象的引用)
静态代理-例子
人类的接口
public interface IPerson {
void eat();
void sleep();
}
人类:
public class Person implements IPerson{
@Override
public void eat() {
System.out.println("我在吃饭");
}
@Override
public void sleep() {
System.out.println("我在睡觉");
}
}
客户端测试:
public class Client {
@Test
public void test() {
Person person = new Person();
person.eat();
person.sleep();
}
}
运行程序,输出如下:
我在吃饭
我在睡觉
我们可以把这个类包装到另一个类中,实现完全一样的行为:
public class PersonProxy implements IPerson {
private final Person person;
public PersonProxy(Person person) {
this.person = person;
}
@Override
public void eat() {
person.eat();
}
@Override
public void sleep() {
person.sleep();
}
}
将客户端修改为调用这个新的类:
public class Client {
@Test
public void test() {
Person person = new Person();
PersonProxy proxy = new PersonProxy(person);
proxy.eat();
proxy.sleep();
}
}
动态代理-例子(使用动态代理,需要把一个类传入,然后根据它正在调用的方法名判断是否需要加以控制)
使用动态代理,需要把一个类传入,然后根据它正在调用的方法名判断是否需要加以控制。
public class HttpProxy {
private final HttpUtil httpUtil;
public HttpProxy(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
}
// 假设调用 httpUtil 的任意方法时,都要通过这个方法间接调用, methodName 表示方法名,args 表示方法中传入的参数
public visit(String methodName, Object[] args) {
if (methodName.equals("request")) {
// 如果方法名是 request,打印日志,并调用 request 方法,args 的第一个值就是传入的参数
System.out.println("发送数据:" + args[0]);
httpUtil.request(args[0].toString());
} else if (methodName.equals("onSuccess")) {
// 如果方法名是 onSuccess,打印日志,并调用 onSuccess 方法,args 的第一个值就是传入的参数
System.out.println("收到数据:" + args[0]);
httpUtil.onSuccess(args[0].toString());
}
}
}
伪代码看起来还是很简单的,实现起来唯一的难点就是怎么让 httpUtil 调用任意方法时,都通过一个方法间接调用。这里需要用到反射技术,不了解反射技术也没有关系,不妨把它记做固定的写法。实际的动态代理类代码如下:
public class HttpProxy implements InvocationHandler {
private HttpUtil httpUtil;
public IHttp getInstance(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
return (IHttp) Proxy.newProxyInstance(httpUtil.getClass().getClassLoader(), httpUtil.getClass().getInterfaces(), this);
}
// 调用 httpUtil 的任意方法时,都要通过这个方法调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
if (method.getName().equals("request")) {
// 如果方法名是 request,打印日志,并调用 request 方法
System.out.println("发送数据:" + args[0]);
result = method.invoke(httpUtil, args);
} else if (method.getName().equals("onSuccess")) {
// 如果方法名是 onSuccess,打印日志,并调用 onSuccess 方法
System.out.println("收到数据:" + args[0]);
result = method.invoke(httpUtil, args);
}
return result;
}
}
总结
- 适配器模式:用于有相关性但不兼容的接口
- 桥接模式:用于同等级的接口互相组合
- 组合模式:用于整体与部分的结构
- 外观模式:体现封装的思想
- 享元模式:体现面向对象的可复用性
- 代理模式:主要用于对某个对象加以控制