设计原则

设计原则

常用的七大原则:

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

    对于类来说,即一个类应该只负责一项职责。否则因其中一个职责需要变更而导致类修改,可能会导致冷一个职责执行出错。

    注意事项和细节

    • 降低类的复杂度,一个类只负责一项职责
    • 提高类的可读性,可维护性
    • 降低变更引起的风险
    • 通常情况下,应该遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则,只有类中的方法足够少,可以在方法级别保持单一职责原则。
  2. 接口隔离原则(Interface Segregation)

    客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上

    image-20230117103042144
  3. 依赖倒转原则(Dependence Inversion)

    高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;依赖倒转原则的中心思想是面向接口编程;设计理念:相对于细节的多边性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多,在java中,抽象指的是接口或抽象类,细节就是具体的实现类。;使用接口或抽象类的目的是制定好规范,而不涉及具体的操作,把展现细节的任务交给他们的实现类去完成。

    依赖关系传递的三种方式和应用案例:

    • 接口传递
    • 构造方法传递
    • setter方式传递

    注意:低层模块尽量都要有抽象类或接口,或者两者都有;变量的申明尽量是抽象类或接口,利于程序扩展和优化。继承时遵循里氏替换原则。

  4. 里氏替换原则(Liskov Substitution)

    如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序p的行为没有发生变化,那么类型T2是类型T1的子类型,换句话说,所有引用基类的地方必须能透明地使用其子类的对象。

    在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。

    解决冲突的办法:通用的做法是,原来的父类和子类都继承一个更通俗的基类原有的继承关系去掉,采用依赖,聚合,组合等关系代替。

  5. 开闭原则 | ocp原则(Open Closed)

    一个软件实体如类,模块和函数应该对扩展开放,对修改关闭,用抽象构建框架,用实现扩展细节,当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

  6. 迪米特法则

    一个对象应该对其他对象保持最少的了解

    类与对象关系越密切,耦合度越大,迪米特法则又叫最少知道原则,即一个对象对自己依赖的类知道的越少越好,也就是说,对于被依赖的类不管多么复杂, 都尽量将逻辑封装在类的内部,对外除了提供public方法,不对外泄露任何信息。迪米特法则还有个更简单的定义:只与直接的朋友通信。

    直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系,耦合的方式很多,依赖,关联,组合,聚合等,其中,我们称出现成员变量,方法参数,方法返回值中的类为直接朋友,而出现在局部变量中的类不是直接的朋友,也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

    解决办法:将方法封装到自己的类,不要把具体的实现写到别人的类中。

    注意事项:迪米特法则的核心是降低类之间的耦合,但是注意,由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。

  7. 合成复用原则

    尽量使用合成/聚合的方式,而不是使用继承

    设计原则核心思想:

    • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
    • 针对接口编程,而不是针对实现编程
    • 为了交互对象之间的松耦合设计而努力

UML(Unified modeling language)

分类:

  1. 用例图
  2. 静态结构图:类图、对象图、包图、组件图、部署图
  3. 动态行为图:交互图、状态图、活动图

类图是描述类与类之间的关系的,是UML图中最核心的。

类之间的关系:依赖、泛化(继承)、实现、关联、聚合和组合

依赖:在类中用到了对方,包括类的成员属性、方法的返回类型、方法接受的参数类型、方法中使用到

聚合和组合都是整体与部分的关系,但是聚合整体和部分可以分开,而组合不能(在属性中new、级联删除)

设计模式(分为三类,共23种)

创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式

结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式

行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式

单例模式

所谓类的单例模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

