侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 297 篇文章
  • 累计创建 134 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Java进阶知识点5:白捡的扩展性 - 枚举值也是对象

孔子说JAVA
2022-05-24 / 0 评论 / 0 点赞 / 87 阅读 / 3,615 字 / 正在检测是否收录...

1、背景

枚举经常被大家用来储存一组有限个数的候选常量。比如下面定义了一组常见数据库类型:

public enum DatabaseType {
    MYSQL,
    ORACLE,
    SQLSERVER
}

当围绕这一组常量出现功能上的扩展点时,很多人的做法是为新的功能编写一个新类,新类中依赖该枚举类型。

比如要在界面上显示常见数据库类型的官方名称,可以用如下类实现这一功能:

public class DatabaseNameParser {
    public String getDatabaseName(DatabaseType databaseType) {
        if (DatabaseType.ORACLE.equals(databaseType)) {
            return "Oracle数据库";
        } else if (DatabaseType.MYSQL.equals(databaseType)) {
            return "MySQL数据库";
        } else if (DatabaseType.SQLSERVER.equals(databaseType)) {
            return "SQL Server数据库";
        } else {
            throw new RuntimeException("不支持的数据库类型。");
        }
    }

    public static void main(String[] args) {
        System.out.println(new DatabaseNameParser().getDatabaseName(DatabaseType.MYSQL));
    }
}

大量的if - else语句以及对其他类的过渡依赖(几乎每两行代码就会引用一次DatabaseType对象),让上面这段代码散发出浓浓的坏味道,你可能会想如果这些逻辑可以整合在枚举类中实现就好了。答案是当然可以。

我们只需意识到枚举值不只是常量值,枚举值也是一种对象,他几乎拥有面向对象编程的绝大部分功能 – 封装、多态、继承(不支持,但一定程度上可以模拟类似效果)。

2、用枚举进行面向对象编程

2.1 封装

上面的例子中,数据库类型的官方名称应该封装到每个数据库类型枚举对象中,作为一个属性字段,每个枚举值提供get方法即可直接通过枚举对象获取其对应的官方名称。如下:

public enum DatabaseType {
    MYSQL("MySQL数据库"),
    ORACLE("Oracle数据库"),
    SQLSERVER("SQL Server数据库");

    private final String name;

    DatabaseType(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static void main(String[] args) {
        System.out.println(MYSQL.getName());
    }
}

2.2 多态

上面的例子中,比如新增一个需求,要在界面上提供数据库的连接检测功能。而不同数据库类型的连接检测逻辑是不同的。此时可以为枚举对象声明一个抽象方法,每个枚举值负责实现此方法。如下:

public enum DatabaseType {
    MYSQL {
        @Override
        public boolean detect(String ip, int port) {
            //为了简化举例环境,此处不真正实现功能
            return false;
        }
    },
    ORACLE {
        @Override
        public boolean detect(String ip, int port) {
            return false;
        }
    },
    SQLSERVER {
        @Override
        public boolean detect(String ip, int port) {
            return false;
        }
    };

    public abstract boolean detect(String ip, int port);

    public static void main(String[] args) {
        System.out.println(MYSQL.detect("127.0.0.1", 3306));

    }
}

这一特性也常被称之为:定义常量相关方法。

2.3 继承

枚举对象均是final对象,故不支持枚举对象的继承。但我们可以通过实现接口来模拟枚举类型的子类化,这也是子类化枚举类型的唯一方法。如下:

interface DatabaseType {
    public boolean detect(String ip, int port);

    enum RDBMS implements DatabaseType {
        MYSQL {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        },
        ORACLE {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        },
        SQLSERVER {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        }
    }

    enum NOSQL implements DatabaseType {
        REDIS {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        },
        HBASE {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        },
        MONGODB {
            @Override
            public boolean detect(String ip, int port) {
                return false;
            }
        }
    }
}

这就模拟了DatabaseType派生出RDBMS和NOSQL两个抽象子类,每个子类下又派生出各自的具体实现类,这种继承结构。

3、用枚举更好地实现设计模式

当枚举值被赋予对象的强大能力后,再结合枚举类型本身的易用性,被聪明的Java开发者们发现了如下两类好用的模式:

3.1 使用枚举定义单例

这是JDK5之后定义单例最好的方法,没有之一。

说起单例模式,常常让我们想起很多技巧或问题:

  1. 延迟初始化单例要Double Check,Double Check会失效需要使用volatile修饰符;
  2. 可以使用静态变量初始化单例;
  3. 单例对象可能可以被多次反序列化,就违背了单例模式的要求了;
    … …

现在不用纠结这些过时的套路了,就按如下方式定义单例,任何已知的问题都没有,一行代码搞定,而且引用起来更紧凑简洁。

public enum Singleton {
    INSTANCE;

    public void anyFunction() {
    }

    public static void main(String[] args) {
        Singleton.INSTANCE.anyFunction();
    }
}

3.2 使用枚举实现责任链或状态机

public enum StateMachine {
    BEGIN {
        @Override
        public StateMachine handleAndReturnNextState() {
            System.out.println("begin");
            return PROCESS1;
        }
    },
    PROCESS1 {
        @Override
        public StateMachine handleAndReturnNextState() {
            System.out.println("process1");
            if (true/*some condition*/) {
                return PROCESS2;
            }
            return END;
        }
    },
    PROCESS2 {
        @Override
        public StateMachine handleAndReturnNextState() {
            System.out.println("process2");
            return END;
        }
    },
    END {
        @Override
        public StateMachine handleAndReturnNextState() {
            System.out.println("end");
            return null;
        }
    };

    public abstract StateMachine handleAndReturnNextState();

    public static void main(String[] args) {
        StateMachine state = BEGIN;
        while (state != null) {
            state = state.handleAndReturnNextState();
        }
    }
}

4、总结

  1. 枚举不只是常量,也是对象,当与枚举相关的功能遇到扩展需求时,可以考虑在枚举对象上扩展功能点,以获得更加简洁紧凑的代码。
  2. 枚举是实现单例的最好方式。
  3. 枚举可以用来实现状态机。
0

评论区