实践中的重构16_多即是少

在编写UT的过程中,随处可见重复,硬编码等等使得代码僵化的代码。
UT也是代码,并且其重要性和产品代码相比,只高不低。
以下是一段产品代码的框架。
public interface UserQueryManager {

	/**
	 * 获得历史库和生产库的分界时间点。
	 * */
	public Date getHisDate();

	/**
	 * 统计两个时间点之间的用户数。由调用方保证该时间范围不是跨库时间范围。
	 * */
	public int countUsersNumber(Date start, Date end);

	/**
	 * 查找两个时间点之间的用户。由调用方保证该时间范围不是跨库时间范围。 pageNum从1开始计数。
	 * */
	public List<User> findUsers(Date start, Date end, int pageSize, int pageNum);

}

public class UserQueryService {

	/**
	 * 时间段是否跨库。
	 * */
	private static boolean isCross(Date start, Date end, Date hisDate) {
		return start.before(hisDate) && end.after(hisDate);
	}

	private UserQueryManager manager;

	/**
	 * 分页查询,查找start和end之间的用户。 pageNum从1开始计数。
	 * */
	public List<User> findUsers(Date start, Date end, int pageSize, int pageNum) {
		Date hisDate = manager.getHisDate();
		// 是否跨库查询
		boolean isCross = isCross(start, end, hisDate);
		if (isCross) {
			return findUsers(start, end, hisDate, pageSize, pageNum);
		} else {
			return manager.findUsers(start, end, pageSize, pageNum);
		}
	}

	/**
	 * 跨库分页查询,查找start和end之间的用户。 pageNum从1开始计数。
	 * */
	private List<User> findUsers(Date start, Date end, Date his, int pageSize,
			int pageNum) {
		// 历史库中的用户数。
		int hisTotalSize = manager.countUsersNumber(start, his);

		int startNum = (pageNum - 1) * pageSize + 1;
		int endNum = pageNum * pageSize;

		// 全部在历史库中。
		if (endNum <= hisTotalSize) {
			return manager.findUsers(start, his, pageSize, pageNum);
		}

		// 全部在生产库。
		if (startNum > hisTotalSize) {
			int remainder = hisTotalSize % pageSize;
			// 页面边界整齐或者不整齐。
			if (remainder == 0) {
				int newPageNum = pageNum - hisTotalSize / pageSize;
				return manager.findUsers(his, end, pageSize, newPageNum);
			} else {
				int newPageNum = pageNum - hisTotalSize / pageSize - 1;
				List<User> firstUserList = manager.findUsers(his, end,
						pageSize, newPageNum);
				List<User> secondUserList = manager.findUsers(his, end,
						pageSize, newPageNum + 1);
				return combinePagedUserList(firstUserList, secondUserList,
						pageSize - hisTotalSize % pageSize, pageSize);
			}
		}

		// 跨库查询
		List<User> firstUserList = manager.findUsers(start, his, pageSize,
				pageNum);
		List<User> secondUserList = manager.findUsers(his, end, pageSize, 1);
		return combinePagedUserList(firstUserList, secondUserList, 0, pageSize);

	}

	/**
	 * 合并分页用户列表,删除最前面的deleteSize个元素。如果结果list的大小大于maxSize,删除尾部的元素使list的
	 * size==maxSize.
	 * 
	 * <pre>
	 * deleteSize>=0
	 * maxSize>0
	 * </pre>
	 * 
	 * */
	private List<User> combinePagedUserList(List<User> list1, List<User> list2,
			int deleteSize, int maxSize) {

		List<User> userList = new ArrayList<User>();
		userList.addAll(list1);
		userList.addAll(list2);

		int fromIndex = deleteSize;
		int toIndex = Math.min(userList.size(), deleteSize + maxSize);
		return userList.subList(fromIndex, toIndex);

	}

	public void setManager(UserQueryManager manager) {
		this.manager = manager;
	}
}