单例模式有八种方式:

  1. 饿汉式(静态常量)

    步骤如下:

    1. 构造器私有化(防止外部通过new创建实例)
    2. 类的内部创建对象
    3. 向外暴露一个静态的公共方法,getInstance()

    代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Singleton{
    // 1.构造器私有化,防止外部通过new创建实例
    private Singleton() {

    }
    // 2.本类内部创建对象实例
    private final static Singleton instance = new Singleton();

    // 3.向外暴露一个静态的公共方法
    public Singleton getInstance() {
    return instance;
    }
    }

    // 测试
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    // instance1 = instance2 (true, hashcode也相同)

    分析:写法简单,在类装载的时候就完成了实例化,避免了线程同步问题,但由于在类加载的时候就完成实例化,没有达到Lazy Loading的效果,如果从始至终从未使用过这个实例,则会造成内存的浪费。此方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类加载的原因有很多种(会导致没有使用getInstance,但是instance被创建了 ),因此不能确定有其他的方式(或者其他的静态方法)导致类加载,这时候初始化instance就没有达到lazy loading的效果

    结论:这种单例模式可用,可能造成内存浪费。

  2. 饿汉式(静态代码块)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Singleton{
    // 1.构造器私有化,防止外部通过new创建实例
    private Singleton() {

    }
    // 2.本类内部创建对象实例
    private final static Singleton instance;

    static { // 在静态代码块中,创建单例对象
    instance = new Singleton();
    }

    // 3.向外暴露一个静态的公共方法
    public Singleton getInstance() {
    return instance;
    }
    }

    // 测试
    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    // instance1 = instance2 (true, hashcode也相同)

    分析:和上面的方式类似,只是将类实例化的过程放在了静态代码块中,也是在类加载的时候,就执行静态代码块中的代码,初始化类的实例,优缺点同上。

    结论:这种单例模式可用,但是可能造成内存浪费

  3. 懒汉式(线程不安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    // 提供一个静态的公共方法,当使用到该方法时,才去创建instance
    // 即懒汉式,线程不安全
    public static Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    return instance;
    }
    }
    }
    1. 起到了懒加载的效果,但是只能在单线程下使用
    2. 如果在多线程下,一个线程进入了if判断语句,还未来得及往下执行,另一个线程也通过了这个判断语句,这是便会产生多个实例,所以在多线程环境下不可使用这种方式

    结论:在实际开发中,不要使用这种方式

  4. 懒汉式(线程安全,同步方法),添加synchronized关键字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    // 提供一个静态的公共方法,加入了同步处理的代码,解决线程安全问题
    // 即懒汉式,线程不安全
    public static synchronized Singleton getInstance() {
    if (instance == null) {
    instance = new Singleton();
    return instance;
    }
    }
    }

    解决了线程不安全问题,但是效率太低了,每个线程在想获得类的时候,执行getInstance方法都要进行同步,而其实这个方法只执行一次实例化代码就够了,后面的想要获得该类实例,直接return就行了,方法进行同步效率太低了。

    结论:在实际开发中,不要使用这种方式

  5. 懒汉式(线程安全,同步代码块)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    instance = new Singleton();
    }
    }
    return instance();
    }
    }

    这种方法并不能起到线程同步的作用,加入一个线程进入了if判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,

    结论:在实际开发中,不能使用这种方式

  6. 双重检查(推荐使用,可以解决线程安全问题、效率和懒加载的问题)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Singleton {
    private static volatile Singleton singleton; // 使得共享的变量一旦被修改就会被刷新到主存中
    private Singleton();
    // 提供一个静态的公共方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,同时保证了效率
    public static Singleton getInstance() {
    if (singleton == null) { // 第一次判断
    synchronized (Singleton.class) { // 保证只有一个线程在执行
    if (singleton == null) { // 第二次检查
    singleton = new Singleton();
    }
    })
    }
    return singleton;
    }

    }

    双重检查概念是多线程开发中常使用到的,我们进行了两次if(singleton = null)检查,这样就可以保证线程安全了,这样,实例化代码只用执行一次,后面再次访问时,判断if(singleton == null)直接return实例化对象,也避免反复进行方法同步,线程安全、延迟加载、效率较高

    结论:在实际开发中,推荐使用这种单例设计模式

  7. 静态内部类,推荐使用

    特点:

    1. 外部的类被装载时,静态内部类并不会立即被装载
    2. 静态内部类只会装载一次,装载时线程是安全的

    懒加载+线程安全

    1. SingletonInstance在Singleton进行装载时并不会被装载,可以实现懒加载的效果
    2. 在通过getInstance获得静态内部类中的静态属性时,才会导致SingletonInstance被加载
    3. JVM在装载类的时候时线程安全的,这里使用了JVM底层提供的机制,保证初始化时线程安全
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 静态内部类完成
    public class Singleton {
    private staic volatile Singleton instance;

    // 构造器私有化
    private Singleton() {}

    // 静态内部类,该类中有一个类型为Singleton的静态属性
    private static class SingletonInstance {
    private static final Singleton INSTANCE = new Singleton();
    }
    // 提供一个静态的共有方法,直接返回SingletonInstance.INSTANCE
    public static synchronized Singleton getInstance() {
    return SingtonInstance.INSTANCE;
    }

    }

    特点:

    1. 采用了类加载的机制保证初始化实例时只有一个线程
    2. 静态内部类方式在Singleton类被加载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载SingletonInstance类,从而完成Singleton的实例化
    3. 类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

    总结:保证线程安全,利用静态内部类特点实现懒加载,效率高--->推荐使用

  8. 枚举

    1
    2
    3
    4
    5
    6
    7
    // 使用枚举,可以实现单例
    enum Sington {
    INSTANCE;
    public void sayOK() {
    System.out.println("ok");
    }
    }

    特点:

    1. 借助JDK1.5中添加的枚举类型来实现单例模式,不仅可以避免多线程同步问题,而且还能防止反序列化重新创建新的对象
    2. 此方法为Effective Java作者John bloch所提倡

    结论:推荐使用

