背景
事情是这样的,业务中有一块需求是根据一个url解析出他代表的电商平台是哪家。比如,通过域名就可以区分出淘宝(taobao),天猫(tmall),京东(jd),亚马逊(amazon)等。然后还要通过一些规则特征,识别出url代表是一个商品详情,还是一家该电商平台的店铺。业务的目的是要通过url获取(或者说抓取)它所对应的商品信息,单商品则抓商品,店铺页则抓所有店铺商品。单品好说,一般url中都有类似restful api的字段可解析,但店铺的话,又要先抓取一次店铺信息。 这块代码反复迭代,又没有经过大修,非常琐碎凌乱,最近,在几种平台的基础上,又要新增平台,我接手这块旧代码,深感力不从心,于是决心在失控之前部分重构。
这个坑的模样
第一步:
var type
if TaobaoShop {
type = taobaoshop
} else if TabaoProduct {
type = taobaoProduct
} else if TmallShop {
....
}
总之第一步,先写好对应类型的判断函数,无论string包含还是正则表达式,不一而足,确定一个类型
第二步:
var productArray
if type & Product {
if type & taobao {
productArray = parseTaobao
} else if type & tmall {
productArray = parseTmall
} else if .....
}
if type & Shop {
if type & taobao {
productArray = fetchTaobaoShop
} else if type & tmall {
productArray = fetchTmallShop
} else if ...
}
这一步,从url中获取相应的待抓取商品
第三步:
if type & taobao {
spiderType = spiderTaobao
} else if type tmall {
spiderType = spiderTmall
} else if .....
第三步设置爬虫类型,幸好爬虫是一个统一的接口,只是传入的参数枚举值不同
加平台?坑爹呢!
这段代码要说这样看,其实还可以,只是无论解析还是抓取准备,都还有一些鸡毛代码;也就是说,这种代码的问题在于,我需要增加一种类型时,需要从头到脚改一遍,去增加他的if..else分支,费心费力,每次扩展都需要从头阅览。这还是我多次阅读老代码后梳理出来的简单流程,实际上原始代码更加复杂,“虬枝旁逸”。
第一波直觉:设计模式,工厂方法
工厂方法作为唯一一种我在实际编程中非常常用的设计模式,我第一时间就想到了它。模型也很合适,抽象一系列抓取、解析商品id的接口,从对url的类型解析生成一个具体的实例,流程代码由接口调用串联起来;这样增加平台时,后续代码只要增加实现的接口,将其放入静态工厂的定义中,就可以完成。
我的第一波代码变成了这样
// 第一步 urlMetaInfo 中包含了url的类型信息
urlMetaInfo = parseUrl(url)
// 第二步 根据类型信息生成对应的实现实例
var fetcher = NewFectherFactory(urlMetaInfo)
var productArray = fetcher.FetchProductId()
// 第三步
var spidertype = fetcher.GetSpiderType()
spider fetch ....
调用代码清爽了很多,表面上不再存在可怕的if..else阵列。。
工厂方法的背后,还是凌乱的定义与解析
这个时候pareUrl依然长这幅鬼样子
if type & taobao {
spiderType = spiderTaobao
} else if type tmall {
spiderType = spiderTmall
} else if .....
而这个时候NewFectherFactory
使用的是一个全局的注册表
map = {
taobao: taobaofetcher,
amazon: amazonfetcher
....
}
平台对应的抓取爬虫类型,依然是一个全局的map
map = {
taobao: taobaoSpiderType,
amazon: amazonSpiderType,
....
}
进步的地方在于,我增加新的平台定义,不再需要动流程代码,但是推动这个背后代码的注册和定义,一个都不能少。
既然觉得定义来定义去太松散,不如组成一个平台模板结构
(ps: 后来我知道这个思路叫做SOP)
整理流程下来,我梳理了一个实现一个平台的抓取需要些什么。首先要有一个函数,帮我匹配url特征,告诉我这个url是不是这个平台的,并且是店铺还是商品url;其次有一个方法直接生成对应实例,不想再去哪里的map注册一个实例类型;最后在模板里定义好,这个平台对应的爬虫枚举值是哪个。 于是一个结构体:
type platformTemplater struct {
typeDef
spiderType
newFetcherFunc
parseFunc
}
其中typeDef表示平台,spiderType表示爬虫枚举值,两个函数指针,分别对应生成fetcher实例的函数与解析url的函数;其中解析url的parseFunc只要告诉我这个url他认识不认识就可以了。
于是假设要定义淘宝的抓取模板
typedef taobaoType
var taobaoplatform = platformTemplate {
typeDef : taobaoType
spiderType: spiderType
newFetcherFunc : func() fetcher {return new taobaofetcer}
parseFunc : func() (bool, isShop)
}
一次实现, 一次定义,一次注册
一次实现:
实现具体平台下的id解析任务
一次定义:
定义平台枚举,定义好平台模板实例,包括 抓取实例的生成函数
一次注册:
将定义好的平台模板实例,写入实例数组,如
var platforms = []platformTemplate{
taobaoplatform,
tmalltplatform
}
注册自动展开
生成实例函数与枚举值的对应关系表仍然存在,只是不需要定义后手动注册; 在初始化代码中,通过遍历platforms
数组,填充了这些map。
实际上伪代码就是:
for p in platforms {
fetcherMap[p.typeDef] = p.newFetcherFunc
spiderMap[p.typeDef] = p.parseFunc
}
func NewFectherFactory(t typeDef) fecther {
return fetcherMap[t]() // 函数调用
}
func SpiderType(t typeDef) spiderType {
return spiderMapp[t]
}
总结
面对流程复杂且可以抽象成标准过程的业务代码,可以先用接口实现过程抽象。然后如果因为接口/过程太多,要实现接口的接口或者定义较多,使得后续再扩展要做的事情比较多,容易出错或遗漏,就用类似SOP的思路,把要做的事情归纳到一个结构中。相当于给实现接口写了一个强类型的文档。开发者只需注册该结构,而不用关心其中的结构是如何被使用的。