这段代码提供了用户分页查询功能。但是用户的信息是保存在不同的数据库里面的。所以,要由应用程序来处理历史库和生产库的查询定向和分页组装功能。
稍稍分析一下,这里主要有如下情况需要处理:
1 时间范围在历史库中,只查询历史库。
2 时间范围在生产库中,只查询生产库。
3 时间范围跨库,但是结果数据只在历史库中。
4 时间范围跨库,结果数据一部分在历史库中,一部分在生产库中,需要做拼接处理。
5 时间范围跨库,但是结果数据只在生产库中,视页面边界是否和数据边界对齐的情况要分别处理。

原始的UT的思路是使用JMock,不同的场景mock出不同的manager。导致UT的可读性比较差。代码中充斥着硬编码,重复等不好的味道。
mock本来是为了简化生活的,但是这个地方的mock却是越看越别扭。
这个时候,我想起了“少即是多”这句名言,少即是多,那么多即是少。如果不使用mock,直接写一个简单的manager实现,这样一来,UT应该更简明。

class MockUserQueryManager implements UserQueryManager {
	private int hisSize;
	private int prodSize;
	private Date hisDate;
	private List<User> hisUserList;
	private List<User> prodUserList;

	private void initUserList() {
		hisUserList = new ArrayList<User>();
		for (int i = 1; i <= hisSize; i++) {
			User user = new User();
			user.setName("his_" + i);
			hisUserList.add(user);
		}

		prodUserList = new ArrayList<User>();
		for (int i = 1; i <= prodSize; i++) {
			User user = new User();
			user.setName("prod_" + i);
			prodUserList.add(user);
		}
	}

	public MockUserQueryManager(Date hisDate, int hisSize, int prodSize) {
		this.hisDate = hisDate;
		this.hisSize = hisSize;
		this.prodSize = prodSize;
		initUserList();
	}

	@Override
	public Date getHisDate() {
		return hisDate;
	}

	@Override
	public int countUsersNumber(Date start, Date end) {

		if (!hisDate.before(end)) {
			return hisSize;
		} else {
			return hisSize + prodSize;
		}
	}

	@Override
	public List<User> findUsers(Date start, Date end, int pageSize, int pageNum) {
		if (!hisDate.before(end)) {
			int fromIndex = (pageNum - 1) * pageSize;
			int toIndex = Math.min(pageSize * pageNum, hisSize);
			return hisUserList.subList(fromIndex, toIndex);
		} else {
			int fromIndex = (pageNum - 1) * pageSize;
			int toIndex = Math.min(pageSize * pageNum, prodSize);
			return prodUserList.subList(fromIndex, toIndex);
		}
	}

}

manager的实现如上,其思路如下:
1 在内存中存储用户对象。模拟一个简单的用户数据库。
2 使用构造函数来构造该manager的数据,包括历史库时间分界点,历史库和生产库用户数多少,自动构建用户。
3 构建用户的同时,设置一个特殊值,用来验证返回结果。
4 实现了manager的接口方法。
构思和实现这个UT的架构比较费时间,但是一旦完成了,UT的编写就是简单到了极点。下面的UT写起来基本不用费什么脑力。这样,我们通过编写一个相对复杂的内存实现,简化了UT的编写。
UT如下:
public class TestUserQueryService {
	/**
	 * 解析"yyyyMMdd"形式的字符串为日期。
	 * */
	private static Date parseDate(String dateStr) {
		Date date = null;
		try {
			DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
			date = dateFormat.parse(dateStr);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return date;
	}

	private UserQueryService queryService;
	private MockUserQueryManager mockUserQueryManager;

	/**
	 * 初始化测试环境。
	 * */
	private void setUpEnv(String hisDate, int hisSize, int prodSize) {
		queryService = new UserQueryService();
		mockUserQueryManager = new MockUserQueryManager(parseDate(hisDate),
				hisSize, prodSize);
		queryService.setManager(mockUserQueryManager);
	}

	/**
	 * 验证返回的结果。
	 * 
	 * @param userList
	 *            用户列表。
	 * @param size
	 *            总用户个数。
	 * @param hisSize
	 *            历史库返回的用户个数。
	 * @param hisFrom
	 *            历史库返回用户的起始index。
	 * @param prodSize
	 *            生产库返回的用户个数。
	 * @param prodFrom
	 *            生产库返回的用户起始index。
	 * 
	 * */
	public void assertUserList(List<User> userList, int size, int hisSize,
			int hisFrom, int prodSize, int prodFrom) {

		Assert.assertNotNull(userList);
		Assert.assertEquals(size, hisSize + prodSize);

		Assert.assertEquals(size, userList.size());

		for (int i = 0; i < hisSize; i++) {
			User user = userList.get(i);
			Assert.assertEquals("his_" + (hisFrom + i), user.getName());
		}

		for (int i = 0; i < prodSize; i++) {
			User user = userList.get(hisSize + i);
			Assert.assertEquals("prod_" + (prodFrom + i), user.getName());
		}
	}

	/**
	 * 用户查询(只查历史库,满页)
	 * 
	 */
	@Test
	public void testQuery_00() {
		setUpEnv("19820110", 40, 20);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 1);
		assertUserList(userList, 10, 10, 1, 0, 0);
	}