总结:枚举、静态内部类、双重检查、饿汉式(内存浪费)

实际应用:Java中的Runtime

单例模式注意事项和细节说明

  1. 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想要实例化一个单例类时,必须要记住使用相应的获取对象的方法,而不是使用new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象,创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常使用到的对象、工具类对象,频繁访问数据库或文件的对象(比如数据源、session工厂等)

工厂模式

image-20230125090854491
image-20230125091220018

改进思路:修改代码可以接受,但是如果我们在其他地方也有创建Pizza的代码,就意味着,也需要修改,而创建Pizza的代码,往往有多处。

思路:把创建Pizza对象封装发到一个类中,这样我们有新的Pizza种类时,只需要修改该类就可,其它有创建到Pizza对象的代码就不需要修改了->简单工厂模式

简单工厂模式

介绍:

  1. 简单工厂模式时属于创建型模式,是工厂模式的一种,简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单使用的模式
  2. 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为
  3. 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式。
image-20230125095025464
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SimpleFactory {
// 根据orderType返回对应的Pizza对象
public Pizza createPizza(String orderType) {
// 使用简单工厂模式
Pizza pizza = null;
pizza.setName(orderType);
if (orderType.equals("greek")) {
...
} else if () {
...
} else if () {

}
return pizza;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class OrderPizza {
// 定义一个简单工厂对象
SimpleFactory simpleFactory;
Pizza pizza = null;

//构造器
public OrderPizza(SimpleFactory simpleFactory) {
setFactory(simpleFactory);
}

public void setFactory(SimpleFactory simpleFactory) {
String orderType = ""; // 用户输入的
this.simpleFactory = simpleFactoryj; //设置简单工厂对象
do {
orderType = getType();
pizza = this.simpleFactory.createPizza(orderType);

if(pizza != null) {
pizza.prepare();
...
} else {
// 失败
break;
}
}while(true);
}


public class OrderPizza {
private String getType() {

}
}
}
1
2
3
4
5
6
7
public class PizzaStore {
public static void main(String[] args) {
// 使用简单工厂模式
new OrderPizza(new SimpleFactory());
// 退出程序
}
}

静态工厂模式可以将SimpleFactory中的create设置为static

工厂方法模式

定义了一个创建对象的抽象方法,由子类决定要实例化的类,工厂方法模式将对象的实例化推迟到子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class OrderPizza {
abstract Pizza createPizza(String orderType);

public OrderPizza() {
Pizza pizza = null;
String orderType;
do {
orderType = getType();
pizza = createPizza(orderType); // 抽象方法,由工厂子类完成
pizza.prepare();
...

} while (true);

}

}
1
2
3
4
5
6
7
8
9
public class BJOrderPizza extends Orderpizza P {
Pizza pizza = null;
if (orderType.equals("chees")) {
pizza = new BJCheesePizza();
} else if {
...
}
return pizza
}
1
2
3
4
5
public class PizzaStore {
public static void main(String[] args) {
new BJOrderPizza();
}
}

抽象工厂模式

定义了一个interface用于创建相关或有依赖关系的对象簇,而无需指明具体的类

将工厂抽象成两层,AbsFactory抽象工厂和具体实现的工厂子类

image-20230125211116191
1
2
3
4
5
// 一个抽象工厂模式的抽象层(接口)
public interface AbsFactory {
// 让下面的工厂子类来具体实现
public Pizza createPizza(String orderType);
}
1
2
3
4
5
6
7
8
9
10
11
12
public class BJFactory implements AbsFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("cheese")) {
pizza = new BJCheesePizza();
} else if () {
...
}
return pizza;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class LDFactory implements AbsFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("cheese")) {
pizza = new LDCheesePizza();
} else if () {
...
}
return pizza;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderPizza {
AbsFactory absfactory;
public OrderType(AbsFactory )

private void setFactory(AbsFactory factory) {
Pizza pizza = null;
String orderType = "";
this.factory = factory;
do {
orderType = getType();
pizza = factory.createPizza(orderType);
...
} while(true);
}
}

三种工厂模式:简单工厂模式、工厂方法模式、抽象工厂模式

设计模式的依赖抽象原则

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。

原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节

