常用设计模式

一、单例模式

1、饿汉式

  • 饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到 IdGenerator 的时候,再创建实例)

  • 如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

    public class IdGenerator { 
        private AtomicLong id = new AtomicLong(0);
        private static final IdGenerator instance = new IdGenerator();
        private IdGenerator() {}
        public static IdGenerator getInstance() {
            return instance;
        }
        public long getId() { 
            return id.incrementAndGet();
        }
    }
    

2、懒汉式

  • 懒汉式相对于饿汉式的优势是支持延迟加载

    public class IdGenerator { 
        private AtomicLong id = new AtomicLong(0);
        private static IdGenerator instance;
        private IdGenerator() {}
        public static synchronized IdGenerator getInstance() {
            if (instance == null) {  // instance 是 null 的时候才去创建这个对象
                instance = new IdGenerator();
            }
            return instance;
        }
        public long getId() { 
            return id.incrementAndGet();
        }
    }
    
  • 我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低

  • 如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了

3、双重检测

  • 既支持延迟加载、又支持高并发的单例实现方式

    public class IdGenerator { 
        private AtomicLong id = new AtomicLong(0);
        private static volatile IdGenerator instance; // 注意有 volatile,多个线程可见
        private IdGenerator() {}
        public static IdGenerator getInstance() {
            if (instance == null) {
                synchronized(IdGenerator.class) { // 此处为类级别的锁
                    if (instance == null) {
                        instance = new IdGenerator();
                    }
                }
            }
            return instance;
        }
        public long getId() { 
            return id.incrementAndGet();
        }
    }
    

4、静态内部类

  • 做到了延迟加载

    public class IdGenerator { 
        private AtomicLong id = new AtomicLong(0);
        private IdGenerator() {}
    
        private static class SingletonHolder{
            private static final IdGenerator instance = new IdGenerator();
        }
    
        public static IdGenerator getInstance() {
            return SingletonHolder.instance;
        }
    
        public long getId() { 
            return id.incrementAndGet();
        }
    }
    
  • SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

5、枚举

  • 这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性

    public enum IdGenerator {
        INSTANCE;
        private AtomicLong id = new AtomicLong(0);
    
        public long getId() { 
            return id.incrementAndGet();
        }
    }
    

二、工厂模式

1、简单工厂模式

  • 和名字一样简单,非常简单,直接上代码吧:

    public class FoodFactory {
        public static Food makeFood(String name) {
            if (name.equals("noodle")) {
                Food noodle = new LanZhouNoodle();
                noodle.addSpicy("more");
                return noodle;
            } else if (name.equals("chicken")) {
                Food chicken = new HuangMenChicken();
                chicken.addCondiment("potato");
                return chicken;
            } else {
                return null;
            }
        }
    }
    

    其中,LanZhouNoodle 和 HuangMenChicken 都继承自 Food。

    简单地说,简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。

    我们强调职责单一原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。

2、工厂方法

  • 工厂方法模式:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类

  • 之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。

    public interface FoodFactory {
        Food makeFood(String name);
    }
    public class ChineseFoodFactory implements FoodFactory {
    
        @Override
        public Food makeFood(String name) {
            if (name.equals("A")) {
                return new ChineseFoodA();
            } else if (name.equals("B")) {
                return new ChineseFoodB();
            } else {
                return null;
            }
        }
    }
    public class AmericanFoodFactory implements FoodFactory {
    
        @Override
        public Food makeFood(String name) {
            if (name.equals("A")) {
                return new AmericanFoodA();
            } else if (name.equals("B")) {
                return new AmericanFoodB();
            } else {
                return null;
            }
        }
    }
    

    其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。

    客户端调用:

    public class APP {
        public static void main(String[] args) {
            // 先选择一个具体的工厂
            FoodFactory factory = new ChineseFoodFactory();
            // 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
            Food food = factory.makeFood("A");
        }
    }
    

    虽然都是调用 makeFood("A") 制作 A 类食物,但是,不同的工厂生产出来的完全不一样。

    第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。

    核心在于,我们需要在第一步选好我们需要的工厂。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。

