阿拉丁和灯

Thoughts, stories and ideas.



异步代码的重构实践(一)


Play! framework是一个异步的Web应用/服务开发框架。其主要的特点有三个,一是它是用Scala语言写的,但提供了Java语言的API;二是类似Ruby on Rails的MVC框架;三是支持Promise形式的异步处理。前两点不是我们这次要讨论的重点。我们这次的重点是第三点:异步处理。异步处理的代码,写起来跟平常的代码有一些不同,如果没有深入理解异步处理的本质,只是照葫芦画瓢的话,很容易导致写出来的代码异常的复杂。其标志是多重的嵌套,难以理解和修改。

下面我们以一个典型的类和方法为示例,讲述异步处理的本质,展示容易出现的糟糕的异步处理代码结构,以及如何将一个这样的多重嵌套的异步处理代码结构,重构为一个功能等价但更清晰的结构。

背景说明

这个方法要实现的功能可以分为两部分:一部分是调用远程Web服务,得到业务对象CollocationGroup;另一部分是,在得到了CollocationGroup之后,再需要调用一些服务,获取CollocationGroup对象的额外两个属性,设置到CollocationGroup对象上。

在一开始的代码中,用了嵌套的promise.map结构来进行多次的异步调用和处理。多重的嵌套导致了复杂的结构,难以理解也难以修改。上代码:

初始代码