原理:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即对象.clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Sheep implements Cloneable{
...
@Override
protected Object clone() {
Sheep sheep = null;
try {
sheep = (Sheep)super.clone();
} catch (Exception e) {
System.out.println(e.getMessage());
}
return sheep;
}
}

Spring中原型bean的创建,就是原型模式的应用

浅拷贝

  1. 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
  2. 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
  3. 浅拷贝是使用默认的clone()方法来实现的

深拷贝

  1. 复制对象的所有基本数据类型的成员变量值
  2. 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象进行拷贝

深拷贝实现方式1:重写clone方法来实现深拷贝

深拷贝实现方式2:通过对象序列化实现深拷贝

只有实现了Serializable接口的类的对象才能被序列化。Serializable接口是一个空接口,只起到标记作用。如果对象的属性是对象,属性对应类也必须实现 Serializable 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DeepCloneableTarget implements Serializable, Cloneable {
private static final serialVersionUID = 1L;
private String cloneName;
private String cloneClass;
public DeepCloneableTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass
}

// 因为该类的属性都是String,因此这里使用默认的克隆方法完成即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class DeepProtoType implements Serializable, Cloneable {
public Sring name;
public DeepCloneableTarget deepCloneableTarget; // 引用类型
public DeepProtoType() {
super();
}
// 深拷贝
// 1.使用clone方法
@Override
protected Object clone() throws CloneNotSupportedExpetion {
Object deep = null;
// 完成对基本数据类型的克隆
deep = super.clone();
// 对引用类型的属性进行单独处理
DeepProtoType deepProtoType = (DeepProtoType)deep;
// DeepCloneableTarget中的属性都是基本数据类型
deepProtoType.deepColoneableTatget = (DeepCloneableTarget)deepCloneableTarget.clone();
return deepProtoType;
}

// 2.通过对象序列化实现(推荐)
public Object deepClone() {
// 创建流对象
ByteArrayOutputSream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputSream bis = null;
ObjectInputStream ois = null;

try {
// 序列化操作
bos = new ByteArrayOutputStream();
// ObjectOutputStream只有一个public权限的构造方法,该构造方法需要传入一个OutputStream表示将对象二进制流写入到指定的OutputStream
oos = new ObjectOutputStream(bos);
oos.writeObject(this); // 将当前这个对象以对象流的方式输出(序列化)

// 反序列化
bis = new ByteArrayInputSream(bos.toByteArray());
ois = new ObjectInputSream(bis);
DeepProtoType cpoyObj = (DeepProtoType)ois.readObject();
return cpoyObj;
} catch (Exception e) {
return null;
} finally {
try {
bos.close();
oos.close();
ois.close();
} catch (Exception e2) {
...
}
}
}
}

原型模式的注意事项和细节

  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  2. 不用重新初始化对象,而是动态地获得对象运行时的状态
  3. 如果原始对象发生变化(增加或者减少属性),其他克隆对象也会发生相应的变化,无需修改代码
  4. 在实现深克隆的时候可能需要比较复杂的代码
  5. 缺点:需要为每一类配备一个克隆方法,这对全新的类来说不是很难,但是对已有的类进行改造时,需要修改其源代码,违背了ocp原则

建造者模式

是一种对象构建模式,可以将复杂对象的建造过程抽象出来,使这个抽象过程的不同实现方式可以构造出不同表现的对象

建造者模式的四个角色:

  1. Product
  2. Builder
  3. ConcreteBuilder
  4. Director

适配器模式

  1. 类适配器
  2. 对象适配器
  3. 接口适配器

类适配器模式注意事项

  1. java是单继承机制,所以类适配器需要继承src类这一点算是一个缺点,因为这要求dst必须是接口,有一定局限性
  2. src类的方法在Adapter中都会暴露出来,也增加了使用的成本
  3. 由于继承了src类,所以它可以根据需要重写src的方法,使得Adapter的灵活性增强了。

对象适配器

  1. 基本思路和类适配器模式相同,只是将Adapter类作修改,不是继承src类,而是持有src类的实例,以解决兼容性的问题,即:持有src类,实现dst类接口,完成src->dst的适配
  2. 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系
  3. 对象适配器模式是适配器模式中常用的一种

接口适配器

  1. 适配器模式又称缺省适配器模式
  2. 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现,那么该抽象类的子类(匿名内部类)可有选择地覆盖父类中的某些方法来实现需求
  3. 适用于一个接口不想使用其他所有的方法的情况

设计原则
https://jetthuang.top/所有/Design Pattern/
作者
Jett Huang
发布于
2023年1月20日
许可协议