【Java】代码结构设计思考 阳光穿透心脏的1/2处 2022-04-06 05:42 213阅读 0赞 ## 背景 ## 这篇博文是博主在做数据图形统计相关接口工作过程中对代码结构设计的一些思考总结,仅代表个人观点。 ### 1.需求简述 ### 提供资金关系数据图形统计,根据不同菜单地址跳转至对应图形页面显示相关业务统计数据。 ### 2.开发设计过程 ### **2.1** 在开发的初期,考虑到各个数据图形统计具体实现细节的不同,以及代码的可扩展性(非全面),博主是这样设计的(忽略网络请求的复杂细节以及框架细节)。 ## ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70][] 图1 初期结构设计图 ## 服务端提供不同的请求地址给客户端调用,如图中的 `/a`, `/b`, `/c`, 每种请求地址对应一种数据图形的统计。后续各自的`service`实现各自图形统计的具体逻辑,互不干扰。当我们新增一种图形统计的类型时,只要对应的增加`请求地址/d`、`图形D Controller`、`图形D Service`,不会影响其他图形数据的统计。后续的增加以此类推。 **2.2** 初期开发完成后,博主发现虽然各图形`service`的具体实现逻辑不同,但是对于提供给对应`controller`的`api`都是一样的,而且只需对外提供一个请求链接并用特定字段标记就可以区分,于是对于图1中的结构进行修改。 ## ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 1][] 图2 改进结构设计图 ## 服务端仅提供一个请求入口地址`/statistics`,同时用`type`字段来进行区分当前我们所需要处理的是哪种图形数据的统计,并调用对应的`service`服务进行统计处理。 这种设计与图1的设计相比而言有以下好处: 1. 减少了不同种类的`图形Controller`,减少了请求入口地址 2. 在`图形Controller`与各个`图形Service`之间新增了一层`图形抽象Service`(可以是抽象类、接口),统一了对外提供访问的`api` 到这一步,其实就有点我们设计模式当中策略模式的意思了。图2中的`图形抽象Service`可以理解为策略模式当中的抽象策略类,而我们具体的`图形A Service`、`图形B Service`、`图形C Service`也可以与策略模式中的具体策略类等同。从而使得图形统计的逻辑可以独立于调用方变化。 接下来在看看上层`图形Controller`的方法该如何写。我们需要根据`type`值的不同来调用我们的对应的`图形Service`的方法,这又不得不使用我们的工厂模式,因为我们先要根据`type`值来获取对应的`图形Service`的对象。大致的伪代码如下(这里忽略Controller方面的细节): ..... 图形抽象Service; switch (type) { case "a": 图形抽象Service = 图形AService对象; break; case "b": 图形抽象Service = 图形BService对象; break; case "c": 图形抽象Service = 图形CService对象; break; default: // 异常 break; } 图形抽象Service.公共api; ..... ## 设计改进 ## 当然, **2.2** 设计中创建对象的操作我们完全可以交由`spring`去执行(通过xml或者注解的方式注入),也就是让`spring`来管理`bean`,但我们仍无法摆脱使用`switch`或者`if else`来区分类型。新增或者减少类型,我们需要改动原有方法:增加或者减少对应`switch`或者`if else`分支。 举个例子,针对 **2.2** 中的代码,假如我们要新增一类图形D的数据统计,那怎么办? 我们当然会这样做:在实现`图形D Service`数据统计逻辑的同时,再在上述代码中新增`case "d"`的分支。 新增图形D的数据统计类是没什么问题的,但是对于`getResult`这个方法来说就有点违背设计模式六大原则中的开闭原则,因为其不仅对扩展开放了,而且对修改也开放了。 所以博主针对这块,再做一小改进。 回到前面`spring`管理`bean`上,看过`spring`源码的同学应该会更加清楚:在我们的`spring`容器当中,`bean`是存储在我们的`map`当中的,并以`id`为`key`值,`bean实例`为`value`值。那也就是说,假如我们拥有这样一个`map`:以标记字段值为`key`值,`bean实例`为`value`值,那我们就可以通过传入的标记字段值(**2.2**中的`type`)直接获取实例`bean`并调用对应的公共方法。 ## 代码实现 ## 由于上述例子涉及相关业务,相对繁琐,为简单起见,博主以计算几种几何图形为周长面积为例,用`square`、`triangle`、`circular`作为标记字段`type`的值,分别对应正方形、全等三角形、圆。 以下列出实现的主要代码。 ### 1.几何图形Service服务管理类 ### 主要用来自己管理`bean`,指定规则`key`的值。 /** * 几何图形Service服务管理类 * Created by Hilox on 2018/12/11 0011. */ @Service public class GeometryServiceManager { /** * 管理实现类 */ private static Map<String, Class<?>> initialCategories = new ConcurrentHashMap<>(); private static Map<String, GeometryAbstractService> categoryMap = new ConcurrentHashMap<>(); /** * 默认构造方法 */ public GeometryServiceManager() { } /** * 子类自注册方法 */ public GeometryServiceManager(String name, Class<?> serviceName) { initialCategories.put(name, serviceName); } /** * 根据传入标记字段值获取对应的几何图形服务 * @param name * @return */ public GeometryAbstractService getServiceByName(String name) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException("Type不能为空!"); } // 扫描预置的实现类 scanForService(); if (categoryMap.containsKey(name)) { return categoryMap.get(name); } else { throw new IllegalArgumentException("对应几何图形服务未注册!"); } } /** * 扫描几何图形服务注册实现类 * 为确保获取正确服务, 需要调用scanForService方可正确注册 */ private void scanForService() { initialCategories.forEach((key, value) -> registryService(key, (GeometryAbstractService) ApplicationContextHelper.getApplicationContext().getBean(value), true)); } /** * 几何图形服务注册方法 * 新增几何图形服务方法, 则将其注入到集合中 * @param name * @param geometryAbstractService * @param replace 同名是否替换现有的服务, 默认为false:不替换 */ private void registryService(String name, GeometryAbstractService geometryAbstractService, boolean replace) { if (!categoryMap.containsKey(name) || replace) { categoryMap.put(name, geometryAbstractService); } } /** * 根据名称卸载几何图形服务方法 * @param name */ private void unRegistryService(String name) { if (categoryMap.containsKey(name)) { categoryMap.remove(name); } } /** * 根据方法卸载几何图形服务方法 */ private void unRegistryService(GeometryAbstractService geometryAbstractService) { if (categoryMap.containsValue(geometryAbstractService)) { categoryMap.remove(geometryAbstractService); } } } ### 2.几何图形Service抽象父类 ### 作为所有几何图形Service的抽象父类,继承了我们自己定义的服务管理类。在这个抽象类中,主要用来写一些对外提供的`api`,以及所有子类涉及到的公共方法。 /** * 所有几何图形Service抽象父类 * Created by Hilox on 2018/12/11 0011. */ public abstract class GeometryAbstractService extends GeometryServiceManager { /** * 默认构造方法 */ public GeometryAbstractService() { } /** * 子类自注册方法 */ public GeometryAbstractService(String name, Class<?> serviceName) { super(name, serviceName); } /** * 获取几何图形周长 * @param condition * @return */ public abstract Object getPerimeter(GeometryCondition condition); /** * 获取几何图形面积 * @param condition * @return */ public abstract Object getArea(GeometryCondition condition); } ### 3.具体图形的Service实现 ### 具体图形Service继承了几何图形Service抽象父类,实现了其中的抽象方法的具体内容(当前也可以覆盖父类方法达到目的)。这里最主要的一点就是自注册,通过构造方法调用父类的构造方法,在通过父类构造方法的处理注册到我们的服务管理器类的静态容器当中,以便后续通过标记字段值提取指定服务。 #### 3.1正方形 #### /** * 正方形Service * Created by Hilox on 2018/12/11 0011. */ @Service public class SquareService extends GeometryAbstractService { /** * 自注册 */ public SquareService() { super(GeometryTypeConstant.SQUARE, SquareService.class); } @Override public BigDecimal getPerimeter(GeometryCondition condition) { BigDecimal side = condition.getSide(); if (side == null) { throw new IllegalArgumentException("正方形边长不能为空!"); } // 4 * side BigDecimal result = side.multiply(new BigDecimal(4)); return result; } @Override public BigDecimal getArea(GeometryCondition condition) { BigDecimal side = condition.getSide(); if (side == null) { throw new IllegalArgumentException("正方形边长不能为空!"); } // side * side BigDecimal result = side.pow(2); return result; } } #### 3.2等边三角形 #### /** * 全等三角形Service * Created by Hilox on 2018/12/11 0011. */ @Service public class TriangleService extends GeometryAbstractService { /** * 自注册 */ public TriangleService() { super(GeometryTypeConstant.TRIANGLE, TriangleService.class); } @Override public BigDecimal getPerimeter(GeometryCondition condition) { BigDecimal side = condition.getSide(); if (side == null) { throw new IllegalArgumentException("全等三角形边长不能为空!"); } // 3 * side BigDecimal result = side.multiply(new BigDecimal(3)); return result; } @Override public Double getArea(GeometryCondition condition) { BigDecimal side = condition.getSide(); if (side == null) { throw new IllegalArgumentException("全等三角形边长不能为空!"); } // 30° double radians = Math.toRadians(30); // 计算cos30° double cos = Math.cos(radians); double sideDouble = side.doubleValue(); // 高 double high = sideDouble * cos; return high * sideDouble / 2; } } #### 3.3圆形 #### /** * 圆形Service * Created by Hilox on 2018/12/11 0011. */ @Service public class CircularService extends GeometryAbstractService { /** * 自注册 */ public CircularService() { super(GeometryTypeConstant.CIRCULAR, CircularService.class); } @Override public Double getPerimeter(GeometryCondition condition) { BigDecimal radius = condition.getRadius(); if (radius == null) { throw new IllegalArgumentException("圆形半径不能为空!"); } // 2 * π * r double radiusDouble = radius.doubleValue(); return 2 * Math.PI * radiusDouble; } @Override public Double getArea(GeometryCondition condition) { BigDecimal radius = condition.getRadius(); if (radius == null) { throw new IllegalArgumentException("圆形半径不能为空!"); } // π * r * r double radiusDouble = radius.doubleValue(); return Math.PI * radiusDouble * radiusDouble; } } ### 4.请求地址入口Controller ### 通过以下代码可以看到,我们只需要通过标记字段`type`值来获取对应的服务进行后续的处理,摆脱了使用`switch`或者`if else`来区分类型。 /** * 几何图形Controller * Created by Hilox on 2018/12/11 0011. */ @Controller public class GeometryController { @Autowired private GeometryServiceManager geometryServiceManager; /** * 获取几何图形周长 * @param condition * @return */ @RequestMapping("/getPerimeter") @ResponseBody public Object getPerimeter(@RequestBody GeometryCondition condition) { GeometryAbstractService service = geometryServiceManager.getServiceByName(condition.getType()); return service.getPerimeter(condition); } /** * 获取几何图形面积 * @param condition * @return */ @RequestMapping("/getArea") @ResponseBody public Object getArea(@RequestBody GeometryCondition condition) { GeometryAbstractService service = geometryServiceManager.getServiceByName(condition.getType()); return service.getArea(condition); } } ## 源码传送门 ## **【源码地址】**:[struct-design-one][] ## 效果展示 ## 以计算图形周长为例: ## ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 2][] 图3 正方形计算结果图 ## ## ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 3][] 图4 全等三角形计算结果图 ## ## ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 4][] 图5 圆形计算结果图 ## [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70]: /images/20220406/9265d0b4f62c40baa865f26abbcc94f6.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 1]: /images/20220406/b5b44f29135c439db8d2695018dc2293.png [struct-design-one]: https://github.com/zhaohaihao/hilox-study-demo/tree/master/struct-design-one [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 2]: /images/20220406/c2faf1ffe05c4eaea0d5a11be1df94d9.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 3]: /images/20220406/a769ada6545a4191a3e76975eee38323.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RoX0NoYW8_size_16_color_FFFFFF_t_70 4]: /images/20220406/6f978cbcfd05435cafa82ceecaf3b6de.png
相关 【代码结构设计】优化重构文件夹的树状结构 背景 公司主要做数据治理相关的产品,奈何初期为了追求速度留下了一些饮鸩止渴的设计,后期影响最大的就是文件夹这块,治理数据首先要归档数据,通过文件夹的方式来将同一业务属性的 秒速五厘米/ 2023年10月04日 21:39/ 0 赞/ 49 阅读
相关 【代码结构设计】优化重构文件夹的树状结构 背景 公司主要做数据治理相关的产品,奈何初期为了追求速度留下了一些饮鸩止渴的设计,后期影响最大的就是文件夹这块,治理数据首先要归档数据,通过文件夹的方式来将同一业务属性的 缺乏、安全感/ 2023年09月24日 19:13/ 0 赞/ 97 阅读
相关 树形结构表设计的思考 前言 我们在实际设计树形表设计中,经常会遇到上级名称拼接下级名称共同构成一个完整的名称这种情况。知道下级id,获取从上级开始到该id的完整链路。这个需要我们去思考这种树形 小咪咪/ 2023年01月01日 04:53/ 0 赞/ 141 阅读
相关 《数据结构》—— 双链表逻辑思考与代码解析 链表 一、双链表 1.1 双链表的存储定义: 1.2 双链表的初始化: 1.3 双链表的判空: 太过爱你忘了你带给我的痛/ 2022年08月28日 15:54/ 0 赞/ 123 阅读
相关 设计线程监控代码结构 线程监控: / 线程状态的监控 @author dell / public class Test5 extend Myth丶恋晨/ 2022年08月09日 14:55/ 0 赞/ 115 阅读
相关 思考的代码001 1 . 前台展示的返回,配置表格查询,方便以后维护升级,不会改动代码; 例: 项目名称 项目接口类型 布满荆棘的人生/ 2022年07月11日 12:20/ 0 赞/ 136 阅读
相关 elasticsearch实践之代码结构设计 之前说过我们项目要集成es搜索引擎模块,这几天一直在做集成个事情。这过程中遇到过很多的坑,也尝试过很多的解决办法,今天给大家分享一下elasticsearch的实践过程。首先我 冷不防/ 2022年05月19日 11:23/ 0 赞/ 139 阅读
相关 Java Exception结构及思考 Java的异常结构: 1. 最顶层是Throwable接口,表示所有可以被抛出的内容 2. 往下是Error和Exception,Error代表的是非常严重的错误,JVM 今天药忘吃喽~/ 2022年05月13日 08:46/ 0 赞/ 179 阅读
相关 【Java】代码结构设计思考 背景 这篇博文是博主在做数据图形统计相关接口工作过程中对代码结构设计的一些思考总结,仅代表个人观点。 1.需求简述 提供资金关系数据图形统计,根据不同菜单地址跳转 阳光穿透心脏的1/2处/ 2022年04月06日 05:42/ 0 赞/ 214 阅读
还没有评论,来说两句吧...