文章目录
开闭原则
对扩展开放,对修改关闭
例子
其中AbstractSkin
是抽象类,皮肤类:
package com.linghu.demo01;
/**
* 抽象皮肤类
* @author linghu
* @date 2024/2/1 15:42
*/
public abstract class AbstractSkin {
//展示皮肤的方法
public abstract void display();
}
另外的DefaultSpecificSkin
、HeimaSpecificSkin
、SouGouInput
三个是子类,不动这三个子类的情况下,自己新建皮肤类,然后在下面代码【2】处进行更改,实现良好的扩展性。
Test
类如下:
package com.linghu.demo01;
/**
* @author linghu
* @date 2024/2/1 15:58
*/
public class Client {
public static void main(String[] args) {
//1、创建搜狗输入法对象
SouGouInput input = new SouGouInput();
//2、创建皮肤对象
HeimaSpecificSkin skin = new HeimaSpecificSkin();
// DefaultSpecificSkin skin = new DefaultSpecificSkin();
//3、将皮肤设置到输入法中
input.setSkin(skin);
//4、显示皮肤
input.display();
}
}
总结
对扩展开放,对修改关闭的含义就是: 我们可以在定义扩展其他皮肤类【2】,只要在上述代码中新建对象即可。不需要修改原有的DefaultSpecificSkin
、HeimaSpecificSkin
、SouGouInput
三个子类。
里氏代换原则
子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除了添加新的方法完成额外功能外,尽量不要重写父类的方法。
基本介绍
- 里氏替换原则在1988年,有麻省理工学院的以为姓里的女士提出的。
- 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得T1定义的所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所欲引用基类的地方必须能透明的使用其子类的对象。
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
- 继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题
例子
为遵循里氏替换原则的后果:
package com.weirdo.principle.liskov;
public class Liskov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3="+a.func1(11,3));
System.err.println("==========================");
B b = new B();
//这里本意求11-3
System.out.println("11-3="+b.func1(11,3));
System.out.println("1-8="+b.func1(1,8));
System.out.println("11+3+1="+b.func2(11,3));
}
}
//A类
class A {
//返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
//B类继承了A
//增加了一个新功能:完成两个数相加,然后和1求和
class B extends A {
//这里,重写了A类的方法,可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return a + b + 1;
}
}
依赖倒转原则
基本介绍
高层模块不应该依赖底层模块,两者都应该依赖其抽象;这里的抽象就是指:接口! 抽象不应该依赖细节,细节应该依赖抽象,简单地说就是要求对抽象(接口)进行编程,不要对实现进行编程,这样就降低了客户与实现模块之间的耦合。
例子
如下图所示,扩充的IntelCpu
类没法更换成其他的CPU类,就很不方便
在这个Computer
类中,如果我们要添加一个CPU的话,就要修改Computer
类,这就会导致臃肿麻烦。违背了开闭原则!
接下来使用依赖倒转原则,就是把类抽取出接口:
如上做的好处就是,如果有其他品牌的CPU加入,我们只需要在实现类旁边进行扩充,去实现接口即可!
接口隔离原则
基本介绍
**客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。 **
例子
上面的代码设计如下图:
这种设计的缺陷在于,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:
改进以后的代码:
package com.linghu.demo04.after;
/**
* 新品牌门的加入
* @author linghu
* @date 2024/2/4 9:57
*/
public class NewDoor implements AntiTheft,Fireproof{
@Override
public void antiTheft() {
System.out.println("防盗");
}
@Override
public void fireproof() {
System.out.println("防火");
}
}
新品牌的门的加入就不会在影响之前的门的牌子类了。这就是接口隔离的好处。
迪米特法则
基本介绍
**其实就是类似于Java中的代理,或者直接点就是外包公司!。**主打的是一种办事情的流程,这种方便的流程。
为什么要找软件公司呢?这个外包公司可以帮助程序员和客户省去交流的成本,沟通的成本,提高效率!
例子
package com.linghu.demo05;
/**
* @author linghu
* @date 2024/2/4 11:12
*/
public class Client {
public static void main(String[] args) {
//创建经纪人类
Agent agent = new Agent();
//创建明星类
Star star = new Star("李小龙");
agent.setStar(star);
//创建粉丝类
Fans fans = new Fans("令狐");
agent.setFans(fans);
//创建公司对象
Company company = new Company("好莱坞");
agent.setCompany(company);
//开会和洽谈业务~
agent.meeting();
agent.business();
}
}
合成复用原则
基本介绍
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
例子
创建者模式
基本介绍
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
创建型模式分为:
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
单例设计模式
饿汉式-方法1:静态变量
package com.linghu.demo06.pattern;
/**
* 饿汉式
* 静态变量-创建类对象
* @author linghu
* @date 2024/2/4 14:41
*/
public class Singleton {
//1、私有化构造器,防止外部创建该类对象,只能让外部通过get方法访问本类
private Singleton(){
}
//2、在成员位置创建该类对象
private static Singleton instance=new Singleton();
//3、对外提供静态方法获取该类对象
public static Singleton getInstance(){
return instance;
}
}
饿汉式-方法2:静态代码块
这个方法跟上面的方法其实差不多。
package com.linghu.demo06.pattern;
/**
* 饿汉式
* 静态变量-创建类对象
* @author linghu
* @date 2024/2/4 14:41
*/
public class Singleton {
//1、私有化构造器,防止外部创建该类对象,只能让外部通过get方法访问本类
private Singleton(){
}
//2、在成员位置创建该类对象
private static Singleton instance;//null
static {
instance=new Singleton();
}
//3、对外提供静态方法获取该类对象
public static Singleton getInstance(){
return instance;
}
}
总结
以上 Singleton类保证了只能对外创建一个对象。
懒汉式-方式1(线程不安全)
这个方式就是用饿汉式的第一种方式进行调整就可以了:
public class Singleton {
//1、私有化构造器,防止外部创建该类对象,只能让外部通过get方法访问本类
private Singleton(){
}
//2、在成员位置创建该类对象
private static Singleton instance;
//3、对外提供静态方法获取该类对象
public static Singleton getInstance(){
//instance=new Singleton();//在这里做了调整,使用该对象的时候再调用,就不会浪费了
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
如上的代码中,我们加入了一个if
判断,这里的操作叫做 懒加载!
在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制,也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用。这个机制在单线程环境下的实现非常简单,然而在多线程环境下却存在隐患。
懒汉式-方式2(线程安全)
在上面懒加载的情况下加入一个 synchronized
同步锁就可以了:
package com.linghu.demo06.pattern01;
/**
* @author linghu
* @date 2024/2/4 17:01
*/
public class Singleton {
//1、构造器私有化
private Singleton(){
}
//2、创建类对象
private static Singleton instance;
//3、对外提供静态方法获取该对象
public static synchronized Singleton getInstance(){
if (instance==null){//当线程1进入以后,先判断,线程2在方法外面等待
//等待到线程1对象创建或者返回完毕释放锁以后,线程2才开始进入
instance=new Singleton();
}
return instance;
}
}
**该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。 **
懒汉式-方式3(双重检查锁)
**懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式 **
package com.linghu.demo07;
/**
* 双重检查方式
* @author linghu
* @date 2024/2/5 10:33
*/
public class Singleton {
//1、私有构造器
private Singleton(){}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance(){
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if (instance==null){
synchronized (Singleton.class){
//第二次判断,抢到锁以后是否为null
if (instance==null){
instance=new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。
懒汉式-方式4(静态内部类)
**静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被static 修饰,保证只被实例化一次,并且严格保证实例化顺序。 **
package com.linghu.demo08;
/**
* @author linghu
* @date 2024/2/5 11:21
*/
public class Singleton {
//1、私有化构造器
private Singleton(){}
//2、静态内部类提供实例
private static class SingletonHolder{//这个类只能内部用,外界访问不了
//调用getInstance方法的时候才能初始化INSTANCE,而且只会被调用一次
private static final Singleton INSTANCE=new Singleton();
}
//3、对外提供静态方法获取该对象
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
枚举方式-饿汉式(不考虑内存空间)
**枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。 **
package com.linghu.demo09;
/**
* 枚举方式
* @author linghu
* @date 2024/2/5 11:34
*/
public enum Singleton {
INSTANCE;
}
JDK源码解析-RunTime类的单例设计模式
在jdk源码中,RunTime类的设计其实就用到了单例设计模式中的恶汉式来实现的。
如上可以看出代码中提供了私有化构造器、静态访问对象的方法、静态变量对象:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
工厂模式
在Java中万物皆对象,创建对象通过都是new出来的,但这么干会出现一些问题:比如说,当我们需要更换对象的时候,所有new对象的地方我们都需要修改一遍,这就违背了软件设计的 开闭原则!
引入工厂模式的目的是为了:解耦!
也就是说,我们可以利用工厂来生产对象,我们开发者只需要和工厂打交道即可,彻底和对象解耦!
三种工厂:
- 简单工厂模式(不属于GOF的23种经典设计模式)
- 工厂方法模式
- 抽象工厂模式
引例
package com.linghu.demo10;
/**
* @author linghu
* @date 2024/2/5 16:20
*/
public abstract class Coffee {
//获取咖啡的名字,定义成抽象方法
public abstract String getName();
public void addMilk(){
System.out.println("加奶~");
}
public void addSugar(){
System.out.println("加糖~");
}
}
package com.linghu.demo10;
/**
* @author linghu
* @date 2024/2/5 16:31
*/
public class CoffeeStore {
//根据不同的咖啡类型生成并返回不同的咖啡对象
public Coffee orderCoffee(String type){
Coffee coffee=null;
if ("america".equals(type)){
coffee=new AmericaCoffee();
}else if ("latte".equals(type)){
coffee=new LatteCoffee();
}
//添加配料
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
package com.linghu.demo10;
/**
* 美式咖啡
* @author linghu
* @date 2024/2/5 16:24
*/
public class AmericaCoffee extends Coffee{
@Override
public String getName() {
return "美式咖啡";
}
}
package com.linghu.demo10;
/**
* @author linghu
* @date 2024/2/5 16:29
*/
public class LatteCoffee extends Coffee{
@Override
public String getName() {
return "拿铁咖啡";
}
}
package com.linghu.demo10;
/**
* @author linghu
* @date 2024/2/5 16:38
*/
public class Client {
public static void main(String[] args) {
//1、创建咖啡店类
CoffeeStore store=new CoffeeStore();
//2、点咖啡
Coffee orderCoffee = store.orderCoffee("america");
System.out.println(orderCoffee.getName());
}
}
简单工厂模式
简单工厂模式不是一种模式,而是一种编码习惯。
结构
简单工厂模式包含结构如下:
- 抽象产品:定义了产品的规范,描述了产品的主要特征和功能。
- 具体产品:实现或者继承抽象产品的子类。
- 具体工厂:提供创建产品的方法,调用者通过该方法获取产品。
实现
对上述咖啡案例的改进就在于: 要将咖啡店和咖啡产品进行解耦工作,引入工厂模式。
工厂类代码如下:
package com.linghu.demo10;
/**
* 这个工厂就负责制造咖啡,里面没有咖啡产品,你要替换咖啡
* 产品,不需要动这里的代码,这就实现了咖啡店和咖啡产品的解耦!
* 咖啡店只需要调用这个工厂就可以生产咖啡了,不需要关注咖啡产品了!
* @author linghu
* @date 2024/2/5 17:11
*/
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type){
Coffee coffee=null;
if ("americano".equals(type)){
coffee=new AmericaCoffee();
}else if ("latte".equals(type)){
coffee=new LatteCoffee();
}
return coffee;
}
}
法,调用者通过该方法获取产品。
实现
对上述咖啡案例的改进就在于: 要将咖啡店和咖啡产品进行解耦工作,引入工厂模式。
[外链图片转存中…(img-EbAXEhcI-1711960898263)]
工厂类代码如下:
package com.linghu.demo10;
/**
* 这个工厂就负责制造咖啡,里面没有咖啡产品,你要替换咖啡
* 产品,不需要动这里的代码,这就实现了咖啡店和咖啡产品的解耦!
* 咖啡店只需要调用这个工厂就可以生产咖啡了,不需要关注咖啡产品了!
* @author linghu
* @date 2024/2/5 17:11
*/
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type){
Coffee coffee=null;
if ("americano".equals(type)){
coffee=new AmericaCoffee();
}else if ("latte".equals(type)){
coffee=new LatteCoffee();
}
return coffee;
}
}
**后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。 **