一、设计模式的使用场景
设计模式(Design Patterns)是在软件开发中经过验证的最佳实践,用于解决常见的设计问题。它们提供了一种可复用的解决方案,可以帮助开发人员提高代码质量、可维护性和可重用性。那么在在工作中,如何有效利用设计模式帮我们解决问题呢?我觉得主要有以下几点:
- 理解业务需求:在开始编码之前,深入了解业务需求是非常重要的。这将帮助我们确定哪些设计模式最适合当前的问题。
- 识别问题:在开发过程中,时刻关注代码中可能出现的重复、冗余或难以维护的部分。这些问题通常是需要应用设计模式的候选者。
- 选择适当的设计模式:根据识别到的问题,拟合设计模式的解决方式,在脑子中模拟改写后的业务代码,大致逻辑通顺后就可以尝试改写了。有的一直设计模式可能还不好解决,那就要先拆解一部分,分批解决,因为工程是有时间、成本要求的,不可能一直等我们苦思冥想和尝试。而且也不要试图一次性将所有代码都改造成使用设计模式。相反,应该逐步改进代码,逐步引入设计模式。这样可以确保代码始终保持在一个稳定且可维护的状态。
- 学习和研究:不断学习新的设计模式,研究它们是如何解决特定问题的。这可以通过阅读书籍、在线教程、博客文章和开源代码库来实现。这样可以让我们脑子里有个设计模式的概念,尽快的能拟合业务场景找到合适的设计模式,这样才保证能在工作中用起来。
- 注意事项:设计模式并不是银弹,我们应该根据具体情况和需求进行选择和调整。不要盲目追求使用设计模式,而是要在理解它们的优缺点和项目排期的基础上做出决策。
二、案例
在之前的工作经历中我有过多次使用设计模式的经历,下面我讲述两个分别是使用工厂和策略两种设计模式和模版方法的案例,方便大家理解。
1、策略和工厂模式
1)背景
当时面临的问题是有多种类型的 app,app 上有不同的页面,不同页面根据不同的业务或者标识去查询配置的不同的页面,但是我们页面的数据逻辑又在其他系统,我们只能在自己系统内完成这样的筛选逻辑,在当时要么根据不同的客户端开放不同的接口,要么根据不同的业务类型开放不同的接口,那这样肯定会随着端的增多或者业务类型的增多会产生接口爆炸的情况,要么就是在一个接口内完成大量的判断来完成这样的逻辑。
2)引入设计模式
那有没有可能借助设计模式来解决呢?当时我就在寻找相关的案例,最后觉得工厂模式和策略模式的结合是很好的方式,简单工厂模式的作用是提供专门的工厂类用于创建对象,实现了对象创建和使用的职责分离,客户端不需知道所创建的具体产品类的类名以及创建过程,只需要知道具体产品类所对应的参数即可。这样不正是拟合了根据上送和客户端参数创建多端工厂的场景吗?而策略模式是把具体的算法实现从业务逻辑中剥离出来,成为一系列独立算法类,使得它们可以相互替换。那也正好拟合了我们根据上送的业务类型和页面类型寻找不同处理类的场景,所以我就在项目中着手使用设计模式来解决这样的问题。
- 客户端
@SpringBootTest public class ApplicationTestBizPage { @Autowired HandlerFeComponentFactory handlerFeComponentFactory; @Test public void test() { List<FeComponent> feComponentList = handlerFeComponentFactory.doWork("alipay", "home", "biz"); System.out.println("-----------------"); System.out.println(feComponentList); } }
- 创建工厂
@Component public class HandlerFeComponentFactory { @Autowired private List<HandlerFeComponentService> handlerFeComponentServices; public List<FeComponent> doWork(String app, String page, String bizType) { for (HandlerFeComponentService handlerFeComponentService : handlerFeComponentServices) { if (handlerFeComponentService.isSupport(app)) { return handlerFeComponentService.doWork(app, page, bizType); } } return new ArrayList<>(); } }
- 客户端接口
public interface HandlerFeComponentService { boolean isSupport(String app); List<FeComponent> doWork(String app, String page, String bizType);
- 支付宝实现
@Component public class AliappHandlerFeComponentServiceImpl implements HandlerFeComponentService { @Autowired private Map<String, AbstractAliappHandlerFeComponent> aliappHandlerFeComponentMap; @Override public boolean isSupport(String app) { return "alipay".equals(app); } @Override public List<FeComponent> doWork(String app, String page, String bizType) { String bizPage = String.format("%s:%s:%s", app, page, bizType); AbstractAliappHandlerFeComponent aliappHandlerFeComponent = aliappHandlerFeComponentMap.get(bizPage); if (Objects.nonNull(aliappHandlerFeComponent)) { return aliappHandlerFeComponent.doWorkForAliapp(app, page); } return null; }
- 业务抽象类定义
public abstract class AbstractAliappHandlerFeComponent { abstract List<FeComponent> doWorkForAliapp(String app, String page) ; }
- 业务抽象类实现
@Component("alipay:home:biz") public class TestBizPage extends AbstractAliappHandlerFeComponent { @Override List<FeComponent> doWorkForAliapp(String app, String page) { List<FeComponent> feComponentList = new ArrayList<>(); FeComponent feComponent = new FeComponent(); feComponent.setMessage("alipay:home:biz"); feComponentList.add(feComponent); return feComponentList; } }
- 实体类
public class FeComponent { public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } private String message; @Override public String toString() { return "FeComponent{" + "message='" + message + '\'' + '}'; } }
从上面可以看出,调用者仍然只需要传入自己的参数,在寻找具体处理类和方法的路径都被包装在工厂方法和 springbean 和具体类的映射关系处理中。以后增加了新的客户端百度,只需要新增一个 BaiduService 和处理类即可。
2、模版方法
还有一次经历是是使用模版方法完成业务逻辑的组件化。
1)背景
之前一个业务逻辑有很多个版本,我还需要再增加2个版本,全部流程大致有15步流程,在不同的版本可能会有缺少不同的步骤,所以之前的逻辑就有在不同步骤中有各自版本的判断导致很难清晰的看出每个版本自己的逻辑是什么,导致我很难加自己的逻辑,也很不利于排查问题。
2)引入设计模式
那怎样解决这样一个问题呢?我是结合模版方法的模式来重构了一遍代码,我们先看下模版方法模式的定义,模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤。它主要优点包括:
- 封装不变部分:将算法的核心结构和不变部分封装在基本方法中,使得子类可以根据需求对可变部分进行扩展,而不影响不变部分。
- 代码复用:抽象类中包含的基本方法可以避免子类重复实现相同的代码逻辑,提高了代码的复用性。
- 更好的扩展性:由于具体实现由子类来完成,可以方便地扩展新的功能或变更实现方式,同时不影响模板方法本身。
这不正好拟合我遇到问题场景吗。之后我将业务逻辑中的步骤梳理出来,放到一个抽象类中,公共部分在抽象类中实现,不同版本的内容定义成抽象方法,由各个版本的子类完成实现,由传入的参数来和子类形成一个映射关系。这样我增加自己的两个版本,只需要增加两个子类即可,每个版本的逻辑也清晰可见,减少了排查的难度。我也来展示一个简化版本根据的 demo,方便大家理解的。
// 抽象类定义了算法的骨架 public abstract class AbstractClass { // 模板方法 public final void templateMethod() { step1(); step2(); hook1(); // 钩子方法,子类可以选择是否覆盖 step3(); hook2(); // 另一个钩子方法 } // 具体步骤,由抽象类实现 protected void step1() { System.out.println("执行步骤1"); } protected void step2() { System.out.println("执行步骤2"); } protected void step3() { System.out.println("执行步骤3"); } // 钩子方法,默认实现为空 protected void hook1() { } // 另一个钩子方法,默认实现为空 protected void hook2() { } } // 子类实现了抽象类,并重写了某些步骤 public class SubClass extends AbstractClass { // 重写钩子方法 @Override protected void hook1() { System.out.println("执行钩子方法1"); } // 重写另一个钩子方法 @Override protected void hook2() { System.out.println("执行钩子方法2"); } } // 客户端代码 public class Client { public static void main(String[] args) { AbstractClass obj = new SubClass(); obj.templateMethod(); } }
在这个示例中,AbstractClass 是一个抽象类,它定义了业务的全部落哦,包括三个具体步骤(step1(), step2(), step3())和两个钩子方法(hook1(), hook2())。
SubClass 是 AbstractClass 的一个子类,它选择性地重写了两个钩子方法,但没有改变算法的骨架。
Client 类是客户端代码,它创建了一个 SubClass 的实例,并调用了模板方法templateMethod()。
当运行这个示例时,输出将是:
执行步骤1
执行步骤2
执行钩子方法1
执行步骤3
执行钩子方法2
模板方法还是比较常见的,大家可以看看 jdk 中的 map 实现和 springboot 的启动流程中都有体现的。
需要注意的是,虽然设计模式在软件开发中很有用,但过度使用或不当地使用它们也可能导致代码过度复杂化和难以理解。那怎样定义过度使用呢,遵循事不过三的原则即可,只要重复的代码或者方法没有超过三个的时候就不用优化。希望大家可以在实际业务中灵活运用。