面向对象设计原则

单一职责原则

  • 是最简单的面向对象设计原则,它用于控制类的粒度大小

开闭原则

  • 通过定义一个抽象类,而不去实现,使得业务可以灵活拓展

里氏替换原则

image-20231221234927788

  • 如果出现JavaCoder必须重写父类的方法的时候, 就不符合里氏替换原则了,那么这个时候,我们可以把自己的思维抬高,定义一个更高级的抽象类, 他没有实现某个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class People {

public abstract void coding(); //这个行为还是定义出来,但是不实现

class Coder extends People{
@Override
public void coding() {
System.out.println("我会打代码");
}
}


class JavaCoder extends People{
public void game(){
System.out.println("艾欧尼亚最强王者已上号");
}

public void coding() {
System.out.println("摆烂了,啊对对对");
}
}
}

依赖倒转原则

高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。

接口隔离原则

客户端不应依赖那些它不需要的接口。

  • 接口的定义要尽可能的细致

合成复用原则

合成复用原则(Composite Reuse Principle)的核心就是委派。

image-20231222000735918

image-20231222000744496

迪米特法则

每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位

  • 降低类与类之间的耦合度
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 Main {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8080); //假设我们当前的程序需要进行网络通信
Test test = new Test();
test.test(socket); //现在需要执行test方法来做一些事情
}

static class Test {
/**
* 比如test方法需要得到我们当前Socket连接的本地地址
*/
public void test(Socket socket){
System.out.println("IP地址:"+socket.getLocalAddress());
}
}
}





public class Main {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8080);
Test test = new Test();
test.test(socket.getLocalAddress().getHostAddress()); //在外面解析好就行了
}

static class Test {
public void test(String str){ //一个字符串就能搞定,就没必要丢整个对象进来
System.out.println("IP地址:"+str);
}
}
}

创建型

工厂方法模式

  • 原本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FruitFactory {
/**
* 这里就直接来一个静态方法根据指定类型进行创建
* @param type 水果类型
* @return 对应的水果对象
*/
public static Fruit getFruit(String type) {
switch (type) {
case "苹果":
return new Apple();
case "橘子":
return new Orange();
default:
return null;
}
}
}
1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getFruit("橘子"); //直接问工厂要,而不是我们自己去创建
System.out.println(fruit);
}
}
  • 优化后
    • 满足了开闭原则
1
2
3
public abstract class FruitFactory<T extends Fruit> {   //将水果工厂抽象为抽象类,添加泛型T由子类指定水果类型
public abstract T getFruit(); //不同的水果工厂,通过此方法生产不同的水果
}
1
2
3
4
5
6
public class AppleFactory extends FruitFactory<Apple> {  //苹果工厂,直接返回Apple,一步到位
@Override
public Apple getFruit() {
return new Apple();
}
}
1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
test(new AppleFactory()::getFruit); //比如我们现在要吃一个苹果,那么就直接通过苹果工厂来获取苹果
}

//此方法模拟吃掉一个水果
private static void test(Supplier<Fruit> supplier){
System.out.println(supplier.get()+" 被吃掉了,真好吃。");
}
}

抽象工厂模式

  • 上面的方法对于产品类较少的时候,还可以用,但是一旦数量过多了,就会出现问题

  • 但是违背了开闭原则

1
2
3
4
5
6
7
8
9
10
11
public class Router {
}
public class Table {
}
public class Phone {
}
public abstract class AbstractFactory {
public abstract Phone getPhone();
public abstract Table getTable();
public abstract Router getRouter();
}

建造者模式

实际上我们是通过建造者来不断配置参数或是内容,当我们配置完所有内容后,最后再进行对象的构建。

相比直接去new一个新的对象,建造者模式的重心更加关注在如何完成每一步的配置,同时如果一个类的构造方法参数过多,我们通过建造者模式来创建这个对象,会更加优雅。

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
public class Student {
...

//一律使用建造者来创建,不对外直接开放
private Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
...
}

