背景

事情是这样的,业务中有一块需求是根据一个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的思路,把要做的事情归纳到一个结构中。相当于给实现接口写了一个强类型的文档。开发者只需注册该结构,而不用关心其中的结构是如何被使用的。