首页 > 技术文章 > Java:面向抽象编程实现OCP

wushaopei 2022-02-28 23:36 原文

title: Java:面向抽象编程实现OCP
copyright: true
tags: 单例模式
categories:

涉及技术基础点:

  1. 反射
  2. 泛型
  3. 抽象
  4. 工厂模式
  5. interface

一、代码优化原则

  1. 代码复杂性的根本

代码编写的出发点:不啰嗦 自描述性的代码 可维护性 好代码

总结,所有软件的复杂性,都是为了可维护性。
  1. 开闭原则 OCP

    为什么java写出来的代码是可维护的,一个非常重要的原则就是开闭原则,也就是OCP。可能会听过里式替换原则,还有迪米特法则,这些都不是最重要的,最重要的是开闭原则,这是代码可维护的基础。其它的原则都是开闭原则的子原则,其他的原则都是为了实现开闭原则。

除了里式替换原则,迪米特法则,还有控制反转,依赖注入,其实也是为了实现开闭原则。

OCP的英文:Open Closed Principle。

不管是软件,还是函数,还是类,都要对扩展是开放的,对修改是封闭的。

当要修改的时候,我们通过新增一业务模块或类,来代替原来的类

注意: 不要面对一个具体的类去编程,要面向抽象。

示例:

public class A{
    private C c ;
    
    public void print(){
        C c = new C();
            c.text1();
        System.out.println("This is Class A");
    }
  1. 为什么要面向抽象编程?

interface abstract

使用抽象接口,可以避免因新增实现类而改动控制代码。

因为如果面向实体类去新建对象,并调用方法,在后续维护(指出现新需求)的时候,改动一个类,会影响到其他依赖此类的类做出改动 new语句以及调用方法的语句,这将是灾难性的修改。

  1. 如何能够忽略掉一个具体类,而面向抽象编程?

就是用的interface(接口)

IOC DI 的过程:

  1. interface
  2. 设计模式:工厂模式
  3. IOC,DI

目的:

=》面向对象 =》OCP =》可维护的代码

二、代码优化之路(从强耦合到稳定)

创建 Diana、Irelia、Camille三个类,分别有多个相同的方法可供调用,例子:

public class Irelia {
    public void g(){
        System.out.println("Irelia Q");
    }

    public void w(){
        System.out.println("Irelia W");
    }

    public void e(){
        System.out.println("Irelia E");
    }

    public void r() {
        System.out.println("Irelia R");
    }
}
  1. 案例代码一:

多个类创建多个对象来调用各自的方法,耦合度高,当有新的类加入时,需要更改main中的代码;

   public static void main(String[] args) {
        String name = Main.getPlayerInput();
        switch (name) {
            case "Diana":
                Diana diana = new Diana();
                diana.r();
            case "Irelia":
                Irelia irelia = new Irelia();
                irelia.r();
            case "Camille":
                Camille camille = new Camille();
                camille.r();
        }
    }

当前的案例中,每个方法的调用需要分别使用所属类的实例去调用,代码的耦合度太高,变更太频繁;

2.案例代码二:

使用 interface 来统一调用方法,这里创建 ISkill 接口,Diana、Irelia、Camille实现该接口;

    public static void main(String[] args) throws Exception {
        String name = Main.getPlayerInput();
        ISkill iSkill;
        switch (name) {
            case "Diana":
                iSkill = new Diana();
                break;
            case "Irelia":
                iSkill = new Irelia();
                break;
            case "Camille":
                iSkill = new Camille();
                break;
            default:
                throw new Exception();
        }
        iSkill.r();
    }

从以上的代码中,可以看出,确实在原有代码上进行了优化。

但是:

  • 单纯的interface可以统一方法的调用,但是他不能统一对象的实例化

  • 面向对象在做两件事情
    a)实例化对象
    b)调用方法(完成业务逻辑)

  • 只有一段代码中没有new的出现,才能保证代码的相对稳定,才能逐步实现OCP

  • 上面这句话的只是表象,实质是一段代码如果要保持稳定,就不应该负责对象的实例化

  • 对象实例化的过程不可能消除掉

  • 把对象实例化的过程,转移到其他的代码片段里

    方法统一的意义在哪?
    1、某方法被大量调用时很繁琐,方法统一后一行代码搞定;
    2、事项功能的单一性,便于后面的提取封装

多个类实现同一个接口,通过反射与多态的方式进行向上转型,从而可以使用一个统一的出口进行方法的调用。代码的耦合度较低,只需要更改要实例化的对象,对于具体调用方法的实例本身是不变的。

  1. 案例代码三:

在案例代码二的基础上,将主要进行 new 的switch-case 代码进行抽取,通过 Factory 的方式来生成具体的实例:

public class HeroFactory {

    public static ISkill getHero(String name) throws Exception {
        ISkill iSkill;
        switch (name) {
            case "Diana":
                iSkill = new Diana();
                break;
            case "Irelia":
                iSkill = new Irelia();
                break;
            case "Camille":
                iSkill = new Camille();
                break;
            default:
                throw new Exception();
        }
        return iSkill;
    }
}

Main.java

    public static void main(String[] args) throws Exception {
        String name = Main.getPlayerInput();
        ISkill iSkill = HeroFactory.getHero(name);
        iSkill.r();
    }

工厂类的作用就是负责创建类的实例,而不影响主业务代码,主业务代码甚至不需要知道工厂类的内部实现,满足了功能也实现了代码分离

当前方式的优化:

  1. 代码中总会存在不稳定,所以我们要做的就是隔离不稳定,保证其他的代码是稳定的。
  2. 把散落在各处的不稳定性全部抽取和集中起来管理,使得绝大多数代码保持稳定,这就是IOC的意义。

弊端:

   当前的代码中存在的不稳定是变化,工厂模式类 HeroFactory 依旧存在 new 这一个过程。
  1. 案例代码四:

基于反射 new 对象,消除变化

public class HeroFactory {

    public static ISkill getHero(String name) throws Exception {
        ISkill iSkill;

        //反射
        //元素
        // C# 、Python
        // 类 是对象的抽象
        // 对象 类 元素
        // reflect.hero.Irelia
        String classStr = "reflect.hero." + name;
        Class<?> cla = Class.forName(name);
        Object obj = cla.newInstance();
        return (ISkill)obj;
    }
}

反射机制,一般情况,我们可以通过用户输入的名字去new一个对象,但是,如果用反射机制,可以根据用户输入的名字,去创建一个对象,注意:使用反射机制接收到的参数是要求是完整的包路径。       

知识点:

java9以上废弃了newInstance,由clazz.getDeclaredConstructor().newInstance()代替

结束: 到这一步,代码基本上就消除了变化。

现在的写法有一定的缺点:

每一次调用都需要重新反射生成对象,性能不好;

而spring是可以把生成的对象加到缓存里面,就不再重复生成;

Spring的配置文件变更属不属于违反OCP原则:

配置文件是属于系统外部的,而不是代码本身的,不违反OCP原则。

面向对象中变化的应对方案?有两种

1.制定一个interface,然后用多个类实现同一个interface ---策略模式

2.一个类,属性 配置类中配置变化的属性值--- 修改数据库链接、端口号 。

更改配置文件,在配置文件中更改,比如修改数据库链接、端口号 支付账号等。
前者更灵活,后者属性值是固定的,扩展性没有第一种强

推荐阅读