public static StudentBuilder builder(){ //通过builder方法直接获取建造者
return new StudentBuilder();
}

public static class StudentBuilder{ //这里就直接创建一个内部类
//Builder也需要将所有的参数都进行暂时保存,所以Student怎么定义的这里就怎么定义
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;

public StudentBuilder id(int id){ //直接调用建造者对应的方法,为对应的属性赋值
this.id = id;
return this; //为了支持链式调用,这里直接返回建造者本身,下同
}

public StudentBuilder age(int age){
this.age = age;
return this;
}

...

public StudentBuilder awards(String... awards){
this.awards = Arrays.asList(awards);
return this;
}

public Student build(){ //最后我们只需要调用建造者提供的build方法即可根据我们的配置返回一个对象
return new Student(id, age, grade, name, college, profession, awards);
toolbarsFlag}
}
}

单例模式

那么,什么是单例模式呢?顾名思义,单例那么肯定就是只有一个实例对象,在我们的整个程序中,同一个类始终只会有一个对象来进行操作。比如数据库连接类,实际上我们只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。

普通模式

1
2
3
4
5
6
7
8
9
public class Singleton {
private final static Singleton INSTANCE = new Singleton(); //用于引用全局唯一的单例对象,在一开始就创建好

private Singleton() {} //不允许随便new,需要对象直接找getInstance

public static Singleton getInstance(){ //获取全局唯一的单例对象
return INSTANCE;
}
}

懒汉模式

  • 在多线程环境下, 懒汉模式存在安全问题

  • 等到用的时候,才去创建对象

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton INSTANCE; //在一开始先不进行对象创建

private Singleton() {} //不用多说了吧

public static Singleton getInstance(){ //将对象的创建延后到需要时再进行
if(INSTANCE == null) { //如果实例为空,那么就进行创建,不为空说明已经创建过了,那么就直接返回
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
  • 然而,我们可以对这个方法进行加锁优化
1
2
3
4
5
6
public static synchronized Singleton getInstance(){   //方法必须添加synchronized关键字加锁
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
  • 但是这样速度又太慢了, 我们可以降低锁的粒度
1
2
3
4
5
6
7
8
public static Singleton getInstance(){
if(INSTANCE == null) {
synchronized (Singleton.class) { //实际上只需要对赋值这一步进行加锁即可
INSTANCE = new Singleton();
}
}
return INSTANCE;
}

可以看到,这种情况下,IDEA会要求我们添加一个volatileINSTANCE,各位还记得这个关键字有什么作用吗?没错,我们还需要保证INSTANCE在线程之间的可见性,这样当其他线程进入之后才会拿INSTANCE由其他线程更新的最新值去判断,这样,就差不多完美了。

  • 最完美的懒汉模式
    • 只有真正使用内部类的时候, 才会进行初始化
1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private Singleton() {}

private static class Holder { //由静态内部类持有单例对象,但是根据类加载特性,我们仅使用Singleton类时,不会对静态内部类进行初始化
private final static Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance(){ //只有真正使用内部类时,才会进行类初始化
return Holder.INSTANCE; //直接获取内部类中的
}
}

原型模式

并且通过复制这个原型来创建新的对象。也就是说,原型对象作为模板,通过克隆操作,来产生更多的对象,就像细胞的复制一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student implements Cloneable{

String name;

public Student(String name){
this.name = name;
}

public String getName() {
return name;
}

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
  • 使用这个方式,虽然这个对象是新创建的, 但是其中的属性和方法,确还是引用类型
1
2
3
4
5
public static void main(String[] args) throws CloneNotSupportedException {
Student student0 = new Student("小明");
Student student1 = (Student) student0.clone();
System.out.println(student0.getName() == student1.getName());
}
  • 我们修改成这样, 就可以真正实现深拷贝了
1
2
3
4
5
6
@Override
public Object clone() throws CloneNotSupportedException { //这里我们改进一下,针对成员变量也进行拷贝
Student student = (Student) super.clone();
student.name = new String(name);
return student; //成员拷贝完成后,再返回
}