	/**
	 * 用户查询(只查历史库,不满页)
	 * 
	 */
	@Test
	public void testQuery_01() {
		setUpEnv("19820110", 43, 20);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 5);
		assertUserList(userList, 3, 3, 41, 0, 0);
	}

	/**
	 * 用户查询(只查生产库,满页)
	 * 
	 */
	@Test
	public void testQuery_10() {
		setUpEnv("19810804", 40, 20);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 2);
		assertUserList(userList, 10, 0, 0, 10, 11);
	}

	/**
	 * 用户查询(只查生产库,不满页)
	 * 
	 */
	@Test
	public void testQuery_11() {
		setUpEnv("19810801", 43, 23);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 3);
		assertUserList(userList, 3, 0, 0, 3, 21);
	}

	/**
	 * 用户查询(跨库,满页)
	 * 
	 */
	@Test
	public void testQuery_20() {
		setUpEnv("19820103", 43, 20);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 5);
		assertUserList(userList, 10, 3, 41, 7, 1);
	}

	/**
	 * 用户查询(跨库,不满页)
	 * 
	 */
	@Test
	public void testQuery_21() {

		setUpEnv("19820103", 43, 4);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 5);
		assertUserList(userList, 7, 3, 41, 4, 1);
	}

	/**
	 * 用户查询(只查生产库,对齐满页)
	 * 
	 */
	@Test
	public void testQuery_30() {

		setUpEnv("19820103", 40, 60);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 6);
		assertUserList(userList, 10, 0, 0, 10, 11);
	}

	/**
	 * 用户查询(只查生产库,对齐不满页)
	 * 
	 */
	@Test
	public void testQuery_31() {

		setUpEnv("19820103", 40, 17);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 6);
		assertUserList(userList, 7, 0, 0, 7, 11);
	}

	/**
	 * 用户查询(只查生产库,不对齐满页)
	 * 
	 */
	@Test
	public void testQuery_40() {

		setUpEnv("19820103", 43, 40);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 6);
		assertUserList(userList, 10, 0, 0, 10, 8);
	}

	/**
	 * 用户查询(只查生产库,不对齐不满页)
	 * 
	 */
	@Test
	public void testQuery_41() {

		setUpEnv("19820103", 43, 20);
		List<User> userList = queryService.findUsers(parseDate("19820101"),
				parseDate("19820107"), 10, 7);
		assertUserList(userList, 3, 0, 0, 3, 18);
	}

}

每一个UT都很简洁,唯一不足的地方在于重复。
但是由于UT的代码比较简洁,很容易看清楚重复的地方,因此,对于这些重复代码的重构也一下子简单起来。(未完待续)

你可能感兴趣的:(框架,生活)