前言
工作中遇到的业务越来越复杂,仅仅是实现功能已经远远不能满足要求。复杂的业务就需要合理的设计,使其变得更容易维护易懂。设计模式不是一个概念,而是一个经验之谈,如何对自己碰到的业务场景去套用一个设计模式,使得自己的代码更加的灵活。很多介绍设计模式的资料,只是生硬的说明什么设计模式,但是没有结合具体的场景,因此当我们学完一个设计模式后并不知道如何去应用到自己的需求场景中。因此笔者决定结合在工作中遇到的场景,写设计模式的系列文章,一方便帮助刚入门的同学理解以及如何应用设计模式,另一方面希望将自己对设计模式的理解分享给大家。
如果你够细心就会发现,代码来源于生活。现在这个社会不同的人有不同的岗位,分工越来越细化,为了追求更大效率与效益。而所谓的设计模式也是模仿现有社会中不同的分工来设计不同的模块,追求开发过程中的可维护,高效率。
简单工厂模式
工厂模式顾名思义,工厂是用来加工同一种产品,汽车加工厂、服饰工厂等。工厂的一个特点是,提供了一个通用的工艺制作流程,能够批量的生产产品。工厂是一个封装的实体,你不需要关注里面如果制造产品的,只需要关注生产出的产品。
数据平台
随着业务越来越复杂,很多公司都在往微服务上转,微服务的话很多都是只暴露一个 Rest API 接口,通过HTTP去调用,然后返回结果。不同的微服务是不同的数据平台向外暴露数据的入口。
假设有HBase,Hive,MySQL,Redis这四种数据平台,每种平台都提供了相应的 Rest API ,方便获取数据。有个业务需要这四种平台上的数据,Rest API 如下:
- www.love.code.com/hbase/_sear…
- www.love.code.com/hive/_searc…
- www.love.code.com/mysql/_sear…
- www.love.code.com/redis/_sear…
笔者刚开始图省事,把四种获取数据的方式都整合到一起,其中针对获取的不同的数据,还需要为每个URL传递不同的参数,数据返回回来后需要各种不同的处理逻辑。
笔者下面这种写法对于当前类来说很简单,我对不同的URL 传递了不同的参数,返回数据之后,我组合并处理不同的数据。
class GetData{
public static final String HBASE_URL="https://www.love.code.com/hbase/_search"
public static final String HIVE_URL="https://www.love.code.com/hive/_search"
public static final String MYSQL_URL="https://www.love.code.com/mysql/_search"
public static final String REDIS_URL="https://www.love.code.com/redis/_search"
........
public void processLogic(){
CustomTypeA hbase= HttpClient.get(HBASE_URL).add(requestParametersA);
CustomTypeB hive=HttpClient.get(HIVE_URL).add(requestParametersB);
CustomTypeC mysql=HttpClient.get(MYSQL_URL).add(requestParametersC);
CustomTypeD redis=HttpClient.get(REDIS_URL).add(requestParametersD);
.....
// TODO: 2021/3/24 start to combine and process different data
.....
}
}
这种写法的弊端体会到了吗?如果你又需要在另外的类中获取数据呢?是不是要把获取数据的逻辑全部照搬过去?又或者说现在又新加了一个数据平台ELK,你是不是又得去各个类中去添加修改?
假设有四个类都用到了Hbase的数据接口,Hbase 接口升级后请求参数改变了,那是不是要同时修改四个类?这真的很让人崩溃,不是吗?
简单工厂模式重构
这里可以用工厂模式重构一下,把不同的Client数据平台,抽象成不同的产品,我们需要一个Client 工厂来管理生产这些Client产品。
interface Client{
HttpClient getClient(Param params);
}
class HbaseClient implements Client{
public static final String HBASE_URL="https://www.love.code.com/hbase/_search"
public CustomTypeA getClient(Param params){
return HttpClient.get(HBASE_URL).add(params);
}
}
class HiveClient implements Client{
public static final String HIVE_URL="https://www.love.code.com/hive/_search"
public CustomTypeB getClient(Param params){
return HttpClient.get(HIVE_URL).add(params);
}
}
class MysqlClient implements Client{
public static final String MYSQL_URL="https://www.love.code.com/mysql/_search"
public CustomTypeC getClient(Param params){
return HttpClient.get(MYSQL_URL).add(params);
}
}
class RedisClient implements Client{
public static final String REDIS_URL="https://www.love.code.com/redis/_search"
public CustomTypeD getClient(Param params){
return HttpClient.get(REDIS_URL).add(params);
}
}
class ClientFactory{
public Client getRequestClient(String type){
switch (type) {
case "HBASE":return new HbaseClient();
case "HIVE":return new HiveClient();
case "MYSQL":return new MysqlClient();
case "REDIS":return new RedisClient();
}
}
}
那么此刻我们的GetData类该如何写呢?
class GetData{
public void processLogic(){
CustomTypeA hbase=ClientFactory.getRequestClient("HBASE").getClient(requestParametersA);;
CustomTypeB hive=ClientFactory.getRequestClient("HIVE").getClient(requestParametersB);
CustomTypeC mysql=ClientFactory.getRequestClient("MYSQL").getClient(requestParametersC);
CustomTypeD redis=ClientFactory.getRequestClient("REDIS").getClient(requestParametersD);
.....
// TODO: 2021/3/24 start to combine and process different data
.....
}
}
是不是觉得GetData类突然变的清爽了很多,如果此刻某个数据平台的API发生变化,是不是只需要到对应的数据API类中修改就可以了,只需要修改一遍。有没有觉得维护起来方便了很多。
UML类图如下,通过UML类型更能够看清不同类直接的关系,绿线代表实现接口关系,灰色的线代表依赖关系。具体其他线的定义,可以查看相关UML类型的定义。
这里ClientFactory中,封装了Client具体实现,将类的实现操作留给Factory,客户端只需要传参调用就行,不需要关心具体如何实现。(其实封装代码也有个小技巧,就是封装变化,这里不同的产品是变化的,那么只需要将产品封装到工厂就可以了---总结一句话,封装变化)这里工厂获取不同的客户端,使用了硬编码,这是不好的编程习惯,如何去改进呢?一是可以考虑写个枚举,传入枚举。但是使用枚举有个特点,就是假装枚举了所有类型,枚举一般设计成不轻易改变的类型。因为枚举跟ClientFactory的switch 代码高度的耦合,如果以后枚举类型改变了,这个工厂内部也需要改变。这就很让人头疼了,如何再进一步改进呢?这里关键在于switch 中的代码。switch中包好不可变的硬编码,因此只要把switch代码优化即可。
反射+工厂模式重构
Java提供了一种反射机制,传入Class类型,不通过new的方式实例化,通过反射实例化类型
优化 ClientFactory 工厂
class ClientFactory{
public Client getRequestClient(Class clientClazz)throws IllegalAccessException, InstantiationException{
Object client= clientClazz.newInstance();
return (Client)client;
}
}
这里实例化后的对象是Object,需要强制转换,这么做不是很安全,代码容易出问题,因为你不知道用户会传个什么Class类型过来,因此还需要继续优化,加上泛型。
但是设置什么类型呢?Client吗?如果写成Client,那传入HbaseClient.class就会报错,这里也有个小知识点,泛型限定。泛型限定分为通配符上限定,与下限定。
具体的就是<? extends T> 与 <? Super T>。这里我们将使用<? extends T> 代表,只能够传递T类型的子类型。具体实现如下:
class ClientFactory{
public Client getRequestClient(Class<? extends Client> clientClazz)throws IllegalAccessException, InstantiationException{
Client client= clientClazz.newInstance();
return client;
}
}
此刻客户端代码变成什么样子了呢?
class GetData{
public void processLogic(){
CustomTypeA hbase=ClientFactory.getRequestClient(HbaseClient.class).getClient(requestParametersA);;
CustomTypeB hive=ClientFactory.getRequestClient(HiveClient.class).getClient(requestParametersB);
CustomTypeC mysql=ClientFactory.getRequestClient(MysqlClient.class).getClient(requestParametersC);
CustomTypeD redis=ClientFactory.getRequestClient(RedisClient.class).getClient(requestParametersD);
.....
// TODO: 2021/3/24 start to combine and process different data
.....
}
}
是不是觉得 so amazing ?就算以后扩展了新的Client 也不需要修改客户端以及工厂代码。
重构后的UML类图是不是更清晰了,ClientFactory不在与其他的具体Client关联,只有Client接口关联,这就是ClientFactory不在依赖具体实现类,解耦的过程。
有没有觉得代码很眼熟,似曾相识。没错,就是我们经常用的Java Log 日志系统的写法。我们需要使用日志系统的时候,一般都是在类中加入这么一行代码。将具体类的class类型传给LoggerFactory 工厂,工厂将会返回一个日志实体类,用来记录这个类的输出日志。
public static Logger logger= LoggerFactory.getLogger(Console.class);
以上就是笔者对工厂设计模式的介绍,希望你能够细心的理解上面的重构过程,不难发现设计模式一直在我们身边,如果你够仔细就能够发现它。将自己所学应用到实践是一件很令人兴奋的事不是吗?如果读者以后在工作过程中遇到了工厂模式的场景,希望能够回头来评论或点赞,您的支持是我继续码字的动力,下篇文章将会结合工作中遇到的场景来介绍另一种设计模式。