public Promise<BaseDto<VszdCollocationGroup>> getCollocationGroup(final int postId,String warehouse,final String timestamp){
	
	Map<String, String> params = new HashMap<String, String>();
	params.put("t", timestamp);
	params.put("wh", warehouse);
	params.put("postId", String.valueOf(postId));
	params.put("sign", CmsProperty.getSign(timestamp));
	
	String url = CmsProperty.getCollocationGroupDetailUrl();
	HttpRequest request = new HttpRequest(url,params,HttpMethod.GET);
	Promise<HttpResponse> promise = HttpInvoker.execute(request);
	
	return promise.flatMap(new Function<HttpResponse,Promise<BaseDto<VszdCollocationGroup>>>() {
		@Override
		public Promise<BaseDto<VszdCollocationGroup>> apply(HttpResponse response) throws Throwable {
			int httpStatus = response.getStatus();
			BaseDto<VszdCollocationGroup> errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			
			if (httpStatus != 200) {
				Logger.error("getCollocationGroup from cms error ---> status="+httpStatus);
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			
			JsonNode result = null;
			try {
				result = JsonUtil.toJsonNode(response.getBody());
			} catch (Exception e) {
				Logger.error("getCollocationGroup from cms error --->"+e.getMessage(),e);
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			
			if (result == null) {
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			
			int code = result.get("code").asInt();
			if (code != 1) {
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			
			final VszdCollocationGroup collocationGroup;
			try {
				collocationGroup = JsonUtil.getNodeToObject(result, "article", VszdCollocationGroup.class);
			} catch (Exception e) {
				Logger.error("getCollocationGroup from cms error --->"+e.getMessage(),e);
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			if (collocationGroup == null) {
				errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
				return DtoUtil.promiseDto(errorDto);
			}
			
			String shareHtmlUrl = getDiffDataNode(result,"shareHtmlUrl");
			collocationGroup.setShareHtmlUrl(shareHtmlUrl);
			
			Promise<BaseDto<Integer>> likeCountPromise = getLikeCount(postId, timestamp);
			
			String contentHtmlUrl = getDiffDataNode(result,"contentHtmlUrl");
			final Promise<ContentHtml> contentHtmlPromise = transHtml2Json(contentHtmlUrl);
			
			return likeCountPromise.flatMap(new Function<BaseDto<Integer>, Promise<BaseDto<VszdCollocationGroup>>>() {

				@Override
				public Promise<BaseDto<VszdCollocationGroup>> apply(BaseDto<Integer> likeCountDto) throws Throwable {
					
					if (likeCountDto.getCode() != 200) {
						collocationGroup.setLikeCount(0);
						return DtoUtil.promiseDto(DtoUtil.ok(collocationGroup));
					}
					
					collocationGroup.setLikeCount(likeCountDto.getData());
					
					return contentHtmlPromise.map(new Function<ContentHtml, BaseDto<VszdCollocationGroup>>() {

						@Override
						public BaseDto<VszdCollocationGroup> apply(ContentHtml contentHtml) throws Throwable {
							collocationGroup.setContentHtmlJson(contentHtml);
							return DtoUtil.ok(collocationGroup);
						}
					});
				}
			});
		}
	});
	
}

重构思路

好的代码要完成三个目的:
1 实现功能
2 错误处理
3 表达意图
是否能清晰地意识到这三个目的,并且做到,是区分菜鸟程序员和大神程序员的标志。所有程序员都能意识到1,大多数也都能意识到2,但能做到并做好的就不多了,能意识到3,并且能在做到1和2的同时,做好3的,就是一个好的程序员。

重构方法

第一步,识别出上面的代码,实际上可以分为两部分,一部分是从后台服务根据id拿到CollocationGroup对象,另一部分是在拿到CollocationGroup之后,再去请求后台服务,获取CollocationGroup对象的额外两个属性,设置到CollocationGroup对象上。把这两部分代码分开来,是重构的第一步。

第二步,对于上面的第一部分,原来的代码传给promise.map的是一个匿名Function,这样没有能够清晰地表达出这个Function的作用(我们说,代码除了实现功能外,还要表达意图)。可以把它定义为一个静态的成员,给它赋上一个合适的名字,比如RESPONSE_TO_COLLOCATION_GROUP。

第三步,对于第二部分的代码,同样出于表达意图的目的,将代码提取出两个方法,分别取上合适的名字:populateLikeCount和populateHtmlJson。这一部分实际上是跟异步处理最相关的部分。异步处理代码的重构,首要的是把依赖关系明确出来。对这部分来说,这里有三个数据,一个是主体的CollocationGroup,另外是两个辅助数据。辅助数据对于主体之间存在依赖关系。但辅助数据之间不存在依赖关系,意味着写成代码之后,它们应该形成如

A
|-B
|-C

这样的结构。而不是

A
|-B
  |-C

这样的结构。

重构结果

以下是重构结果:

F.Function<HttpResponse, BaseDto<VszdCollocationGroup>> RESPONSE_TO_COLLOCATION_GROUP = new Function<HttpResponse,BaseDto<VszdCollocationGroup>>() {
	@Override
	public BaseDto<VszdCollocationGroup> apply(HttpResponse response) throws Throwable {
		int httpStatus = response.getStatus();
		BaseDto<VszdCollocationGroup> errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");

		if (httpStatus != 200) {
			Logger.error("getCollocationGroup from cms error ---> status="+httpStatus);
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}

		JsonNode result = null;
		try {
			result = JsonUtil.toJsonNode(response.getBody());
		} catch (Exception e) {
			Logger.error("getCollocationGroup from cms error --->"+e.getMessage(),e);
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}

		if (result == null) {
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}

		int code = result.get("code").asInt();
		if (code != 1) {
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}

		final VszdCollocationGroup collocationGroup;
		try {
			collocationGroup = JsonUtil.getNodeToObject(result, "article", VszdCollocationGroup.class);
		} catch (Exception e) {
			Logger.error("getCollocationGroup from cms error --->"+e.getMessage(),e);
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}
		if (collocationGroup == null) {
			errorDto = DtoUtil.dto(CommonError.SYS_ERROR,"CMS内部服务错误");
			return errorDto;
		}

		String shareHtmlUrl = getDiffDataNode(result,"shareHtmlUrl");
		collocationGroup.setShareHtmlUrl(shareHtmlUrl);

		String contentHtmlUrl = getDiffDataNode(result,"contentHtmlUrl");
		collocationGroup.setContentHtmlUrl(contentHtmlUrl);

		return DtoUtil.ok(collocationGroup);
	}
};

public Promise<BaseDto<VszdCollocationGroup>> getCollocationGroup(final int postId,String warehouse){
	final String timestamp = Long.valueOf(System.currentTimeMillis()/1000).toString();

	Map<String, String> params = new HashMap();
	params.put("t", timestamp);
	params.put("wh", warehouse);
	params.put("postId", String.valueOf(postId));
	params.put("sign", CmsProperty.getSign(timestamp));
	
	String url = CmsProperty.getCollocationGroupDetailUrl();
	HttpRequest request = new HttpRequest(url,params,HttpMethod.GET);
	Promise<HttpResponse> promise = HttpInvoker.execute(request);

	Promise<BaseDto<VszdCollocationGroup>> collocationGroupPromise = promise.map(RESPONSE_TO_COLLOCATION_GROUP);

	Promise<BaseDto<VszdCollocationGroup>> collocationGroupPromiseWithLikeCount = populateLikeCount(collocationGroupPromise, postId);

	Promise<BaseDto<VszdCollocationGroup>> collocationGroupPromiseWithHtmlJson = populateHtmlJson(collocationGroupPromiseWithLikeCount);

	return collocationGroupPromiseWithHtmlJson;

}

/**
 * 在collocation group 上设置like count的值
 * @param collocationGroupPromise
 * @param postId
 * @return
 */
Promise<BaseDto<VszdCollocationGroup>> populateLikeCount(Promise<BaseDto<VszdCollocationGroup>> collocationGroupPromise, final int postId)
{
	Long timestamp = System.currentTimeMillis() / 1000;
	final Promise<BaseDto<Integer>> likeCountPromise = getLikeCount(postId, timestamp.toString());

	return collocationGroupPromise.flatMap(new Function<BaseDto<VszdCollocationGroup>, Promise<BaseDto<VszdCollocationGroup>>>() {
		@Override
		public Promise<BaseDto<VszdCollocationGroup>> apply(final BaseDto<VszdCollocationGroup> collocationGroup) throws Throwable {
			try {
				if (collocationGroup.getCode() == 200) {
					return likeCountPromise.map(new Function<BaseDto<Integer>, BaseDto<VszdCollocationGroup>>() {
						@Override
						public BaseDto<VszdCollocationGroup> apply(BaseDto<Integer> likeCountDto) throws Throwable {
							if (likeCountDto.getCode() != 200) {
								collocationGroup.getData().setLikeCount(0);
								return collocationGroup;
							}

							collocationGroup.getData().setLikeCount(likeCountDto.getData());
							return collocationGroup;
						}
					});
				} else {
					return Promise.pure(collocationGroup);
				}
			} catch (Exception e) {
				Logger.error("populateLikeCount encountered an exception: "+e);
				return Promise.pure(collocationGroup);
			}
		}
	});
}

/**
 * 在collocation group上设置contentHtmlJson的值
 * @param collocationGroupPromise
 * @return
 */
Promise<BaseDto<VszdCollocationGroup>> populateHtmlJson(Promise<BaseDto<VszdCollocationGroup>> collocationGroupPromise)
{
	return collocationGroupPromise.flatMap(new Function<BaseDto<VszdCollocationGroup>, Promise<BaseDto<VszdCollocationGroup>>>() {
		@Override
		public Promise<BaseDto<VszdCollocationGroup>> apply(final BaseDto<VszdCollocationGroup> vszdCollocationGroup) throws Throwable {
			try {
				if (vszdCollocationGroup.getCode() == 200) {
					String contentHtmlUrl = vszdCollocationGroup.getData().getContentHtmlUrl();
					final Promise<ContentHtml> contentHtmlPromise = transHtml2Json(contentHtmlUrl);

					return contentHtmlPromise.map(new Function<ContentHtml, BaseDto<VszdCollocationGroup>>() {
						@Override
						public BaseDto<VszdCollocationGroup> apply(ContentHtml contentHtml) throws Throwable {
							vszdCollocationGroup.getData().setContentHtmlJson(contentHtml);
							return vszdCollocationGroup;
						}
					});
				} else {
					return Promise.pure(vszdCollocationGroup);
				}
			} catch (Exception e) {
				Logger.error("populateHtmlJson encountered an exception: "+e);
				return Promise.pure(vszdCollocationGroup);
			}
		}
	});
}

总结

上面的重构,主要依赖于以下几点:普通Java代码的Bad Smell识别(Long Method)和重构技术(Method Extraction, Constant Extraction),以及对异步代码处理机制的深入理解。

理解每种编程技术背后的本质,有助于我们在不同的应用场景下,都能识别出背后的逻辑,灵活应用相应的技术,从而写出清晰有效的代码。



View or Post Comments