场景说明
我们需要从不同的存储载体(driver)上读取数据,载体目前有两种,分别为
- Excel
- VFS(Virtual file system)
用户在表单上指定存储载体,并提供连接到该载体所需要的参数。比如VFS,需要指定路径,用户名及密码。
现在问题在于,我们从表单上收集到的数据格式,并不是API要求的格式,需要转换为API规定的格式才能请求成功。然而每一种载体都有其特殊性,不可能使用一个统一的转换器来进行转换,所以我们针对每一种载体都有一个转换器。
既然有了转换器,那么在请求API之前,我们先用合适的转换器将数据转换到指定格式,就可以搞定了。
重构之前的实现方式
class ExcelConvertor{
getDefaultModelData(){
return {}
}
getDefaultVisualData(){
return {}
}
// 将表单数据转化为API接受的格式
digitalize(visualData){
if(!visualData){
return this.getDefaultModelData()
}
...
}
// 将API返回的数据转化为表单能回填的格式
visualize(modelData){
if(!modelData){
return this.getDefaultVisualData()
}
...
}
}
class VfsConvertor{
getDefaultModelData(){
return {}
}
getDefaultVisualData(){
return {}
}
digitalize(modelData){
if(!modelData){
return this.getDefaultModelData()
}
...
}
visualize(visualData){
if(!visualData){
return this.getDefaultVisualData()
}
...
}
}
class Convertor{
static digitalizeByDriver(driver, visualData){
if(driver === 'excel'){
return new ExcelConvert().digitalize(visualData);
}
if(driver === 'vfs'){
return new VfsConvert().digitalize(visualData);
}
return {}
}
static visualizeByDriver(driver, modelData){
if(driver === 'excel'){
return new ExcelConvert().visualize(data, modelData);
}
if(driver === 'vfs'){
return new VfsConvert().visualize(data, modelData);
}
return {}
}
}
function save(formData){
const { driver, ...visualData} = formData
const params = Convertor.digitalizeByDriver(driver, visualData);
return fetch('xxx', params)
}
问题分析
直观上来看,代码很冗余。比如判空的逻辑在每个convertor中都出现了,并且一模一样,另外有大量的if语句,和重复的实例化具体convertor的语句。接下来尝试从设计模式的角度来分析我们违背了设计模式的什么原则?
- 开闭原则: 对修改关闭,对扩展开放
试想,如果我们需要增加一种载体,比如SQL,那么我们必须创建一个SqlConvertor的类,其中digitalize和visualize都有相同的判断空值的逻辑。
除此之外,我们必须改动Convertor,在里面增加新的if分支才能达到目的。
而理想情况下,我们应该只需要新增一个SQLConvert才符合开闭原则。
-
依赖倒转原则: 抽象不应该依赖于细节,细节已经依赖于抽象。
Convertor逻辑上来说应该是抽象的,它不应该关心系统中有多少具体的载体。 -
单一职责原则
Convertor既要负责对应类的创建,还要负责具体的业务逻辑。
重构步骤
在我们的Case中,转换数据有多种策略,需要根据载体类型来判断使用哪种策略,而这些策略的细节我们不希望客户端理解其细节,所以应用策略模式来优化;另外,根据载体类型创建转换器实例这项职责可以工厂方法来承担,引用简单工厂模式来优化现有代码。
以下是具体的重构步骤:
首先我们借助typescript的interface定义一个convertor的模样.
interface Converter{
getDefaultModelData: () => any;
getDefaultVisualData: () => any;
digitalie: (visualData) => any;
visualize: (modelData) => any;
}
随后我们重命名Converter为DriverConvert, 并使用构造函数注入的方式, 将具体的convertor实例注入到DriverConvert中,消除对具体convertor的依赖;同时将每个具体Convertor共有的判空逻辑放到DriverConvert类中。
Class DriverConvert implement Converter{
private converter: Converter;
constructor(converter: Convertor){
this.convertor = converter;
}
digitalize(visualData){
if(!data){
return this.getDefaultModelData()
}
return this.convertor.digitalize(visualData);
}
visualize(modelData){
if(!data){
return this.getDefaultVisualData()
}
return this.convertor.visualize(modelData);
}
}
然后我们需要一个简单工厂来根据driver生产对应的convertor,将创建具体convertor的职责抛出去,实现自身的单一职责.
class ConvertorFactory{
static getConvertor(type){
if(type === "excel"){
return new ExcelConvertor()
}
if(type === "vfs"){
return new VfsConvertor()
}
}
}
有了DriverConvertor和ConvertorFactory后,来改写调用方save函数
function save(formData){
const { driver, ...others } = formData;
conts params =
new DriverConvertor(ConvertorFactory.getConvertor(driver).digitalize(others);
return fetch('xxx', params)
}
最后,我们也应该让每一个具体的driver也实现Convertor这个接口, 这样才能被DriverConvertor的构造函数所接受。
对比总结
经过以上重构步骤,我们再来验证一下现在是否符合上述三种原则。
-
开闭原则
试想如果新增一种载体比如SQL,我们只需要新建一个sqlConvertor:
class SqlConvertor implements Convertor{
}
然后在Factory中增加一行
DriverConvertor一行都不用改,所以开闭原则满足。 -
依赖倒转原则。
至于依赖倒转原则,现在DriverConvertor整个类已经不再关心具体是哪一种convertor了,所以也满足。 -
单一职责
现在工厂方法专门负责生产具体Convertor,而DriverConvertor只需要负责转换即可。
通过这个Case,我明白了每当发现代码中需要些许多if-else的时候,就应该要意识到是不是会造成难扩展,抽象依赖细节的情况,如果确定是,那么进而选用合适的方式来优化它。
当然,正如lovelion在从招式与内功谈起——设计模式概述(三)
所说,应该谨慎地使用模式。通过对比重构前后代码可以发现,我们的代码量,类的个数都有增加,这对代码的维护和可理解性都增加了负担。如果我们的载体数量在一开始就能确保控制在三五个,永远都不发生变化,那么就按照重构前的写法也完全没有问题,毕竟那样代码更少,且更直接。
参考链接
算法的封装与切换——策略模式(四)
工厂三兄弟之简单工厂模式(三)
面向对象设计原则之开闭原则
面向对象设计原则之依赖倒转原则
面向对象设计原则之单一职责原则
网友评论