java 设计模式:软件设计原则面向对象理论23 种设计模式

Posted Henrik-Yao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 设计模式:软件设计原则面向对象理论23 种设计模式相关的知识,希望对你有一定的参考价值。

文章目录

软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用

软件设计原则

1.单一职责原则(Single Responsibility Principle)

一个类只负责完成一个职责或者功能

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性,所以,单一职责的粒度要根据具体业务设计

2.开闭原则(Open Closed Principle)

对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级

想要达到这样的效果,需要使用接口和抽象类

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了

3.里氏代换原则(Liskov Substitution Principle)

里氏代换原则是面向对象设计的基本原则之一

里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大

4.接口隔离原则(Interface Segregation Principle)

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上

3.依赖倒转原则(Dependency Inversion Principle)

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层

Tomcat 是运行 Java Web 应用程序的容器。Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个抽象,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范

5.迪米特法则(Law of Demeter)

迪米特法则又叫最少知识原则

只和直接朋友交谈,不跟陌生人说话(Talk only to your immediate friends and not to strangers)

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性

迪米特法则中的朋友是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法

6.合成复用原则(Composite/Aggregate Reuse Principle)

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现

通常类的复用分为继承复用和合成复用两种

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象

7.其他原则(KISS、YAGNI、DRY)

  • KISS 原则:即 Keep It Short and Simple,尽量保持简洁
  • YAGNI 原则:即 You Ain’t Gonna Need It,不要过度设计不需要的代码
  • DRY 原则:即 Don’t Repeat Yourself,不要写重复的代码

面向对象

面向对象编程因为其具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础,面向对象的优势是易复用、易扩展、易维护

1.面向对象的特性

特性定义实现目的
封装隐藏信息,保护数据访问暴露有限接口和数学,需要编程语言提供访问控制的语法(public,private)提供代码可维护性,降低接口复杂度,提供类的易用性
抽象隐藏具体实现,使用者只需关注功能,无需关注实现通过接口类或者抽象类实现,特殊语法机制非必须提供代码的扩展性、维护性,降低代码复杂度,减少细节负担
继承表示 is-a 关系,分为单继承和多继承需要编程语言提供特殊语法机制,例如 java 的extends解决代码复用问题
多态子类替换父类,在运行时调用子类的实现需要编程语言提供特殊的语法机制。比如继承、接口类、duck-typing提高代码扩展性和复用性

2.面向对象与面向过程

面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元

面向对象编程相比起面向过程编程的优势

  1. 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发
  2. 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护
  3. 从编程语言跟机器打交道的方式的演进规律中,可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能

违反面向对象编程风格的典型代码设计

  1. 滥用 getter、setter 方法,破坏了代码的封装性
  2. Constants 类、Utils 类的设计问题,滥用全局变量和全局方法,应细分职责内聚到对应类中
  3. 基于贫血模型的开发模式,即定义数据和方法分离的类

3.抽象类与接口

抽象类的特征:

  • 抽象类不允许被实例化,只能被继承
  • 抽象类可以包含属性和方法
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法

接口的特征:

  • 接口不能包含属性(也就是成员变量)
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口中声明的所有方法

抽象类和接口存在的意义:抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性

抽象类和接口的应用场景区别:如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那就用接口

tips:在 java8 之前,定义的接口不能有具体实现,这会导致在后续维护的时候如果想要在接口中新增一个方法,必须在所有实现类中都实现一遍,并且只有几个新的实现类可能要去具体实现,其他的都只是加上默认实现,这样比较麻烦。在 java8 中接口可以用使用关键字 default,来实现一个默认方法,这样就解决了上述的麻烦

4.基于接口而非实现编程

  • 基于接口而非实现编程也就是基于抽象而非实现编程,在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性
  • 在定义接口的时候,一方面,命名要足够通用,不能包含跟具体实现相关的字眼;另一方面,与特定实现有关的方法不要定义在接口中

5.贫血模型与充血模型

