设计模式
面向对象设计原则
单一职责原则
单一职责原则(Simple Responsibility Pinciple,SRP)是最简单的面向对象设计原则,它用于控制类的粒度大小。
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
例如:
public class People{
public void cooking(){
System.out.println("我会做饭");
}
public void teaching(){
System.out.println("我会教书");
}
。。。。
}
People类中功能太多,略显臃肿,应该将各种能力再细分成对应的职业:
public class Cooker{
public void cooking(){
System.out.println("我会做饭");
}
}
public class Teacher{
public void teaching(){
System.out.println("我会教书");
}
}
我们将类的粒度进行更近一步的划分,这样就很清晰了,包括我们以后在设计Mapper、Service、Controller等等,根据不同的业务进行划分,都可以采用单一职责原则,以它作为我们实现高内聚低耦合的指导方针。实际上我们的微服务也是参考了单一职责原则,每个微服务只应担负一个职责。
开闭原则
开闭原则(Open Close Principle)也是重要的面向对象设计原则。
软件实体应当对扩展开放,对修改关闭。
//定义抽象接口
public abstract Code{
public abstract void coding();
class JavaCoder extends Code(){
@Override
public void coding() {
System.out.println("Java太卷了T_T");
}
}
}
抽象类,只用定义这个类要求的行为,具体实现由需要实现他的类自行实现,有较好的扩展性延续性。
里氏替换原则
里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为 “数据的抽象与层次” 的演说中首先提出。
所有引用基类的地方必须能透明地使用其子类的对象。
简单的说就是,子类可以扩展父类的功能,但不能改变父类原有的功能:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。
public class Father{
public void play(){
System.out.println("我会打lol");
}
class Son extends Father{
public void study(){
System.out.println("我爱学习");
}
}
}
Son继承了Father,但是并没有覆盖play方法。符合里氏替换原则。
public class Father{
public void play(){
System.out.println("我会打lol");
}
class Son extends Father{
public void study(){
System.out.println("我爱学习");
}
public void play(){
System.out.println("我爱cs");
}
}
}
Son覆盖了父类play,他不再具备父类的行为了,违背了里氏替换原则。
这种情况可以让他们两都继承于一个更高的类People
public abstract class People {
public abstract void play(); //这个行为还是定义出来,但是不实现
public class Father{
public void play(){
System.out.println("我会打lol");
}
class Son extends Father{
public void study(){
System.out.println("我爱学习");
}
public void play(){
System.out.println("我爱cs");
}
}
}
}
依赖倒转原则
依赖倒转原则(Dependence Inversion Principle)也是我们一直在使用的,最明显的就是我们的Spring框架了。
高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。
在之前,编写项目:
public class Main {
public static void main(String[] args) {
UserController controller = new UserController();
}
static class UserMapper {
//CRUD...
}
static class UserServiceNew { //由于UserServiceNew发生变化,会直接影响到其他高层模块
UserMapper mapper = new UserMapper();
//业务代码....
}
static class UserController { //焯,干嘛改底层啊,我这又得重写了
UserService service = new UserService(); //哦豁,原来的不能用了
UserServiceNew serviceNew = new UserServiceNew(); //只能修改成新的了
//业务代码....
}
}
使用Spring框架后,使用ServiceImpl来具体实现Service中的方法,Controller直接注入Service接口,这样修改实现也不用修改对应业务的逻辑。
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)实际上是对接口的细化。
客户端不应依赖那些它不需要的接口。
电脑和风扇都是电子设备,但是电脑拥有CPU显卡等部件,因此不能单单编写一个Device类让他们两个继承,需要细分为电子设备和普通设备,避免过多不需要的属性堆叠。
合成复用原则
合成复用原则(Composite Reuse Principle)的核心就是委派。
优先使用对象组合,而不是通过继承来达到复用的目的。
可以将B中需要的A类当作B的一个属性 A a,在要用到B的时候进行指定A。
迪米特法则
迪米特法则(Law of Demeter)又称最少知识原则,是对程序内部数据交互的限制。
每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
简单来说就是,一个类/模块对其他的类/模块有越少的交互越好。当一个类发生改动,那么,与其相关的类(比如用到此类啥方法的类)需要尽可能少的受影响(比如修改了方法名、字段名等,可能其他用到这些方法或是字段的类也需要跟着修改)这样我们在维护项目的时候会更加轻松一些。
Test方法需要一个A类的name属性,传入时不传入A类,而是传入String name,在外部Main方法,直接提取A中的name传入Test。
设计模式(创建型)
软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
工厂方法模式
使用简单工厂模式
来创建对象
public abstract class Fruit { //水果抽象类
private final String name;
public Fruit(String name){
this.name = name;
}
@Override
public String toString() {
return name+"@"+hashCode(); //打印一下当前水果名称,还有对象的hashCode
}
}
public class Apple extends Fruit{ //苹果,继承自水果
public Apple() {
super("苹果");
}
}
public class Orange extends Fruit{ //橘子,也是继承自水果
public Orange() {
super("橘子");
}
}
正常情况下,我们直接new就可以得到对象了:
public class Main {
public static void main(String[] args) {
Apple apple = new Apple();
System.out.println(apple);
}
}
现在我们将对象的创建封装到工厂中:
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;
}
}
}
现在我们就可以使用此工厂来创建对象了:
public class Main {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getFruit("橘子"); //直接问工厂要,而不是我们自己去创建
System.out.println(fruit);
}
}
利用对扩展开放,对修改关闭的性质,将简单工厂模式
改进为工厂方法模式
,那现在既然不让改,那么我们就看看如何去使用扩展的形式:
public abstract class FruitFactory<T extends Fruit> { //将水果工厂抽象为抽象类,添加泛型T由子类指定水果类型
public abstract T getFruit(); //不同的水果工厂,通过此方法生产不同的水果
}
public class AppleFactory extends FruitFactory<Apple> { //苹果工厂,直接返回Apple,一步到位
@Override
public Apple getFruit() {
return new Apple();
}
}
这样,我们就可以使用不同类型的工厂来生产不同类型的水果了,并且如果新增了水果类型,直接创建一个新的工厂类就行,不需要修改之前已经编写好的内容。
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()+" 被吃掉了,真好吃。");
}
}
这样,我们就简单实现了工厂方法模式,通过工厂来屏蔽对象的创建细节,使用者只需要关心如何去使用对象即可。
建造者模式
经常看到有很多的框架都为我们提供了形如XXXBuilder
的类型,我们一般也是使用这些类来创建我们需要的对象。
JavaSE中的StringBulider类。。。
现在有一个学生类:
public class Student {
int id;
int age;
int grade;
String name;
String college;
String profession;
List<String> awards;
public Student(int id, int age, int grade, String name, String college, String profession, List<String> awards) {
this.id = id;
this.age = age;
this.grade = grade;
this.name = name;
this.college = college;
this.profession = profession;
this.awards = awards;
}
}
他的属性很多,直接构造容易出错。所以,我们现在可以使用建造者模式来进行对象的创建:
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);
}
}
}
现在,我们就可以使用建造者来为我们生成对象了:
public static void main(String[] args) {
Student student = Student.builder() //获取建造者
.id(1) //逐步配置各个参数
.age(18)
.grade(3)
.name("小明")
.awards("ICPC-ACM 区域赛 金牌", "LPL 2022春季赛 冠军")
.build(); //最后直接建造我们想要的对象
}
单例模式
懒汉式和饿汉式
饿汉式:在使用这个类时自动进行创建对应实例对象。
懒汉式:在使用这个类的单例对象初始化方法时才会创建对象。
饿汉式多线程环境下会有问题!,如果几个线程同时调用,会被创建多次。为了避免这个问题,最简单的是加一个synchronized
锁,但是在高并发情况下效率很低。
优化:在一开始进行if判断,判断需要的单例对象是否为空,但是这样也有可能多个线程同时判断为null进入等锁状态
public static Singleton getInstance(){
if(INSTANCE == null) {
synchronized (Singleton.class) {
if(INSTANCE == null) INSTANCE = new Singleton(); //内层还要进行一次检查,双重检查锁定
}
}
return INSTANCE;
}
java中可以使用静态内部类,实现不加锁写法:
public class Singleton {
private Singleton() {}
private static class Holder { //由静态内部类持有单例对象,但是根据类加载特性,我们仅使用Singleton类时,不会对静态内部类进行初始化
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){ //只有真正使用内部类时,才会进行类初始化
return Holder.INSTANCE; //直接获取内部类中的
}
}
原型模式
原型模式与对象的拷贝紧密相关
深拷贝和浅拷贝
- 浅拷贝: 对于类中基本数据类型,会直接复制值给拷贝对象;对于引用类型,只会复制对象的地址,而实际上指向的还是原来的那个对象
- 深拷贝: 无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也会进行拷贝。