zoukankan      html  css  js  c++  java
  • java基础-设计模式(创建型模式)

    这部分内容的脑图:链接

    image-20200508190605436

    简介

    设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。

    为什么要使用设计模式?根本原因还是软件开发要实现可维护、可扩展,就必须尽量复用代码,并且降低代码的耦合度。设计模式主要是基于OOP编程提炼的,它基于以下几个原则:

    开闭原则

    由Bertrand Meyer提出的开闭原则(Open Closed Principle)是指,软件应该对扩展开放,而对修改关闭。
    这里的意思是在增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。

    里氏替换原则

    里氏替换原则是Barbara Liskov提出的,这是一种面向对象的设计原则,即如果我们调用一个父类的方法可以成功,那么替换成子类调用也应该完全可以运行。

    23个常用模式分为创建型模式、结构型模式和行为型模式三类

    1. 创建型模式

    创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。

    创建型模式包括:

    • 工厂方法:Factory Method
    • 抽象工厂:Abstract Factory
    • 建造者:Builder
    • 原型:Prototype
    • 单例:Singleton

    1.1 工厂方法

    Factory Method

    基本方式

    一般的工厂方法的创建使用方式

    编写工厂接口

    //编写工厂接口
    public interface NumberFactory{
        //创建方法(工厂中的创建方法)
        Number parse(String s);
    
        //创建静态的接口类,创建真实的子类
        static NumberFactory impl = new NumberFactoryImp();
    
        //有了工厂接口的实现类,再添加静态方法返回真正的子类
        static NumberFactoryImp getFactory(){
            return (NumberFactoryImp) impl;
        }
    }
    

    编写工厂接口的实现类(真正的工厂)

    //有了工厂接口后,编写工厂接口的实现类
    //这个实现类,才是真正的工厂
    //通过这样两层的嵌套,调用方只需要调用接口而可以完全忽略真正的工厂
    //同样,可以忽略真正的产品类型 BigDecimal,而只用和抽象产品Number打交道
    public static class NumberFactoryImp implements NumberFactory{
        //实现接口声明的方法
        public Number parse(String s){
            return new BigDecimal(s);
        }
    }
    

    使用

    NumberFactory factory = NumberFactory.getFactory();
    Number result = factory.parse("123.456");
    

    好处

    • 只需要和工厂接口 NumberFactory和抽象产品 Number打交道,
      不关心真正的工厂NumberFactoryImpl和实际的产品BigDecimal
    • 允许创建产品的代码独立地变换,而不会影响到调用方

    静态工厂方法

    实际上大多数情况下我们并不需要抽象工厂(即上面的工厂接口),而是通过静态方法直接返回产品,即:

    public class NumberFactory {
        public static Number parse(String s) {
            return new BigDecimal(s);
        }
    }
    

    这种简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。
    静态工厂方法广泛地应用在Java标准库中。例如:

    Integer n = Integer.valueOf(100);
    

    Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer

    .valueOf() 对比 new Integer()

    观察valueOf()方法:

    public final class Integer {
        public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }
        ...
    }
    

    valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。

    工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。

    如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。

    小结

    • 工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂与抽象产品打交道。
    • 实际更常用的是更简单的静态工厂方法,它允许工厂内部对创建产品进行优化。
    • 调用方尽量持有接口或抽象类,避免持有具体类型的子类,以便工厂方法能随时切换不同的子类返回,却不影响调用方代码。

    1.2 抽象工厂

    提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

    抽象工厂模式(Abstract Factory)是一个比较复杂的创建型模式。

    抽象工厂模式和工厂方法不太一样,它要解决的问题比较复杂,不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:

    image-20200507230533053

    这种模式有点类似于多个供应商负责提供一系列类型的产品。

    例子:Markdown转HTML和Word

    为用户提供一个Markdown文本转换为HTML和Word的服务,它的接口定义如下:

    需求方

    抽象工厂接口:

    public interface AbstractFactory {
        // 创建Html文档:
        HtmlDocument createHtml(String md);
        // 创建Word文档:
        WordDocument createWord(String md);
    }
    

    上述就得到了

    抽象工厂(接口)

    • AbstractFactory

    抽象产品(接口)

    • HtmlDocument
    • WordDocument

    因为HtmlDocumentWordDocument都比较复杂,现在我们并不知道如何实现它们,所以只有接口,但是需要在接口内部描述清楚,接口能实现的方法和功能,并通过:

    // Html文档接口:
    public interface HtmlDocument {
        String toHtml();
        void save(Path path) throws IOException;
    }
    
    // Word文档接口:
    public interface WordDocument {
        void save(Path path) throws IOException;
    }
    

    供应商:实现产品

    市场上有两家供应商可以提供上述抽象产品

    FastDoc Soft

    • FastHtmlDocument
    • FastWordDocument

    GoodDoc Soft

    • GoodHtmlDocument
    • GoodWordDocument

    第一家公司的产品(实现需求方给出的抽象产品,是一个实际的实现类)

    public class FastHtmlDocument implements HtmlDocument {
        public String toHtml() {
            ...
        }
        public void save(Path path) throws IOException {
            ...
        }
    }
    
    public class FastWordDocument implements WordDocument {
        public void save(Path path) throws IOException {
            ...
        }
    }
    

    FastDoc Soft必须提供一个实际的工厂来生产这两种产品,即FastFactory

    也就是implement抽象工厂接口,最后可以通过抽象产品接口调用这个类

    public class FastFactory implements AbstractFactory {
        public HtmlDocument createHtml(String md) {
            return new FastHtmlDocument(md);
        }
        public WordDocument createWord(String md) {
            return new FastWordDocument(md);
        }
    }
    

    第二家公司的产品(实现需求方给出的抽象产品,是一个实际的实现类)

    // 实际工厂:
    public class GoodFactory implements AbstractFactory {
        public HtmlDocument createHtml(String md) {
            return new GoodHtmlDocument(md);
        }
        public WordDocument createWord(String md) {
            return new GoodWordDocument(md);
        }
    }
    
    // 实际产品:
    public class GoodHtmlDocument implements HtmlDocument {
        ...
    }
    
    public class GoodWordDocument implements HtmlDocument {
        ...
    }
    

    使用供应商提供的产品

    客户端编写代码如下:(FastDoc公司)

    // 创建AbstractFactory,实际类型是FastFactory:
    AbstractFactory factory = new FastFactory();
    // 生成Html文档:
    HtmlDocument html = factory.createHtml("#Hello
    Hello, world!");
    html.save(Paths.get(".", "fast.html"));
    // 生成Word文档:
    WordDocument word = fastFactory.createWord("#Hello
    Hello, world!");
    word.save(Paths.get(".", "fast.doc"));
    

    客户端编写代码如下:(GoodDoc公司)

    只需要将第一行改动

    AbstractFactory factory = new GoodFactory();
    

    屏蔽实际工厂

    将创建工厂的代码,放在抽象工厂接口中(用接口的静态方法实现),就可以屏蔽实际工厂

    在创建工厂的时候指定fast或者good即可

    public interface AbstractFactory {
        public static AbstractFactory createFactory(String name) {
            if (name.equalsIgnoreCase("fast")) {
                return new FastFactory();
            } else if (name.equalsIgnoreCase("good")) {
                return new GoodFactory();
            } else {
                throw new IllegalArgumentException("Invalid factory name");
            }
        }
    }
    

    小结

    • 抽象工厂模式是为了让创建工厂和一组产品与使用相分离,并可以随时切换到另一个工厂以及另一组产品;
    • 抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道。

    1.3 生成器

    Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。

    生成器模式(Builder)是使用多个“小型”工厂来最终创建出一个完整对象。

    当我们使用Builder的时候,一般来说,是因为创建这个对象的步骤比较多,每个步骤都需要一个零部件,最终组合成一个完整的对象。

    例子:我们把Markdown转HTML看作一行一行的转换,每一行根据语法,使用不同的转换器:

    • 如果以#开头,使用HeadingBuilder转换;
    • 如果以>开头,使用QuoteBuilder转换;
    • 如果以---开头,使用HrBuilder转换;
    • 其余使用ParagraphBuilder转换。

    定义Builder

    这个HtmlBuilder写出来如下:

    public class HtmlBuilder {
        private HeadingBuilder headingBuilder = new HeadingBuilder();
        private HrBuilder hrBuilder = new HrBuilder();
        private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
        private QuoteBuilder quoteBuilder = new QuoteBuilder();
    
        public String toHtml(String markdown) {
            StringBuilder buffer = new StringBuilder();
            markdown.lines().forEach(line -> {
                if (line.startsWith("#")) {
                    buffer.append(headingBuilder.buildHeading(line)).append('
    ');
                } else if (line.startsWith(">")) {
                    buffer.append(quoteBuilder.buildQuote(line)).append('
    ');
                } else if (line.startsWith("---")) {
                    buffer.append(hrBuilder.buildHr(line)).append('
    ');
                } else {
                    buffer.append(paragraphBuilder.buildParagraph(line)).append('
    ');
                }
            });
            return buffer.toString();
        }
    }
    

    注意观察上述代码,HtmlBuilder并不是一次性把整个Markdown转换为HTML,而是一行一行转换,并且,它自己并不会将某一行转换为特定的HTML,而是根据特性把每一行都“委托”给一个XxxBuilder去转换,最后,把所有转换的结果组合起来,返回给客户端。

    这样一来,我们只需要针对每一种类型编写不同的Builder。例如,针对以#开头的行,需要HeadingBuilder

    public class HeadingBuilder {
        public String buildHeading(String line) {
            int n = 0;
            while (line.charAt(0) == '#') {
                n++;
                line = line.substring(1);
            }
            return String.format("<h%d>%s</h%d>", n, line.strip(), n);
        }
    }
    

    简化Builder模式(链式调用)

    简化Builder模式,以链式调用的方式来创建对象。例如,我们经常编写这样的代码:

    StringBuilder builder = new StringBuilder();
    builder.append(secure ? "https://" : "http://")
           .append("www.liaoxuefeng.com")
           .append("/")
           .append("?t=0");
    String url = builder.toString();
    

    由于我们经常需要构造URL字符串,可以使用Builder模式编写一个URLBuilder,调用方式如下:

    String url = URLBuilder.builder() // 创建Builder
            .setDomain("www.liaoxuefeng.com") // 设置domain
            .setScheme("https") // 设置scheme
            .setPath("/") // 设置路径
            .setQuery(Map.of("a", "123", "q", "K&R")) // 设置query
            .build(); // 完成build
    

    上面代码中使用的类似 .setDomain(),都是在URLBuilder这个类的定义中,将这些方法定义好了

    小结

    • Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,
    • 或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。

    1.4 原型模式

    原型模式,即Prototype,是指创建新对象的时候,根据现有的一个原型来创建。

    对于普通类,我们如何实现原型拷贝?Java的Object提供了一个clone()方法,它的意图就是复制一个新的对象出来,我们需要实现一个Cloneable接口来标识一个对象是“可复制”的:

    public class Student implements Cloneable {
        private int id;
        private String name;
        private int score;
    
        // 复制新对象并返回:
        public Object clone() {
            Student std = new Student();
            std.id = this.id;
            std.name = this.name;
            std.score = this.score;
            return std;
        }
    }
    

    使用的时候,因为clone()的方法签名是定义在Object中,返回类型也是Object,所以要强制转型,比较麻烦:

    Student std1 = new Student();
    std1.setId(123);
    std1.setName("Bob");
    std1.setScore(88);
    // 复制新对象:
    Student std2 = (Student) std1.clone();
    System.out.println(std1);
    System.out.println(std2);
    System.out.println(std1 == std2); // false
    

    实际上,使用原型模式更好的方式是定义一个copy()方法,返回明确的类型:

    public class Student {
        private int id;
        private String name;
        private int score;
    
        public Student copy() {
            Student std = new Student();
            std.id = this.id;
            std.name = this.name;
            std.score = this.score;
            return std;
        }
    }
    

    原型模式应用不是很广泛,因为很多实例会持有类似文件、Socket这样的资源,而这些资源是无法复制给另一个对象共享的,只有存储简单类型的“值”对象可以复制。

    1.5 单例模式

    保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。

    基础实现方式

    因为这个类只有一个实例,因此不能让调用方使用new Xyz()来创建实例了。
    所以,单例的构造方法必须是private,这样就防止了调用方自己创建实例,但是在类的内部,是可以用一个静态字段来引用唯一创建的实例的:

    实例:

    public class Singleton {
        // 静态字段引用唯一实例:
        private static final Singleton INSTANCE = new Singleton();
    
        // 通过静态方法返回实例:
        public static Singleton getInstance() {
            return INSTANCE;
        }
    
        // private构造方法保证外部无法实例化:
        private Singleton() {
        }
    }
    

    或者直接把static变量暴露给外部:

    public class Singleton {
        // 静态字段引用唯一实例:
        public static final Singleton INSTANCE = new Singleton();
    
        // private构造方法保证外部无法实例化:
        private Singleton() {
        }
    }
    

    单例模式的实现方式很简单:

    1. 只有private构造方法,确保外部无法实例化;
    2. 通过private static变量持有唯一实例,保证全局唯一性;
    3. 通过public static方法返回此唯一实例,使外部调用方能获取到实例。

    利用enum实现

    另一种实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

    public enum World {
        // 唯一枚举:
    	INSTANCE;
    
    	private String name = "world";
    
    	public String getName() {
    		return this.name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    }
    

    枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:

    String name = World.INSTANCE.getName();
    

    使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

    采用"约定"实现

    即采用约定的方式,把普通类视作单例

    那我们什么时候应该用Singleton呢?实际上,很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new操作符:

    @Component // 表示一个单例组件
    public class MyService {
        ...
    }
    

    因此,除非确有必要,否则Singleton模式一般以“约定”为主,不会刻意实现它。

  • 相关阅读:
    AES加密
    Axis创建webservice客户端和服务端
    RandomAccessFile操作文件
    使用HttpClient实现文件的上传下载
    System.getProperty()方法可以获取的值
    C#面向对象 基础概念25个
    C#面向对象基础
    JQuery————基础&&基础选择器
    css3实现图片遮罩效果鼠标hover以后出现文字
    JAVASCRIPT——图片滑动效果
  • 原文地址:https://www.cnblogs.com/tangg/p/12852524.html
Copyright © 2011-2022 走看看