贫血模型(Anemic Domain Model)是指像 UserVo 这样,只包含数据,不包含业务逻辑的类

充血模型(Rich Domain Model)正好相反,其数据和对应的业务逻辑被封装到同一个类中。因此,充血模型满足面向对象的封装特性,是典型的面向对象编程风格

创建者模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是将对象的创建与使用分离

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节

其中单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的

创建型模式是将创建和使用代码解耦

1.单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

单例设计模式分类两种:

  • 饿汉式:类加载就会导致该单实例对象被创建
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

懒汉式之双检锁

在 getInstance() 方法上加 synchronized 关键字可以实现线程安全,但会导致方法的执行效率低,并且 getInstance() 的大多数操作是读操作,读操作是线程安全的,没必要让其持有锁再执行,由此引出了双检锁机制(Demo 如下)

 /**
  * 双重检查方式
  */
public class Singleton 

    //私有构造方法
    private Singleton() 

    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() 
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) 
            synchronized (Singleton.class) 
                //抢到锁之后再次判断是否为空
                if(instance == null) 
                    instance = new Singleton();
                
            
        
        return instance;
    

双重检查的原因在于当两个线程同时竞争锁资源时,其中一个竞争成功创建对象解锁后唤醒另外一个线程,另外一个线程已经通过第一层 if 的判断在锁外等着,这时拿到锁资源后会进入 synchronized 代码块,如果没有第二次 if 的话会重复创建对象,破坏单例

在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作,要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字,,volatile 关键字可以保证可见性和有序性

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题

2.工厂方法模式

简单工厂模式:不是一种设计模式,像是一种编程习惯,将类的创建交给工厂,如果加静态方法的话则为静态工厂

public class SimpleCoffeeFactory 

    public static Coffee createCoffee(String type) 
        Coffee coffee = null;
        if("americano".equals(type)) 
            coffee = new AmericanoCoffee();
         else if("latte".equals(type)) 
            coffee = new LatteCoffee();
        
        return coffe;
    

工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象,工厂方法使一个产品类的实例化延迟到其工厂的子类


public interface IRuleConfigParserFactory 
  IRuleConfigParser createParser();


public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory 
  @Override
  public IRuleConfigParser createParser() 
    return new JsonRuleConfigParser();
  


public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory 
  @Override
  public IRuleConfigParser createParser() 
    return new XmlRuleConfigParser();
  


public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory 
  @Override
  public IRuleConfigParser createParser() 
    return new YamlRuleConfigParser();
  


public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory 
  @Override
  public IRuleConfigParser createParser() 
    return new PropertiesRuleConfigParser();
  

3.抽象工厂模式

是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品


public interface IConfigParserFactory 
  IRuleConfigParser createRuleParser();
  ISystemConfigParser createSystemParser();
  //此处可以扩展新的parser类型,比如IBizConfigParser


public class JsonConfigParserFactory implements IConfigParserFactory 
  @Override
  public IRuleConfigParser createRuleParser() 
    return new JsonRuleConfigParser();
  

  @Override
  public ISystemConfigParser createSystemParser() 
    return new JsonSystemConfigParser();
  


public class XmlConfigParserFactory implements IConfigParserFactory 
  @Override
  public IRuleConfigParser createRuleParser() 
    return new XmlRuleConfigParser();
  

  @Override
  public ISystemConfigParser createSystemParser() 
    return new XmlSystemConfigParser();
  


// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

4.建造者模式

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示,建造者模式可以实现链式编程


