简单故事悖论

我最近一直很感兴趣地关注 Kent Beck(@kentbeck),David Heinemeier Hansson(@dhh)和Martin Fowler(@martinfowler)之间的#isTDDDead辩论。 我认为,以建设性的方式挑战通常被认为是理所当然的想法是特别有益的。 这样,您就可以确定他们是经得起审查还是跌落在脸上。

讨论从@dhh开始,就TDD和测试技术提出以下几点,我希望我是对的。 首先,TDD的严格定义包括以下内容:

  1. TTD用于驱动单元测试
  2. 你不能有合作者
  3. 你不能碰数据库
  4. 您无法触摸文件系统
  5. 快速的单元测试,只需眨眼即可完成。

他继续说,因此,您通过使用模拟来驱动系统的体系结构,从而使体系结构遭受隔离和模拟所有事物的驱动器的损害,而“红色,绿色,重构”周期的强制实施也是如此说明性的。 他还指出,很多人会误以为您对代码没有信心,并且无法通过测试交付增量功能,除非您经过这条精心设计的,精心设计的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数据库检索用户的详细信息,但是任何DB访问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编写的。

翻译自: https://www.javacodegeeks.com/2014/06/the-simple-story-paradox.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值