设计模式巡礼:多板适配案例解析与深度重构

3,541 阅读9分钟

月黑风高,好兄弟发给我一个重构需求,咨询我的意见。

一、 场景分析

开发的产品是需要运行到不同的定制Android板子,不同板子有对应的不同SDK提供的API,目前的业务端,业务流程基本是确定的,比如有业务流程为打开板子的某项开关(需求就是打开开关),对应在板子中可能存在A、B、C三个板子或者更多,其中板子都提供了打开开关S的方法,但是方法名称各不相同,目前在代码中的使用方式都是,创建一个服务于业务的工具类,在工具类中判断板子类型创建不同的SDK,并使用不同SDK的API完成这个需求。

对于商业SDK的开发及多SDK使用,我有丰富的设计经验,面对这个问题,立马能说出这个描述中存在的问题有多少,所以意见是重构!必须重构。

1.1 存在问题

我从来都是以理服人,必须要着说明为什么重构,怎么重构,结果是什么.

images.jpeg

1.2 还原代码,暴露问题

分析上述问题,其中的重点有以下几处:

  1. 多板子(开发平台多,第一反应就是要适配(描述混乱的原因之一就是适配导致的))
  2. 业务概念统一(什么是业务概念统一呢?举个例子,对于产品而言,在产品需求发布的时候说,当用户点击按钮1时,红灯亮,这就是一个统一的业务概念,因为我们是多板子的开发,我们立马,应该考虑的就是分散性的思考)
  3. 不同板子都提供了SDK,但是API并不相同(此处可以这样分析一下,对于定制开发的场景中,特别是这种场景下,需求的实现与否只与板子的供应需求是否相吻合(供应需求就是板子自身对外的开放功能))

通过这三点可以看出,这个需求其实很简单,很清晰,但是对于上述的描述的实现方式,肯定是不行的。

为了好解决问题,我们要引入几个实体板子名称(在开发中,领域模型非常重要,事关需求的成功与否、事关团队的配合度高低), 假设目前面对的板子有例如树莓派(Raspberry Pi)、小米开发板、华为开发板。然后还原一下代码

假设工具类名称为ControlBroadUtil, 还原代码如下

image.png

这大概就是描述还原的代码,这问题就很清晰了。

  1. 硬编码板子类型判断: 目前的实现方式中,通过在业务工具类中硬编码板子类型判断,这会导致代码的脆弱性。一旦有新的板子类型加入,就需要修改代码,可能引入新的错误。
  2. SDK方法名称不一致: 不同板子的SDK提供了相同功能的方法,但方法名称却不同。这种情况可能会导致混乱,使得代码变得难以理解和维护。
  3. 紧耦合的业务工具类: 目前的设计中,业务工具类负责判断板子类型并选择相应的SDK。这样的紧耦合设计违反了单一职责原则,使得代码难以测试和扩展。
  4. 可维护性差: 随着板子类型的增多,业务工具类会变得越来越庞大,难以维护。任何一次修改都可能引发意外的问题。
  5. 扩展性问题: 当需要支持新的板子类型时,目前的设计需要修改现有代码,而不是简单地添加新的板子类型的实现。这降低了系统的可扩展性。
  6. 缺乏抽象层: 目前的实现没有明确的抽象层,使得在引入新板子类型时无法简单地使用接口进行统一操作。这违反了面向对象设计的一些原则。
  7. 缺乏文档和规范: 由于不同板子类型的方法名称不同,缺乏清晰的文档和规范,团队成员可能难以理解和使用不同板子的SDK。

二、 谈谈重构思路,有哪些预留项

面对这类型的问题,其实老手第一眼就想到重构的方式了,我的建议是在给出重构意见时必须考虑后续的问题。

2.1 使用抽象工厂模式(目的就是简化、统一、分离创建流程,使用抽象工厂模式可以满足)

  • SDK的创建的独立互不影响,对应SDK的配置项都可以在自己的工厂中完成(为什么要用抽象工厂不用简单工厂?因为后续对于一个板子的变化维度可能超过两个(多套API))
  • 可以根据不同的板子进行合理的选择
  • 在特定场景中可以自由切换,比如华为的不同版本板子,在升级之后的切换场景。

2.2 类图结构如下

2.2.1 SDK创建UML

image.png

其中:

ControlBoardFactory (抽象工厂):它声明了一组用于创建一族产品的方法,每个方法对应一种产品,在这里就是每一种板子的SDK。

/**  
*  
* @author: kpa  
* @date: 2024/2/7  
* @description: 它声明了一组用于创建一族产品的方法,每个方法对应一种产品,在这里就是每一种板子的SDK。  
*/  
abstract class ControlBoardFactory<out T : ControlBoardService> {  
    abstract fun createControlBoard(): T  
}

