实践中的重构16_多即是少
程序员文章站
2022-03-03 18:39:43
...
在编写UT的过程中,随处可见重复,硬编码等等使得代码僵化的代码。
UT也是代码,并且其重要性和产品代码相比,只高不低。
以下是一段产品代码的框架。
这段代码提供了用户分页查询功能。但是用户的信息是保存在不同的数据库里面的。所以,要由应用程序来处理历史库和生产库的查询定向和分页组装功能。
稍稍分析一下,这里主要有如下情况需要处理:
1 时间范围在历史库中,只查询历史库。
2 时间范围在生产库中,只查询生产库。
3 时间范围跨库,但是结果数据只在历史库中。
4 时间范围跨库,结果数据一部分在历史库中,一部分在生产库中,需要做拼接处理。
5 时间范围跨库,但是结果数据只在生产库中,视页面边界是否和数据边界对齐的情况要分别处理。
原始的UT的思路是使用JMock,不同的场景mock出不同的manager。导致UT的可读性比较差。代码中充斥着硬编码,重复等不好的味道。
mock本来是为了简化生活的,但是这个地方的mock却是越看越别扭。
这个时候,我想起了“少即是多”这句名言,少即是多,那么多即是少。如果不使用mock,直接写一个简单的manager实现,这样一来,UT应该更简明。
manager的实现如上,其思路如下:
1 在内存中存储用户对象。模拟一个简单的用户数据库。
2 使用构造函数来构造该manager的数据,包括历史库时间分界点,历史库和生产库用户数多少,自动构建用户。
3 构建用户的同时,设置一个特殊值,用来验证返回结果。
4 实现了manager的接口方法。
构思和实现这个UT的架构比较费时间,但是一旦完成了,UT的编写就是简单到了极点。下面的UT写起来基本不用费什么脑力。这样,我们通过编写一个相对复杂的内存实现,简化了UT的编写。
UT如下:
每一个UT都很简洁,唯一不足的地方在于重复。
但是由于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的代码比较简洁,很容易看清楚重复的地方,因此,对于这些重复代码的重构也一下子简单起来。(未完待续)
下一篇: 实践中的重构12_不要乱用异常
推荐阅读