public class ResourcePoolConfig 
  private String name;
  private int maxTotal;
  private int maxIdle;
  private int minIdle;

  private ResourcePoolConfig(Builder builder) 
    this.name = builder.name;
    this.maxTotal = builder.maxTotal;
    this.maxIdle = builder.maxIdle;
    this.minIdle = builder.minIdle;
  
  //...省略getter方法...

  //我们将Builder类设计成了ResourcePoolConfig的内部类。
  //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
  public static class Builder 
    private static final int DEFAULT_MAX_TOTAL = 8;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;

    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int maxIdle = DEFAULT_MAX_IDLE;
    private int minIdle = DEFAULT_MIN_IDLE;

    public ResourcePoolConfig build() 
      // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
      if (StringUtils.isBlank(name)) 
        throw new IllegalArgumentException("...");
      
      if (maxIdle > maxTotal) 
        throw new IllegalArgumentException("...");
      
      if (minIdle > maxTotal || minIdle > maxIdle) 
        throw new IllegalArgumentException("...");
      

      return new ResourcePoolConfig(this);
    

    public Builder setName(String name) 
      if (StringUtils.isBlank(name)) 
        throw new IllegalArgumentException("...");
      
      this.name = name;
      return this;
    

    public Builder setMaxTotal(int maxTotal) 
      if (maxTotal <= 0) 
        throw new IllegalArgumentException("...");
      
      this.maxTotal = maxTotal;
      return this;
    

    public Builder setMaxIdle(int maxIdle) 
      if (maxIdle < 0) 
        throw new IllegalArgumentException("...");
      
      this.maxIdle = maxIdle;
      return this;
    

    public Builder setMinIdle(int minIdle) 
      if (minIdle < 0) 
        throw new IllegalArgumentException("...");
      
      this.minIdle = minIdle;
      return this;
    
  


// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .setMaxIdle(10)
        .setMinIdle(12)
        .build();

5.原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

原型模式的克隆分为浅克隆和深克隆

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址

public class Realizetype implements Cloneable 

    public Realizetype() 
        System.out.println("具体的原型对象创建完成!");
    

    @Override
    protected Realizetype clone() throws CloneNotSupportedException 
        System.out.println("具体原型复制成功!");
        return (Realizetype) super.clone();
    

结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象

由于组合关系或聚合关系比继承关系耦合度低,满足合成复用原则,所以对象结构型模式比类结构型模式具有更大的灵活性

代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类

对比

设计模式特点
代理模式代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同
桥接模式桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
装饰者模式装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
适配器模式适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口

结构型模式是将不同的功能代码解耦

1.代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

Java 中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在 Java 运行时动态生成。动态代理又有 JDK 代理和 CGLib 代理两种

静态代理

//卖票接口
public interface SellTickets 
    void sell();


//火车站  火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets 

    public void sell() 
        System.out.println("火车站卖票");
    


//代售点
public class ProxyPoint implements SellTickets 

    private TrainStation station = new TrainStation();

    public void sell() 
        System.out.println("代理点收取一些服务费用");
        station.sell();
    


//测试类
public class Client 
    public static void main(String[] args) 
        ProxyPoint pp = new ProxyPoint();
        pp.sell();
    

JDK 动态代理

//卖票接口
public interface SellTickets 
    void sell();


//火车站  火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets 

    public void sell() 
        System.out.println("火车站卖票");
    


//代理工厂,用来创建代理对象
public class ProxyFactory 

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() 
        //使用Proxy获取代理对象
        /*
            newProxyInstance()方法参数说明:
                ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
                Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
                InvocationHandler h : 代理对象的调用处理程序
         */
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() 
                    /*
                        InvocationHandler中invoke方法参数说明:
                            proxy : 代理对象
                            method : 对应于在代理对象上调用的接口方法的 Method 实例
                            args : 代理对象调用接口方法时传递的实际参数
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 

                        System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                        //执行真实对象
                        Object result = method.invoke(station, args);
                        return result;
                    
                );
        return sellTickets;
    


//测试类
public class Client 
    public static void main(String[] args) 
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();
        
        SellTickets proxyObject = factory.getProxyObject()java 设计模式:软件设计原则面向对象理论23 种设计模式

GOF 的23种JAVA常用设计模式总结 03 面向对象七大设计原则

Java设计模式:23种设计模式

喵星之旅-沉睡的猫咪-面向对象的设计原则

面向对象开发中的七大设计原则和23种设计模式

java设计模式:面向对象设计的7个原则