HuaweiFactory等(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每种产品都位于某个产品等级结构中。

/**  
*  
* @author: kpa  
* @date: 2024/2/7  
* @description:huawei 板子的创建工厂  
*/  
class HuaweiControlBoardFactory : ControlBoardFactory<HuaweiBoardServiceImpl>() {  
  
    override fun createControlBoard(): HuaweiBoardServiceImpl {  
        return HuaweiBoardServiceImpl()  
    }  
}

ControlBoardService(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。

/**  
*  
* @author: kpa  
* @date: 2024/2/7  
* @description: 抽象产品,对应业务,为需求接口  
*/  
interface ControlBoardService {  
    fun switch(switchValue: Int)  
}

HuaweiControlBordImpl等(具体产品):它定义具体工厂生产的具体产品对象,实现在抽象产品接口中声明的业务方法。

/**  
*  
* @author: kpa  
* @date: 2024/2/7  
* @description:  
*/  
class HuaweiBoardServiceImpl : ControlBoardService {  
    override fun switch(switchValue: Int) {  
        TODO("Not yet implemented")  
    }  
}

2.2.2 SDK API使用UML

对于类中提的提供方接口将使用适配器模式完成与已知SDK API适配,此处设计的目的是:

  1. 需求开发中,不需要考虑具体的实现,我们应该做到抽象的需求接口和产品需求是一致的。这样我们即使撤走部分SDK或者添加n中SDK都不会影响我们的业务。
  2. SDK的API我们是不可以介入编程的,所以他在编码体系中只能是直接使用,但是直接使用就会导致代码的耦合性太高,对三方的依赖太强可不是什么好事

所以决定对抽象产品(ControlBoardService)部分使用适配器模式进行设计,要求是:

  1. 业务方无感SDK的调用但要调用
  2. 业务方可以多组合+自主实现

image.png

组合方式:

class HuaweiBoardServiceImpl : ControlBoardService {  
    private val huaweiBoardSDK: HuaweiBoardSDK by lazy { HuaweiBoardSDK() }  
  
    override fun switch(switchValue: Int) {  
        huaweiBoardSDK.huaweiOpenSwitch(switchValue)  
    }  
}

2.2.3 外观模式,保持业务掉用的整洁

image.png

如果使用上述的代码,在一套体系中还是会出现调用混乱的问题,呐,处理方式就是使用外观模式,右边部分为外观模式下的物理、逻辑结构。

外观模式相对简单。 类图就不画了代码如下:

/**  
*  
* @author: kpa  
* @date: 2024/2/7  
* @description: 外观模式下的工具类  
*/  
object ControlBroadUtil {  
    private val huaweiBoardFactory: HuaweiControlBoardFactory by lazy { HuaweiControlBoardFactory() }  
    // 其他工厂  
    //...  
  
    // 需求接口,面相该接口编程  
    private var controlBoardService: ControlBoardService? = null  
  
    /**  
    * 供应商环境  
    */  
    private var supplierEnvironment = ""  
    private fun init() {  
    // 统一配置读取  
       supplierEnvironment = System.getProperty("")  
        controlBoardService = huaweiBoardFactory.createControlBoard()  
    }  
    /**  
    * 直接使用接口编程  
    */  
    public fun getControlBoardService(): ControlBoardService {  
        return controlBoardService ?: HuaweiControlBoardFactory().createControlBoard()  
    }  
}

三、重构结果分析

经过以上的分析和重构思路,可以得出以下重构结果分析:

3.1 抽象工厂模式的优点

  1. 松耦合: 抽象工厂模式将产品的创建与使用分离,使得系统更加灵活,减少了模块间的直接依赖,达到松耦合的效果。
  2. 可扩展性: 当需要增加新的板子类型时,只需添加新的具体工厂和产品类,无需修改已有代码,符合开闭原则,提高了系统的可扩展性。
  3. 统一接口: 抽象工厂模式提供了一组统一的接口,使得客户端无需关心不同板子的具体实现细节,从而简化了客户端代码。
  4. 业务概念统一: 通过抽象工厂模式,可以将不同板子的SDK统一到一组接口中,使得业务概念更加清晰和统一。

3.2 适配器模式的优点

  1. 解耦: 适配器模式将业务代码与SDK的具体实现解耦,业务方无需关心底层SDK的细节,提高了代码的可维护性和可读性。
  2. 灵活性: 适配器模式使得业务方可以更灵活地选择和切换不同的SDK,而无需修改业务代码,降低了对SDK的依赖性。
  3. 可扩展性: 当需要添加新的SDK时,只需实现适配器接口即可,无需修改已有代码,符合开闭原则,提高了系统的可扩展性。

四、总结

通过对现有代码的分析和重构,我们解决了原有代码存在的问题,提高了系统的可维护性、可扩展性和可读性。使用抽象工厂模式和适配器模式,使得系统更加灵活,业务概念更加统一,业务代码与底层SDK的实现解耦。这样的设计不仅适应了当前的业务需求,还为未来的扩展和变化提供了良好的支持。

在实际开发中,重构是一个不断演进的过程,需要根据实际情况灵活运用设计模式和原则,不断优化和改进代码结构。同时,良好的文档和规范也是团队协作的重要保障,能够使团队成员更加容易理解和使用不同板子的SDK。