3、抽象工厂

  • 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

  • 当涉及到产品族的时候,就需要引入抽象工厂模式了。

    一个经典的例子是造一台电脑。我们先不引入抽象工厂模式,看看怎么实现。

    因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:

    image-20220327113329364

    这个时候的客户端调用是这样的:

    // 得到 Intel 的 CPU
    CPUFactory cpuFactory = new IntelCPUFactory();
    CPU cpu = intelCPUFactory.makeCPU();
    
    // 得到 AMD 的主板
    MainBoardFactory mainBoardFactory = new AmdMainBoardFactory();
    MainBoard mainBoard = mainBoardFactory.make();
    
    // 组装 CPU 和主板
    Computer computer = new Computer(cpu, mainBoard);
    

    单独看 CPU 工厂和主板工厂,它们分别是前面我们说的工厂模式。这种方式也容易扩展,因为要给电脑加硬盘的话,只需要加一个 HardDiskFactory 和相应的实现即可,不需要修改现有的工厂。

    但是,这种方式有一个问题,那就是如果 Intel 家产的 CPU 和 AMD 产的主板不能兼容使用,那么这代码就容易出错,因为客户端并不知道它们不兼容,也就会错误地出现随意组合。

    下面就是我们要说的产品族的概念,它代表了组成某个产品的一系列附件的集合:

    image-20220327113339104

    当涉及到这种产品族的问题的时候,就需要抽象工厂模式来支持了。我们不再定义 CPU 工厂、主板工厂、硬盘工厂、显示屏工厂等等,我们直接定义电脑工厂,每个电脑工厂负责生产所有的设备,这样能保证肯定不存在兼容问题。

    image-20220327113346431

    这个时候,对于客户端来说,不再需要单独挑选 CPU厂商、主板厂商、硬盘厂商等,直接选择一家品牌工厂,品牌工厂会负责生产所有的东西,而且能保证肯定是兼容可用的。

    public static void main(String[] args) {
        // 第一步就要选定一个“大厂”
        ComputerFactory cf = new AmdFactory();
        // 从这个大厂造 CPU
        CPU cpu = cf.makeCPU();
        // 从这个大厂造主板
        MainBoard board = cf.makeMainBoard();
          // 从这个大厂造硬盘
          HardDisk hardDisk = cf.makeHardDisk();
    
        // 将同一个厂子出来的 CPU、主板、硬盘组装在一起
        Computer result = new Computer(cpu, board, hardDisk);
    }
    

    当然,抽象工厂的问题也是显而易见的,比如我们要加个显示器,就需要修改所有的工厂,给所有的工厂都加上制造显示器的方法。这有点违反了对修改关闭,对扩展开放这个设计原则。

三、原型模式

  • 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

  • 原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。如果要拷贝的对象是不可变对象,浅拷贝共享不可变对象是没问题的,但对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的风险,也就变得复杂多了。,在操作非常耗时的情况下推荐使用浅拷贝,否则,没有充分的理由,不要为了一点点的性能提升而使用浅拷贝。

    java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。

四、代理模式

  • 什么是代理模式:它在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

  • 代理模式得实现:

    • 静态代理
      • 实现被代理对象口: 要求被代理类和代理类同时实现相应的一套接口,通过代理类调用重写接口的方法,实际上调用的是原始对象的同样的方法。
      • 继承被代理对象:代理类继承原始类,然后扩展附加功能。
    • 动态代理 : 在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
      • jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
      • cglib动态代理是利用asm开源包,对被代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
  • 代理模式是用一个代理来隐藏具体实现类的实现细节,通常还用于在真实的实现的前后添加一部分逻辑。

  • 既然说是代理,那就要对客户端隐藏真实实现,由代理来负责客户端的所有请求。当然,代理只是个代理,它不会完成实际的业务逻辑,而是一层皮而已,但是对于客户端来说,它必须表现得就是客户端需要的真实实现。

    public interface FoodService {
        Food makeChicken();
        Food makeNoodle();
    }
    
    public class FoodServiceImpl implements FoodService {
        public Food makeChicken() {
            Food f = new Chicken()
            f.setChicken("1kg");
            f.setSpicy("1g");
            f.setSalt("3g");
            return f;
        }
        public Food makeNoodle() {
            Food f = new Noodle();
            f.setNoodle("500g");
            f.setSalt("5g");
            return f;
        }
    }
    
    // 代理要表现得“就像是”真实实现类,所以需要实现 FoodService
    public class FoodServiceProxy implements FoodService {
    
        // 内部一定要有一个真实的实现类,当然也可以通过构造方法注入
        private FoodService foodService = new FoodServiceImpl();
    
        public Food makeChicken() {
            System.out.println("我们马上要开始制作鸡肉了");
    
            // 如果我们定义这句为核心代码的话,那么,核心代码是真实实现类做的,
            // 代理只是在核心代码前后做些“无足轻重”的事情
            Food food = foodService.makeChicken();
    
            System.out.println("鸡肉制作完成啦,加点胡椒粉"); // 增强
            food.addCondiment("pepper");
    
            return food;
        }
        public Food makeNoodle() {
            System.out.println("准备制作拉面~");
            Food food = foodService.makeNoodle();
            System.out.println("制作完成啦")
    		return food;
        }
    }
    

    客户端调用,注意,我们要用代理来实例化接口:

    // 这里用代理类来实例化
    FoodService foodService = new FoodServiceProxy();
    foodService.makeChicken();
    
  • image-20220327113358443
  • 我们发现没有,代理模式说白了就是做 “方法包装” 或做 “方法增强”。在面向切面编程中,其实就是动态代理的过程。比如 Spring 中,我们自己不定义代理类,但是 Spring 会帮我们动态来定义代理,然后把我们定义在 @Before、@After、@Around 中的代码逻辑动态添加到代理中。

    说到动态代理,又可以展开说,Spring 中实现动态代理有两种,一种是如果我们的类定义了接口,如 UserService 接口和 UserServiceImpl 实现,那么采用 JDK 的动态代理,感兴趣的读者可以去看看 java.lang.reflect.Proxy 类的源码;另一种是我们自己没有定义接口的,Spring 会采用 CGLIB 进行动态代理,它是一个 jar 包,性能还不错。

