简单故事悖论

2023-10-13 18:30
文章标签 简单 故事 悖论

本文主要是介绍简单故事悖论,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我最近一直很感兴趣地关注 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编写的。

  • 该博客的代码可在Github上找到,网址为https://github.com/roghughe/captaindebug/tree/master/cargo-cult

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

这篇关于简单故事悖论的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/205076

相关文章

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

利用Python编写一个简单的聊天机器人

《利用Python编写一个简单的聊天机器人》这篇文章主要为大家详细介绍了如何利用Python编写一个简单的聊天机器人,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 使用 python 编写一个简单的聊天机器人可以从最基础的逻辑开始,然后逐步加入更复杂的功能。这里我们将先实现一个简单的

使用IntelliJ IDEA创建简单的Java Web项目完整步骤

《使用IntelliJIDEA创建简单的JavaWeb项目完整步骤》:本文主要介绍如何使用IntelliJIDEA创建一个简单的JavaWeb项目,实现登录、注册和查看用户列表功能,使用Se... 目录前置准备项目功能实现步骤1. 创建项目2. 配置 Tomcat3. 项目文件结构4. 创建数据库和表5.

使用PyQt5编写一个简单的取色器

《使用PyQt5编写一个简单的取色器》:本文主要介绍PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16进制颜色编码,一款跟随鼠标刷新图像的RGB和16... 目录取色器1取色器2PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16

四种简单方法 轻松进入电脑主板 BIOS 或 UEFI 固件设置

《四种简单方法轻松进入电脑主板BIOS或UEFI固件设置》设置BIOS/UEFI是计算机维护和管理中的一项重要任务,它允许用户配置计算机的启动选项、硬件设置和其他关键参数,该怎么进入呢?下面... 随着计算机技术的发展,大多数主流 PC 和笔记本已经从传统 BIOS 转向了 UEFI 固件。很多时候,我们也

基于Qt开发一个简单的OFD阅读器

《基于Qt开发一个简单的OFD阅读器》这篇文章主要为大家详细介绍了如何使用Qt框架开发一个功能强大且性能优异的OFD阅读器,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 目录摘要引言一、OFD文件格式解析二、文档结构解析三、页面渲染四、用户交互五、性能优化六、示例代码七、未来发展方向八、结论摘要

MyBatis框架实现一个简单的数据查询操作

《MyBatis框架实现一个简单的数据查询操作》本文介绍了MyBatis框架下进行数据查询操作的详细步骤,括创建实体类、编写SQL标签、配置Mapper、开启驼峰命名映射以及执行SQL语句等,感兴趣的... 基于在前面几章我们已经学习了对MyBATis进行环境配置,并利用SqlSessionFactory核

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个