14悖论
我最近一直很感兴趣地关注 Kent Beck(@kentbeck),David Heinemeier Hansson(@dhh)和Martin Fowler(@martinfowler)之间的#isTDDDead辩论。 我认为,可以以建设性的方式挑战通常被认为是理所当然的想法是特别有益的。 这样,您就可以确定他们是经得起审查还是跌落在脸上。
讨论从@dhh开始,就TDD和测试技术提出以下几点,我希望我是对的。 首先,TDD的严格定义包括以下内容:
- TTD用于驱动单元测试
- 你不能有合作者
- 您无法触摸数据库
- 您无法触摸文件系统
- 快速的单元测试,只需眨眼即可完成。
他继续说,因此您要通过使用模拟来驱动系统的体系结构,从而使体系结构遭受隔离和模拟所有事物的驱动器的破坏,而“红色,绿色,重构”周期的强制实施也是如此说明性的。 他还指出,很多人会误以为您对代码没有信心,并且无法通过测试交付增量功能,除非您经过这条精心准备的TDD铺装之路。
@Kent_Beck说,TDD不一定包含大量嘲笑,讨论仍在继续……
我在这里解释了一下; 但是,正是使用TDD的理解和经验上的差异让我开始思考。
TDD真的有问题吗,还是@dhh对其他开发人员对TDD的解释的经验? 我不想把单词放在@dhh的嘴里,但是似乎问题是TDD技术的教条式应用,即使它不适用也是如此。 我给人的印象是,在某些开发机构中,TDD的退化程度仅比Cargo Cult Programming少。
“货品崇拜编程”一词似乎源于我发现真正令人鼓舞的人,已故的理查德·费曼教授。 在 1974年加州理工学院毕业典礼上,他发表了一篇题为《 货物崇拜科学-关于科学,伪科学和学习如何不自欺欺人的言论》的论文。 后来这成为他的自传的一部分: 当然,你一定是在开玩笑Feynman先生 ,我恳求你读这本书。
Feynman在其中重点介绍了来自多个伪科学的实验,例如教育科学,心理学,超心理学和物理学,其中保持开放思维,质疑一切并寻找理论缺陷的科学方法已被信念,礼仪和信念所取代:愿意将他人的研究成果视为理所当然的代替。
费曼从1974年的论文中得出的结论将《货物崇拜科学》总结为:
“在南海,有一群人崇拜货物。 在战争中,他们看到飞机降落了很多优质的材料,他们希望现在也能发生同样的事情。 因此,他们已经安排模仿跑道之类的东西,沿着跑道的侧面放火,为一个人坐在一个木制的小屋里,头上有两个木块,像耳机,竹节像天线一样伸出–他是管制员–他们等待飞机降落。 他们做的一切正确。 形式是完美的。 它看起来完全像以前一样。 但这是行不通的。 没有飞机降落。 我之所以称其为货物崇拜科学,是因为它们遵循所有显而易见的科学调查规则和形式,但由于飞机无法降落,它们缺少了一些必不可少的东西。”
您可以将这种想法应用于编程,在其中您会发现团队和个人执行惯例化的程序并使用技术,而没有真正理解他们背后的理论,而是希望他们能够工作,并且因为他们是“正确的事情”。
在该系列的第二次演讲中,@ dhh举了一个例子,他称之为“测试引起的设计损坏” ,在此我感到很兴奋,因为我已经看过很多次了。 我对基本代码的唯一保留是,对我来说,它似乎并不是TDD产生的,这种说法似乎有点局限。 我想说这更多是“货物崇拜”编程的结果,这是因为在我遇到过此示例的情况下,并未使用TDD。
如果您看过要点 ,您可能知道我在说什么; 但是,该代码是用Ruby编写的,对此我几乎没有经验。 为了更详细地探讨这一点,我认为我将创建一个Spring MVC版本并从那里开始。
这里的场景是一个非常简单的故事:所有代码所做的就是从数据库中读取对象并将其放入模型中进行显示。 没有额外的处理,没有业务逻辑,也没有要执行的计算。 敏捷的故事将是这样的:
Title: View User Details
As an admin user
I want to click on a link
So that I can verify a user's details
在这个“适当的” N层示例中,我有一个User
模型对象,一个控制器和服务层以及DAO以及它们的接口和测试。
而且,这有一个悖论:您着手编写编写可能最好的代码,以使用众所周知的,并且可能是最受欢迎的MVC'N'层模式来实现故事,而对于这样一个简单的场景,最终结果是完全过头了。 就像@jhh所说的那样,某些东西已损坏。
在我的示例代码中,我正在使用JdbcTemplate类从MySQL数据库检索用户的详细信息,但是任何数据库访问API都可以。
这是示例代码,演示了实现故事的常规“正确”方法。 准备进行很多滚动操作...
public class User {
public static User NULL_USER = new User(-1, "Not Available", "", new Date());
private final long id;
private final String name;
private final String email;
private final Date createDate;
public User(long id, String name, String email, Date createDate) {
this.id = id;
this.name = name;
this.email = email;
this.createDate = createDate;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public Date getCreateDate() {
return createDate;
}
}
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/find1")
public String findUser(@RequestParam("user") String name, Model model) {
User user = userService.findUser(name);
model.addAttribute("user", user);
return "user";
}
}
public interface UserService {
public abstract User findUser(String name);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* @see com.captaindebug.cargocult.ntier.UserService#findUser(java.lang.String)
*/
@Override
public User findUser(String name) {
return userDao.findUser(name);
}
}
public interface UserDao {
public abstract User findUser(String name);
}
@Repository
public class UserDaoImpl implements UserDao {
private static final String FIND_USER_BY_NAME = "SELECT id, name,email,createdDate FROM Users WHERE name=?";
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* @see com.captaindebug.cargocult.ntier.UserDao#findUser(java.lang.String)
*/
@Override
public User findUser(String name) {
User user;
try {
FindUserMapper rowMapper = new FindUserMapper();
user = jdbcTemplate.queryForObject(FIND_USER_BY_NAME, rowMapper, name);
} catch (EmptyResultDataAccessException e) {
user = User.NULL_USER;
}
return user;
}
}
如果您看一下这段代码,反而看起来不错。 实际上,它看起来像是有关如何编写“ N”层MVC应用程序的经典教科书示例。 控制器将负责整理业务规则的责任传递给服务层,并且服务层使用数据访问对象从数据库中检索数据,该对象又使用RowMapper<>
帮助程序类来检索User
对象。 当控制器具有User
对象时,它将其注入模型中以供显示。 这种模式是清晰可扩展的。 我们通过使用接口将数据库与服务隔离开来,并将服务与控制器隔离开来,并且我们正在使用带有Mockito的JUnit和集成测试来测试所有内容。 这应该是教科书MVC编码中的硬道理吗? 让我们看一下代码。
首先,有不必要的接口使用。 有人会说切换数据库实现很容易,但是谁会这样做呢? 1加,现代嘲讽工具可以使用类定义是这样,除非您的设计明确要求同一个接口的多个实现创建自己的代理,然后使用接口是没有意义的。
接下来,有一个UserServiceImpl
,它是惰性类反模式的经典示例,因为它除了无意义地委托给数据访问对象外,不执行任何操作。 同样,控制器也很懒惰,因为它在将结果User
类添加到模型之前将其委托给lazy UserServiceImpl
:实际上,所有这些类都是lazy类反模式的示例。
编写了一些惰性类之后,现在就可以对它们进行不必要的测试,包括非事件UserServiceImpl
类。 只需要为实际上执行某些逻辑的类编写测试。
public class UserControllerTest {
private static final String NAME = "Woody Allen";
private UserController instance;
@Mock
private Model model;
@Mock
private UserService userService;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
instance = new UserController();
ReflectionTestUtils.setField(instance, "userService", userService);
}
@Test
public void testFindUser_valid_user() {
User expected = new User(0L, NAME, "aaa@bbb.com", new Date());
when(userService.findUser(NAME)).thenReturn(expected);
String result = instance.findUser(NAME, model);
assertEquals("user", result);
verify(model).addAttribute("user", expected);
}
@Test
public void testFindUser_null_user() {
when(userService.findUser(null)).thenReturn(User.NULL_USER);
String result = instance.findUser(null, model);
assertEquals("user", result);
verify(model).addAttribute("user", User.NULL_USER);
}
@Test
public void testFindUser_empty_user() {
when(userService.findUser("")).thenReturn(User.NULL_USER);
String result = instance.findUser("", model);
assertEquals("user", result);
verify(model).addAttribute("user", User.NULL_USER);
}
}
public class UserServiceTest {
private static final String NAME = "Annie Hall";
private UserService instance;
@Mock
private UserDao userDao;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
instance = new UserServiceImpl();
ReflectionTestUtils.setField(instance, "userDao", userDao);
}
@Test
public void testFindUser_valid_user() {
User expected = new User(0L, NAME, "aaa@bbb.com", new Date());
when(userDao.findUser(NAME)).thenReturn(expected);
User result = instance.findUser(NAME);
assertEquals(expected, result);
}
@Test
public void testFindUser_null_user() {
when(userDao.findUser(null)).thenReturn(User.NULL_USER);
User result = instance.findUser(null);
assertEquals(User.NULL_USER, result);
}
@Test
public void testFindUser_empty_user() {
when(userDao.findUser("")).thenReturn(User.NULL_USER);
User result = instance.findUser("");
assertEquals(User.NULL_USER, result);
}
}
public class UserDaoTest {
private static final String NAME = "Woody Allen";
private UserDao instance;
@Mock
private JdbcTemplate jdbcTemplate;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
instance = new UserDaoImpl();
ReflectionTestUtils.setField(instance, "jdbcTemplate", jdbcTemplate);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testFindUser_valid_user() {
User expected = new User(0L, NAME, "aaa@bbb.com", new Date());
when(jdbcTemplate.queryForObject(anyString(), (RowMapper) anyObject(), eq(NAME))).thenReturn(expected);
User result = instance.findUser(NAME);
assertEquals(expected, result);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testFindUser_null_user() {
when(jdbcTemplate.queryForObject(anyString(), (RowMapper) anyObject(), isNull())).thenReturn(User.NULL_USER);
User result = instance.findUser(null);
assertEquals(User.NULL_USER, result);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testFindUser_empty_user() {
when(jdbcTemplate.queryForObject(anyString(), (RowMapper) anyObject(), eq(""))).thenReturn(User.NULL_USER);
User result = instance.findUser("");
assertEquals(User.NULL_USER, result);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml",
"file:src/test/resources/test-datasource.xml" })
public class UserControllerIntTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void testFindUser_happy_flow() throws Exception {
ResultActions resultActions = mockMvc.perform(get("/find1").accept(MediaType.ALL).param("user", "Tom"));
resultActions.andExpect(status().isOk());
resultActions.andExpect(view().name("user"));
resultActions.andExpect(model().attributeExists("user"));
resultActions.andDo(print());
MvcResult result = resultActions.andReturn();
ModelAndView modelAndView = result.getModelAndView();
Map<String, Object> model = modelAndView.getModel();
User user = (User) model.get("user");
assertEquals("Tom", user.getName());
assertEquals("tom@gmail.com", user.getEmail());
}
}
在编写此示例代码时,我将所有可以想到的内容添加到了组合中。 你可能会认为这个例子是“洁癖”在其建设特别是与包括冗余接口,但我已经看到了这样的代码。
这种模式的好处在于它遵循了大多数开发人员所理解的独特设计。 干净且可扩展。 缺点是有很多类。 更多的类需要花费更多的时间来编写,并且您必须维护或增强此代码,因此更难掌握。
那么,有什么解决方案? 这很难回答。 在#IsTTDDead辩论中,@ dhh提供的解决方案是将所有代码放在一个类中,将数据访问与模型填充混合在一起。 如果您针对我们的用户案例实施此解决方案,您仍然会获得一个User
类,但是所需的类数量将大大减少。
@Controller
public class UserAccessor {
private static final String FIND_USER_BY_NAME = "SELECT id, name,email,createdDate FROM Users WHERE name=?";
@Autowired
private JdbcTemplate jdbcTemplate;
@RequestMapping("/find2")
public String findUser2(@RequestParam("user") String name, Model model) {
User user;
try {
FindUserMapper rowMapper = new FindUserMapper();
user = jdbcTemplate.queryForObject(FIND_USER_BY_NAME, rowMapper, name);
} catch (EmptyResultDataAccessException e) {
user = User.NULL_USER;
}
model.addAttribute("user", user);
return "user";
}
private class FindUserMapper implements RowMapper<User>, Serializable {
private static final long serialVersionUID = 1L;
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User(rs.getLong("id"), //
rs.getString("name"), //
rs.getString("email"), //
rs.getDate("createdDate"));
return user;
}
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml",
"file:src/test/resources/test-datasource.xml" })
public class UserAccessorIntTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void testFindUser_happy_flow() throws Exception {
ResultActions resultActions = mockMvc.perform(get("/find2").accept(MediaType.ALL).param("user", "Tom"));
resultActions.andExpect(status().isOk());
resultActions.andExpect(view().name("user"));
resultActions.andExpect(model().attributeExists("user"));
resultActions.andDo(print());
MvcResult result = resultActions.andReturn();
ModelAndView modelAndView = result.getModelAndView();
Map<String, Object> model = modelAndView.getModel();
User user = (User) model.get("user");
assertEquals("Tom", user.getName());
assertEquals("tom@gmail.com", user.getEmail());
}
@Test
public void testFindUser_empty_user() throws Exception {
ResultActions resultActions = mockMvc.perform(get("/find2").accept(MediaType.ALL).param("user", ""));
resultActions.andExpect(status().isOk());
resultActions.andExpect(view().name("user"));
resultActions.andExpect(model().attributeExists("user"));
resultActions.andExpect(model().attribute("user", User.NULL_USER));
resultActions.andDo(print());
}
}
上面的解决方案将第一类的数量减少为两个:实现类和测试类。 所有测试方案都是在很少的端到端集成测试中满足的。 这些测试将访问数据库,但是在这种情况下会很糟糕吗? 如果每次到数据库的行程大约花费20毫秒或更短的时间,那么它们仍然会在几分之一秒内完成; 那应该足够快。
在增强或维护该代码方面,一个单一的小类比几个甚至更小的类更容易学习。 如果确实必须添加一堆业务规则或其他复杂性,那么将此代码更改为“ N”层模式将不会很困难; 但是问题是,如果/当需要进行更改时,可能会给经验不足的开发人员,该开发人员没有足够的信心进行必要的重构。 结果是,而且您肯定已经看过很多次了,新的更改可能会在这种一类解决方案的基础上出现问题,从而导致混乱的意大利面条式代码。
在实施这样的解决方案时,您可能不会很受欢迎,因为代码是非常规的。 这就是我认为这种单一类解决方案引起很多人争议的原因之一。 正是在每种情况下都严格应用的标准“正确方式”和“错误方式”编写代码的想法导致了这种完美的设计成为问题。
我想这全是课程的事 。 为正确的情况选择正确的设计。 如果我要执行一个复杂的故事,那么我会毫不犹豫地分担各种职责,但是在简单的情况下,这是不值得的。 因此,在结束之前,我想问问是否有人对上面显示的“ 简单故事悖论”有更好的解决方案,请告诉我。
1在过去的十年编程中,我曾经从事过一个项目,其中基础数据库已更改以满足客户需求。 那是很多年,数千里之外,并且代码是用C ++和Visual Basic编写的。
- 此博客的代码可在Github上找到,网址为https://github.com/roghughe/captaindebug/tree/master/cargo-cult
翻译自: https://www.javacodegeeks.com/2014/06/the-simple-story-paradox.html
14悖论