五、模板方法模式

  • 模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

  • 在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

  • 模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能

    public abstract class AbstractTemplate {
        // 这就是模板方法
        public void templateMethod() {
            init();
            apply(); // 这个是重点
            end(); // 可以作为钩子方法
        }
    
        protected void init() {
            System.out.println("init 抽象层已经实现,子类也可以选择覆写");
        }
    
        // 留给子类实现
        protected abstract void apply();
    
        protected void end() {
        }
    }
    

    模板方法中调用了 3 个方法,其中 apply() 是抽象方法,子类必须实现它,其实模板方法中有几个抽象方法完全是自由的,我们也可以将三个方法都设置为抽象方法,让子类来实现。也就是说,模板方法只负责定义第一步应该要做什么,第二步应该做什么,第三步应该做什么,至于怎么做,由子类来实现。

    我们写一个实现类:

    public class ConcreteTemplate extends AbstractTemplate {
        public void apply() {
            System.out.println("子类实现抽象方法 apply");
        }
    
        public void end() {
            System.out.println("我们可以把 method3 当做钩子方法来使用,需要的时候覆写就可以了");
        }
    }
    

    客户端调用演示:

    public static void main(String[] args) {
        AbstractTemplate t = new ConcreteTemplate();
        // 调用模板方法
        t.templateMethod();
    }
    

六、MVC 模式

  • MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

    • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
    • View(视图) - 视图代表模型包含的数据的可视化。
    • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
    image-20220327113409095

我们将创建一个作为模型的 Student 对象。StudentView 是一个把学生详细信息输出到控制台的视图类,StudentController 是负责存储数据到 Student 对象中的控制器类,并相应地更新视图 StudentView

MVCPatternDemo,我们的演示类使用 StudentController 来演示 MVC 模式的用法。

img
  • 创建模型

    public class Student {
        private String rollNo;
        private String name;
        public String getRollNo() {
            return rollNo;
        }
        public void setRollNo(String rollNo) {
            this.rollNo = rollNo;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    
  • 创建视图

    public class StudentView {
       public void printStudentDetails(String studentName, String studentRollNo){
          System.out.println("Student: ");
          System.out.println("Name: " + studentName);
          System.out.println("Roll No: " + studentRollNo);
       }
    }
    
  • 创建控制器

    public class StudentController {
       private Student model;
       private StudentView view;
     
       public StudentController(Student model, StudentView view){
          this.model = model;
          this.view = view;
       }
     
       public void setStudentName(String name){
          model.setName(name);    
       }
     
       public String getStudentName(){
          return model.getName();    
       }
     
       public void setStudentRollNo(String rollNo){
          model.setRollNo(rollNo);      
       }
     
       public String getStudentRollNo(){
          return model.getRollNo();     
       }
     
       public void updateView(){           
          view.printStudentDetails(model.getName(), model.getRollNo());
       }  
    }
    
  • 使用 StudentController 方法来演示 MVC 设计模式的用法

    public class MVCPatternDemo {
       public static void main(String[] args) {
     
          //从数据库获取学生记录
          Student model  = retrieveStudentFromDatabase();
     
          //创建一个视图:把学生详细信息输出到控制台
          StudentView view = new StudentView();
     
          StudentController controller = new StudentController(model, view);
     
          controller.updateView();
     
          //更新模型数据
          controller.setStudentName("John");
     
          controller.updateView();
       }
     
       private static Student retrieveStudentFromDatabase(){
          Student student = new Student();
          student.setName("Robert");
          student.setRollNo("10");
          return student;
       }
    }
    
  • 执行程序,输出结果

    Student: 
    Name: Robert
    Roll No: 10
    Student: 
    Name: John
    Roll No: 10
    

极客时间设计模式之美

Javadoop设计模式

菜鸟教程设计模式

posted @ 2022-06-08 09:56  Maple~  阅读(30)  评论(0编辑  收藏  举报