认识
1.基本规则
- 当且仅当存在失败的自动化测试时,才开始编写生产代码
- 消除重复(消除坏味道)
2.三步骤
红 / 绿 / 重构
- 红:编写一个失败的小测试,甚至可以是无法编译的测试
- 绿:让这个测试快速通过,甚至不惜犯下任何罪恶
- 重构:消除上一步中产生的所有重复(坏味道)
3.任务分解法
- 构思软件被使用的方式
- 构思功能的实现方式,划分所需组件以及组件间的关系(没思路,可以不划分)
- 根据需求的功能描述拆分功能点,功能点考虑正确路径(Happy Path),边界路径(Side Path)
- 依照组件以及组件间的关系,将功能拆分到对应组件
- 针对拆分的结果编写测试,进入红 / 绿 / 重构循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pErGbuY-1647857672466)(./images/TDD整体工作流程.jpg)]
总结
-
TDD是三项已有技术的重组:先大概设计,再落地测试,再重构出最终代码
- 设计能力:软件设计原则/思想/模式
- 测试能力:测试技术/方法/工具
- 重构能力:代码坏味道,重构方法/工具
-
需求分析:
-
任务列表:从无到有实现各个功能点
任务列表是一个随代码结构(重构)、测试策略(在哪个范围内测试)、代码实现情况(存在哪些缺陷)等因素而动态调整的列表。它的内容体现了我们最新的认知,它的变化记录了我们认知改变的过程。
-
测试列表:通过所有测试即表示实现功能
-
-
为什么一定要先看到红灯?
- 看到测试以预期的方式出错
- 红灯表示缺少功能/实现错误
-
为什么一定要看到绿灯?
- 用尽可能简洁的代码使当前所有测试通过
-
为什么一定要重构?
- 弥补为了快速看到绿灯所犯的过错
- 每次重构都要运行所有测试,确保绿灯!一旦红灯,回退到绿灯再重构!
项目1命令行参数解析
需求:
- 传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:
-l -p 8080 -d /usr/logs:
“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为 true,不存在则为 false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。
标志后面如果存在多个值,则该标志表示一个列表:-g this is a list -d 1 2 -3 5:
"g"表示一个字符串列表[“this”, “is”, “a”, “list”],“d"标志表示一个整数列表[1, 2, -3, 5]。
如果参数中没有指定某个标志,那么解析器应该指定一个默认值。
例如,false 代表布尔值,0 代表数字,"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。"
01 | 任务分解法与整体工作流程
1.API 构思与组件划分
-
确定API形式:使用者将以什么方式使用这个代码,这段代码的整体对外接口部分
-
思考功能如何实现
示例:该实战可以采用如下两种甚至你能想到的其他最简实现方式
// 第一种:通过数组下标的形式 [-l],[-p, 8080],[-d, /usr/logs] // 第二种:采用Map的形式 {-l:[], -p:8080, -d: /us/logs}
2.功能分解与任务列表
-
根据上面的构思,得到的是一个大的需求,但是由于这样跨度太大,开发过程中可能会导致很多细节的问题被忽略,因此需要将他划分为更小的粒度。
该实战,将功能分解为单个值功能和列表功能
-
在根据功能的分解拆分为任务列表,通过TODO列表,在划分为一个个小的TODO,示例:
- 单个值情况
- 列表情况
- 边界情况:只有一个值后面没跟值,有多个值
- 默认情况(默认值)
// Single Option: // TODO: - Bool -l // TODO: - Integer -p 8080 // TODO: - String -d /usr/logs // multi Options: -l - p 8080 -d /usr/logs // sad path: // - bool -l t / -l t f // - int -p / -p 8080 8081 // - string -d / -d /usr/logs /usr/test // default value // - bool: false // -int: 0 // -string:""
3.红绿灯循环
-
Single Option做红绿灯测试
示例:
- 刚开始parse返回null,测试红灯
- 根据已知条件,添加如下代码为绿灯
public class Args { public static <T> T parse(Class<T> optionsClass, String... args) { Constructor<?> constructor = optionsClass.getDeclaredConstructors()[0]; try { return (T) constructor.newInstance(true); } catch(Exception e) { throw new RuntimeException(e); } } }
@Test public void should_set_boolean_option_to_true_if_flag_present() { BooleanOption option = Args.parse(BooleanOption.class, "-l"); assertTrue(option.logging()); } static record BooleanOption(@Option("l") boolean logging) {}
-
红灯
@Test public void should_set_boolean_option_to_false_if_flag_not_present() { BooleanOption option = Args.parse(BooleanOption.class); assertFalse(option.logging()); }
对代码更改,实现绿灯,再次进行两个测试
public static <T> T parse(Class<T> optionsClass, String... args) { try { Constructor<?> constructor = optionsClass.getDeclaredConstructors()[0]; Parameter parameter = constructor.getParameters()[0]; Option option = parameter.getAnnotation(Option.class); List<String> arguments = Arrays.asList(args); return (T) constructor.newInstance(arguments.contains("-" + option.value())); } catch(Exception e) { throw new RuntimeException(e); } }
-
在对Integer和String进行相同的测试
- 先编写测试,红灯
- 在进行代码修改,绿灯
- 重复上述步骤直到功能点完成
-
对一组数据进行测试
-
对一组数据编写测试,红灯
@Test public void should_parse_multi_options() { Options options = Args.parse(Options.class, "-l", "-p", "8080", "-d", "/usr/logs"); assertTrue(options.logging()); assertEquals(8080, options.port()); assertEquals("/usr/logs", options.directory()); }
-
修改,绿灯
public class Args { public static <T> T parse(Class<T> optionsClass, String... args) { try { List<String> arguments = Arrays.asList(args); Constructor<?> constructor = optionsClass.getDeclaredConstructors()[0]; Object[] values = Arrays.stream(constructor.getParameters()).map(it -> parseOption(it, arguments)).toArray(); return (T) constructor.newInstance(values); } catch(Exception e) { throw new RuntimeException(e); } } private static Object parseOption(Parameter parameter, List<String> arguments) { Option option = parameter.getAnnotation(Option.class); Object value = null; if (parameter.getType() == boolean.class) { value = arguments.contains("-" + option.value()); } if (parameter.getType() == int.class) { int index = arguments.indexOf("-" + option.value()); value = Integer.parseInt(arguments.get(index + 1)); } if (parameter.getType() == String.class) { int index = arguments.indexOf("-" + option.value()); value = String.valueOf(arguments.get(index + 1)); } return value; } }
-
-
总结
通过红绿灯循环,逐步的完善功能
前面对boolean、int、string的测试,最终成为解决针对不同类型参数方法的处理
从而最终实现对一组数据的解析
-
最终代码
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Option { String value(); }
public class Args { public static <T> T parse(Class<T> optionsClass, String... args) { try { List<String> arguments = Arrays.asList(args); Constructor<?> constructor = optionsClass.getDeclaredConstructors()[0]; Object[] values = Arrays.stream(constructor.getParameters()).map(it -> parseOption(it, arguments)).toArray(); return (T) constructor.newInstance(values); } catch(Exception e) { throw new RuntimeException(e); } } private static Object parseOption(Parameter parameter, List<String> arguments) { Option option = parameter.getAnnotation(Option.class); Object value = null; if (parameter.getType() == boolean.class) { value = arguments.contains("-" + option.value()); } if (parameter.getType() == int.class) { int index = arguments.indexOf("-" + option.value()); value = Integer.parseInt(arguments.get(index + 1)); } if (parameter.getType() == String.class) { int index = arguments.indexOf("-" + option.value()); value = String.valueOf(arguments.get(index + 1)); } return value; } }
public class ArgsTest { @Test public void should_set_boolean_option_to_true_if_flag_present() { BooleanOption option = Args.parse(BooleanOption.class, "-l"); assertTrue(option.logging()); } @Test public void should_set_boolean_option_to_false_if_flag_not_present() { BooleanOption option = Args.parse(BooleanOption.class); assertFalse(option.logging()); } static record BooleanOption(@Option("l") boolean logging) {} @Test public void should_parse_int_as_option_value() { IntegerOption option = Args.parse(IntegerOption.class, "-p","8080"); assertEquals(8080, option.port()); } static record IntegerOption(@Option("p") int port) {} @Test public void should_get_string_as_option_value() { StringOption option = Args.parse(StringOption.class, "-d","/usr/logs"); assertEquals("/usr/logs", option.directory()); } static record StringOption(@Option("d") String directory) {} @Test public void should_parse_multi_options() { Options options = Args.parse(Options.class, "-l", "-p", "8080", "-d", "/usr/logs"); assertTrue(options.logging()); assertEquals(8080, options.port()); assertEquals("/usr/logs", options.directory()); } static record Options(@Option("l") boolean logging, @Option("p") int port, @Option("d") String directory) {} }
02 | 识别坏味道与代码重构
重构:保持小步骤且稳定的节奏,逐步完成重构,而不是按照目标对代码进行重写
完成前面的测试有两个选择:继续完善功能;进入重构
前提条件:
- 测试都是绿的
- 坏味道足够明细
目前项目问题:
-
存在多个分支条件,随着支持类型越多,分支越多
if (parameter.getType() == boolean.class) { value = arguments.contains("-" + option.value()); } if (parameter.getType() == int.class) { int index = arguments.indexOf("-" + option.value()); value = Integer.parseInt(arguments.get(index + 1)); } if (parameter.getType() == String.class) { int index = arguments.indexOf("-" + option.value()); value = arguments.get(index + 1); }
1.引入多态接口
-
将分支里的逻辑抽为方法(隔离需要变化的地方)
-
提取为接口,并实现接口,实现多态替换
-
注意在这个过程中,也需要进行测试,
这里虽然没有修改代码逻辑,但是由于修改了代码结构,避免出错,因此最好还是进行测试
private static Object parseOption(Parameter parameter, List<String> arguments) { Option option = parameter.getAnnotation(Option.class); Object value = null; if (parameter.getType() == String.class) { value = parseString(arguments, option); } return value; } // 省略... interface OptionParser { Object parser(List<String> arguments, Option option); } private static Object parseString(List<String> arguments, Option option) { return new StringOptionParser().parser(arguments, option); } static class StringOptionParser implements OptionParser { @Override public Object parser(List<String> arguments, Option option) { int index = arguments.indexOf("-" + option.value()); return String.valueOf(arguments.get(index + 1)); } }
-
在这个过程中,将代码进一步逻辑,使目光最终放在主要的处理逻辑上
private static Object parseOption(Parameter parameter, List<String> arguments) { return getOptionParser(parameter.getType()).parse(arguments, parameter.getAnnotation(Option.class)); } private static OptionParser getOptionParser(Class<?> type) { OptionParser parser = null; if (type == boolean.class) { parser = new BooleanOptionParser(); } if (type == int.class) { parser = new IntOptionParser(); } if (type == String.class) { parser = new StringOptionParser(); } return parser; }
2.使用“抽象工厂”模式的变体来替换分支(消除分支)
由于这里不能对class修改,因此不能使用多态进行重构,只能利用工厂来重构
-
查表法:
private static Object parseOption(Parameter parameter, List<String> arguments) { return getOptionParser(parameter.getType()).parse(arguments, parameter.getAnnotation(Option.class)); } // 定义一个Map,直接来查找相关类型 private static Map<Class<?>, OptionParser> PARSERS = Map.of( boolean.class, new BooleanOptionParser(), int.class, new IntOptionParser(), String.class, new StringOptionParser() ); private static OptionParser getOptionParser(Class<?> type) { return PARSERS.get(type); } // 最后在内联过去, private static Object parseOption(Parameter parameter, List<String> arguments) { return PARSERS.get(parameter.getType()).parse(arguments, parameter.getAnnotation(Option.class)); } private static Map<Class<?>, OptionParser> PARSERS = Map.of( boolean.class, new BooleanOptionParser(), int.class, new IntOptionParser(), String.class, new StringOptionParser() );
3.使用Function特性(消除代码重复)
注:也可以使用策略模式
重复代码:两种只有转换的地方不同,其他相同
class StringOptionParser implements OptionParser {
@Override
public Object parse(List<String> arguments, Option option) {
int index = arguments.indexOf("-" + option.value());
String value = arguments.get(index + 1);
return String.valueOf(value);
}
}
class IntOptionParser implements OptionParser {
@Override
public Object parse(List<String> arguments, Option option) {
int index = arguments.indexOf("-" + option.value());
String value = arguments.get(index + 1);
return Integer.parseInt(value);
}
}
-
将不变的抽取为方法
class IntOptionParser implements OptionParser { @Override public Object parse(List<String> arguments, Option option) { int index = arguments.indexOf("-" + option.value()); return parseValue(arguments.get(index + 1)); } private int parseValue(String value) { return Integer.parseInt(value); } }
-
通过继承,重写的方式,实现消除代码
注:需要修改访问权限为protected,这样才能重写
class StringOptionParser extends IntOptionParser { @Override public Object parse(List<String> arguments, Option option) { int index = arguments.indexOf("-" + option.value()); return parseValue(arguments.get(index + 1)); } @Override protected Object parseValue(String value) { return String.valueOf(value); } }
-
通过Function实现去除重复代码parse
class IntOptionParser implements OptionParser { /** * 默认值 */ Function<String, Object> valueParse = Integer::parseInt; public IntOptionParser() { } public IntOptionParser(Function<String, Object> valueParse) { this.valueParse = valueParse; } @Override public Object parse(List<String> arguments, Option option) { int index = arguments.indexOf("-" + option.value()); return valueParse.apply(arguments.get(index + 1)); } } class StringOptionParser extends IntOptionParser { public StringOptionParser() { super(String::valueOf); } }
4.利用工厂方法,消除代码
可以发现StringOptionParser只有一个String::valueOf的功能,因此可以将其重构,只使用IntOptionParser实现两者功能
换成工厂方法的原因:可以inLine,构造函数无法被inLine
-
工厂方法替换构造函数 && 替换为接口类型
class StringOptionParser extends IntOptionParser { private StringOptionParser() { super(String::valueOf); } public static OptionParser createStringOptionParser() { return new StringOptionParser(); } }
-
可以发现,只有createStringOptionParser,使用了StringOption的构造函数,由于接受类型是个接口,因此可以写成下面格式
class StringOptionParser extends IntOptionParser { public static OptionParser createStringOptionParser() { return new IntOptionParser(String::valueOf); } }
-
在将StringOption的createStringOptionParser内联,发现StringOptionParser没用,因此可以删掉,最终调用如下
private static Map<Class<?>, OptionParser> PARSERS = Map.of( boolean.class, new BooleanOptionParser(), int.class, new IntOptionParser(), String.class, new IntOptionParser(String::valueOf) );
-
同理,修改IntOptionParser
目的:去除无参构造函数
-
最终代码
private static Map<Class<?>, OptionParser> PARSERS = Map.of( boolean.class, new BooleanOptionParser(), int.class, new SingleOptionParser<>(Integer::parseInt), String.class, new SingleOptionParser<>(String::valueOf) );
class SingleOptionParser<T> implements OptionParser { Function<String, T> valueParse; public SingleOptionParser(Function<String, T> valueParse) { this.valueParse = valueParse; } @Override public T parse(List<String> arguments, Option option) { int index = arguments.indexOf("-" + option.value()); return valueParse.apply(arguments.get(index + 1)); } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RWAmXydR-1647857672467)(./images/代码结构.jpg)]
03 | 按测试策略重组测试
1.调整任务列表
原因:最开始定义任务列表的时候,没有进行重构,系统只有Args类,当重构后,提取了Option接口和两个实现类。
因此当再去测试时,存在两个不同选择:
-
继续针对 Args 进行测试
-
直接对 BooleanOptionParser 进行测试
//1.Args @Test public void should_not_accept_extra_argument_for_boolean_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> Args.parse(BooleanOption.class, "-l", "t")); assertEquals("l", e.getOption()); } //2.BooleanOptionParser @Test public void should_not_accept_extra_argument_for_boolean_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new BooleanOptionParser().parse("-l", "t", option("l"))); assertEquals("l", e.getOption()); }
-
这两个测试,测试的功能是一样的,但是测试范围不同
可以选择粒度更小的测试,这样更有益于问题的定位
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxrsQxbn-1647857672468)(./images/命令行参数重构测试-范围.jpg)]
-
修改任务列表
BooleanOptionParserTest: // sad path: // TODO: -bool -l t / -l t f // default: // TODO: - bool : false SingleValuedOptionParserTest: // sad path: // TODO: - int -p/ -p 8080 8081 // TODO: - string -d/ -d /usr/logs /usr/vars // default value: // TODO: -int :0 // TODO: - string ""
2.红绿灯循环
BooleanOptionParserTest
-
多个值
@Test public void should_not_accept_extra_argument_for_boolean_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new BooleanOptionParser().parse(asList("-l", "t"), option("l"))); assertEquals("l", e.getOption()); }
-
默认值情况
@Test public void should_set_default_value_to_false_if_option_not_present() { assertFalse(new BooleanOptionParser().parse(asList(), option("l"))); }
SingleValuedOptionParserTest
-
多个值
@Test public void should_not_accept_extra_argument_for_single_value_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new SingleOptionParser<>(Integer::parseInt).parse(asList("-p", "8080", "8081"), option("p"))); assertEquals("p", e.getOption()); }
-
-p 后面 跟-l / 缺少值
@ParameterizedTest @ValueSource(strings = {"-p -l", "-p"}) public void should_not_accept_insufficient_argument_for_single_value_option(String arguments) { InsufficientArgumentEception e = assertThrows(InsufficientArgumentEception.class, () -> new SingleOptionParser<>(Integer::parseInt).parse(asList(arguments.split(",")), option("p"))); assertEquals("p", e.getOption()); }
-
默认值
@Test public void should_set_default_value_to_0_if_option_not_present() { assertEquals(0, new SingleOptionParser<Integer>(0, Integer::parseInt).parse(asList(), option("p"))); }
-
多值字符串
可以直接通过,原因:重构后实现都是SingleOptionParser类,因此两个多值其实是等效的
@Test public void should_not_accept_extra_argument_for_string_single_value_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new SingleOptionParser<>("",String::valueOf).parse(asList("-d", "/usr/logs", "/usr/vars"), option("d"))); assertEquals("d", e.getOption()); }
3.按照测试策略重组测试
重构测试,更加清晰展示意图
- 重构后,整数类型和字符串类型的异常场景中,差异仅仅在于如何构造 SingleValuedOptionParser:
new SingleValuedOptionParser(0, Integer:parseInt)
new SingleValuedOptionParser("", String::valueOf)
-
仅仅是测试代码的差别,而被测试的代码则没有任何区别。按照任务列表,再构造其他场景的测试,也仅仅是不同测试数据的重复而已。所以将剩余任务从列表中取消就好了。
-
对比经过重构之后新写的测试,就会发现对于类似的功能,测试的出发点和测试的范围都有不同,这是一种坏味道。需要对测试进行重构,以消除这些不一致:
public class BooleanOptionParserTest { @Test// side path public void should_not_accept_extra_argument_for_boolean_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new BooleanOptionParser().parse(asList("-l", "t"), option("l"))); assertEquals("l", e.getOption()); } @Test // default value public void should_set_default_value_to_false_if_option_not_present() { assertFalse(new BooleanOptionParser().parse(asList(), option("l"))); } @Test // happy path public void should_set_default_value_to_true_if_option_present() { assertTrue(new BooleanOptionParser().parse(asList("-l"), option("l"))); } static Option option (String value){ return new Option() { @Override public Class<? extends Annotation> annotationType() { return Option.class; } @Override public String value() { return value; } }; } }
public class SingleValuedOptionParserTest { @Test // side_path public void should_not_accept_extra_argument_for_single_value_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new SingleOptionParser<>(Integer::parseInt).parse(asList("-p", "8080", "8081"), option("p"))); assertEquals("p", e.getOption()); } @ParameterizedTest // side_path @ValueSource(strings = {"-p -l", "-p"}) public void should_not_accept_insufficient_argument_for_single_value_option(String arguments) { InsufficientArgumentEception e = assertThrows(InsufficientArgumentEception.class, () -> new SingleOptionParser<>(0, String::valueOf).parse(asList(arguments.split(" ")), option("p"))); assertEquals("p", e.getOption()); } @Test //default_path public void should_set_default_value_for_single_value_option() { Function<String, Object> whatever = (it) -> null; Object defaultValue = new Object(); assertSame(defaultValue, new SingleOptionParser<>(defaultValue, whatever).parse(asList(), option("p"))); } @Test // happy_path public void should_parse_value_if_flag_present() { Object parsed = new Object(); Function<String, Object> parse = (it) -> parsed; Object whatever = new Object(); assertSame(parsed, new SingleOptionParser<>(whatever, parse).parse(asList("-p", "8080"), option("p"))); } static Option option (String value){ return new Option() { @Override public Class<? extends Annotation> annotationType() { return Option.class; } @Override public String value() { return value; } }; } }
public class ArgsTest { // multi Options: -l - p 8080 -d /usr/logs @Test public void should_parse_multi_options() { Options options = Args.parse(Options.class, "-l", "-p", "8080", "-d", "/usr/logs"); assertTrue(options.logging()); assertEquals(8080, options.port()); assertEquals("/usr/logs", options.directory()); } static record Options(@Option("l") boolean logging, @Option("p") int port, @Option("d") String directory) {} }
4.红绿灯循环
目前问题:
- parseOption获取的注解不在返回空指针
TDD:
-
在TDD中,实现一个测试来展示这个问题
@Test public void should_throw_illegal_option_exception_if_annotation_not_present() { IllegalOptionException e = assertThrows(IllegalOptionException.class, () -> Args.parse(IllegalOptions.class, "-l", "-p", "8080", "-d", "/usr/logs")); assertEquals("port", e.getParameter()); } static record IllegalOptions(@Option("l") boolean logging, int port, @Option("d") String directory) {}
04 |实现对于列表参数的支持
实现之前,在检查代码是否有坏味道,是否需要重构
1.不易察觉的坏味道
不易察觉的坏味道,意图也不直观
if (index + 1 == arguments.size() ||
arguments.get(index + 1).startsWith("-")) throw new InsufficientArgumentException(option.value());
if (index + 2 < arguments.size() &&
!arguments.get(index + 2).startsWith("-")) throw new TooManyArgumentsException(option.value());
定义方法代替注释
-
一般都会通过添加注释来说明,如下:
// -p -l(参数不足情况) if (index + 1 == arguments.size() || arguments.get(index + 1).startsWith("-")) throw new InsufficientArgumentException(option.value()); // -p 8080 8081(参数给多情况) if (index + 2 < arguments.size() && !arguments.get(index + 2).startsWith("-")) throw new TooManyArgumentsException(option.value());
-
可以采用添加代码注释的方式(抽取方法,让方法名成为注释)
随着代码写注释,可以通过定义方法的方式实现
需要很强的实现细节,才编写文档
if (isReachEndOfList(arguments, index) || secondArgumentIsNotAtFlag(arguments, index)) { throw new InsufficientArgumentEception(option.value()); } return valueParse.apply(arguments.get(index + 1)); } private boolean secondArgumentIsNotAtFlag(List<String> arguments, int index) { return arguments.get(index + 1).startsWith("-"); } private boolean isReachEndOfList(List<String> arguments, int index) { return index + 1 == arguments.size(); }
变化实现方式,使实现直观
-
问题:方法不直观,通过变化实现方式使本身直观
int followingFlag = IntStream.range(index + 1, arguments.size()) .filter(it -> arguments.get(it).startsWith("-")) .findFirst().orElse(arguments.size()); List<String> values = arguments.subList(index + 1, followingFlag); if (values.size() < 1) throw new InsufficientArgumentEception(option.value()); if (values.size() > 1) throw new TooManyArgumentsException(option.value()); // 将获取值进行封装 List<String> values = valuesFrom(arguments, index); if (values.size() < 1) throw new InsufficientArgumentEception(option.value()); if (values.size() > 1) throw new TooManyArgumentsException(option.value());
Optional消除重复代码
-
BooleanOptionParser 和 SingleValuedOptionParser 之间存在隐含的重复的代码
很显然Boolean和Single两者之间是有相似代码的
-
可以通过构造interface消除
-
通过JDK8新特性Optional消除
-
修改SingleValuedOptionParser
这里将魔法值1抽取出来,实现动态修改预期值,从而达到通用
@Override public T parse(List<String> arguments, Option option) { Optional<List<String>> argumentList; int expectedSize = 1; int index = arguments.indexOf("-" + option.value()); if (index == -1) { argumentList = Optional.empty(); } else { List<String> values = valuesFrom(arguments, index); if (values.size() < expectedSize) { throw new InsufficientArgumentEception(option.value()); } if (values.size() > expectedSize) { throw new TooManyArgumentsException(option.value()); } argumentList = Optional.of(values); } return argumentList.map(it -> parseValue(option, it.get(0))).orElse(defaultValue); } private T parseValue(Option option, String value) { return valueParse.apply(value); }
抽取方法,实现通用逻辑
@Override public T parse(List<String> arguments, Option option) { return valueForm(arguments, option, 1).map(it -> parseValue(option, it.get(0))).orElse(defaultValue); } private Optional<List<String>> valueForm(List<String> arguments, Option option, int expectedSize) { int index = arguments.indexOf("-" + option.value()); if (index == -1) { return Optional.empty(); } List<String> values = valuesFrom(arguments, index); if (values.size() < expectedSize) { throw new InsufficientArgumentEception(option.value()); } if (values.size() > expectedSize) { throw new TooManyArgumentsException(option.value()); } return Optional.of(values); }
-
BooleanOption修改
class BooleanOptionParser implements OptionParser<Boolean> { @Override public Boolean parse(List<String> arguments, Option option) { return valuesFrom(arguments, option, 0) .map(it -> true).orElse(false); } }
-
-
重组代码结构
将Boolean类型的方法,搬移到Single,使其更紧凑
- 通过抽取一个基类,继承下来Boolean和Single
- java新特性,将接口转为匿名的lambda(使用)
- 无构造函数,工厂方法替换
- 直接通过匿名函数返回
- 重命名,搬移到Single,删除Boolean
问题:
-
两个同样的功能,使用了不同的方法:一个构造子类;一个通过匿名函数表达式
重构Single,都使用Function的实现方式
class OptionParsers<T> { public static OptionParser<Boolean> bool() { return (arguments, option) -> valuesFrom(arguments, option, 0) .map(it -> true).orElse(false); } public static <T> OptionParser<T> unary(T defaultValue, Function<String, T> valueParse) { // return new SingleOptionParser<T>(defaultValue, valueParse); return ((arguments, option) -> valuesFrom(arguments, option, 1) .map(it -> parseValue(option, it.get(0), valueParse)).orElse(defaultValue)); } protected static Optional<List<String>> valuesFrom(List<String> arguments, Option option, int expectedSize) { int index = arguments.indexOf("-" + option.value()); if (index == -1) { return Optional.empty(); } List<String> values = valuesFrom(arguments, index); if (values.size() < expectedSize) { throw new InsufficientArgumentEception(option.value()); } if (values.size() > expectedSize) { throw new TooManyArgumentsException(option.value()); } return Optional.of(values); } private static <T> T parseValue(Option option, String value, Function<String, T> valueParse) { return valueParse.apply(value); } protected static List<String> valuesFrom(List<String> arguments, int index) { int followingFlag = IntStream.range(index + 1, arguments.size()) .filter(it -> arguments.get(it).startsWith("-")) .findFirst().orElse(arguments.size()); List<String> values = arguments.subList(index + 1, followingFlag); return values; } }
-
修改测试
public class SingleValuedOptionParserTest { @Nested class UnaryOptionParser { @Test // side_path public void should_not_accept_extra_argument_for_single_value_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> new OptionParsers().unary(0, Integer::parseInt).parse(asList("-p", "8080", "8081"), option("p"))); assertEquals("p", e.getOption()); } @ParameterizedTest // side_path @ValueSource(strings = {"-p -l", "-p"}) public void should_not_accept_insufficient_argument_for_single_value_option(String arguments) { InsufficientArgumentEception e = assertThrows(InsufficientArgumentEception.class, () -> OptionParsers.unary(0, String::valueOf).parse(asList(arguments.split(" ")), option("p"))); assertEquals("p", e.getOption()); } @Test //default_path public void should_set_default_value_for_single_value_option() { Function<String, Object> whatever = (it) -> null; Object defaultValue = new Object(); assertSame(defaultValue, OptionParsers.unary(defaultValue, whatever).parse(asList(), option("p"))); } @Test // happy_path public void should_parse_value_if_flag_present() { Object parsed = new Object(); Function<String, Object> parse = (it) -> parsed; Object whatever = new Object(); assertSame(parsed, OptionParsers.unary(whatever, parse).parse(asList("-p", "8080"), option("p"))); } static Option option(String value) { return new Option() { @Override public Class<? extends Annotation> annotationType() { return Option.class; } @Override public String value() { return value; } }; } } @Nested public class BooleanOptionParserTest { @Test// side path public void should_not_accept_extra_argument_for_boolean_option() { TooManyArgumentsException e = assertThrows(TooManyArgumentsException.class, () -> OptionParsers.bool().parse(asList("-l", "t"), option("l"))); assertEquals("l", e.getOption()); } @Test // default value public void should_set_default_value_to_false_if_option_not_present() { assertFalse(OptionParsers.bool().parse(asList(), option("l"))); } @Test // happy path public void should_set_default_value_to_true_if_option_present() { assertTrue(OptionParsers.bool().parse(asList("-l"), option("l"))); } static Option option (String value){ return new Option() { @Override public Class<? extends Annotation> annotationType() { return Option.class; } @Override public String value() { return value; } }; } } }
2.列表参数解析
-
目前的代码结构中,如果需要增加不同类型的单值型参数,只需要修改 Args 类中的类型注册表,提供默认值以及解析函数即可
private static Map<Class<?>, OptionParser> PARSERS = Map.of( boolean.class, bool(), int.class, unary(0, Integer::parseInt), String.class, unary("", String::valueOf));
-
需要支持除布尔或者单值型参数,则需要实现 OptionParser 接口
在实现 OptionParser 接口时,可以利用 OptionParsers 类中提供的支撑方法(values、parseValue 等)。最后,在 OptionParsers 上增加工厂方法。
interface OptionParser<T> { T parse(List<String> arguments, Option option); }
任务列表划分
ArgsTest:
//TODO: -g this is a list -d 1 2 -3 5
分解成一组更小的任务:
//TODO: -g "this" "is" {"this", is"}
//TODO: default value []
//TODO: -d a throw exception
红绿循环
实现:
public static <T> OptionParser<T[]> list(IntFunction<T[]> generator, Function<String, T> valueParse) {
return (arguments, option) -> valuesFrom(arguments, option)
.map(it -> it.stream().map(value -> parseValue(option, value, valueParse))
.toArray(generator)).orElse(generator.apply(0));
}
-
happy path
@Test public void should_parse_list_value() { assertArrayEquals(new String[]{"this", "is"}, OptionParsers.list(String[]::new, String::valueOf) .parse(asList("-g", "this", "is"), option("g"))); }
-
default value
@Test public void should_use_empty_array_as_default_value() { String[] value = OptionParsers.list(String[]::new, String::valueOf).parse(asList(), option("g")); assertEquals(0, value.length); }
-
side path
@Test public void should_throw_exception_if_value_parser_cant_parse_value() { Function<String, String> parser = (it) -> { throw new RuntimeException(); }; IllegalValueException e = assertThrows(IllegalValueException.class, () -> OptionParsers.list(String[]::new, parser).parse(asList("-g", "this", "is"), option("g"))); System.out.println(e.getOption()); assertEquals("this", e.getOption()); }
重构
问题
- 还没有在查表Map中进行配置
@Test
public void should_example_2() {
ListOptions options = Args.parse(ListOptions.class, "g", "this", "is", "a", "list", "-d", "1", "2", "3", "4");
assertArrayEquals(new String[]{"this", "is", "a", "list"}, options.group());
assertArrayEquals(new Integer[]{1, 2, -3, 5}, options.decimals());
}
static record ListOptions(@Option("g") String[] group, @Option("d") Integer[] decimals) {}
添加对应类型
String[].class, list(String[]::new, String::valueOf),
Integer[].class, list(Integer[]::new, Integer::valueOf)
- 数值负数问题
@Test
public void should_example_2() {
ListOptions options = Args.parse(ListOptions.class, "-g", "this", "is", "a", "list", "-d", "1", "--2", "3", "4");
assertArrayEquals(new String[]{"this", "is", "a", "list"}, options.group());
assertArrayEquals(new Integer[]{1, 2, -3, 5}, options.decimals());
}
由于是以startWitch获取横线判断,由于负数也有-,导致错误
protected static List<String> valuesFrom(List<String> arguments, int index) {
int followingFlag = IntStream.range(index + 1, arguments.size())
.filter(it -> arguments.get(it).matches("^-[a-zA-Z]+$"))
.findFirst().orElse(arguments.size());
List<String> values = arguments.subList(index + 1, followingFlag);
return values;
}
去除重复代码
- 观察如下代码,可以发现,两者区别:就多了个expectedSize
protected static Optional<List<String>> valuesFrom(List<String> arguments, Option option) {
int index = arguments.indexOf("-" + option.value());
if (index == -1) {
return Optional.empty();
}
List<String> values = valuesFrom(arguments, index);
return Optional.of(values);
}
protected static Optional<List<String>> valuesFrom(List<String> arguments, Option option, int expectedSize) {
int index = arguments.indexOf("-" + option.value());
if (index == -1) {
return Optional.empty();
}
List<String> values = valuesFrom(arguments, index);
if (values.size() < expectedSize) {
throw new InsufficientArgumentEception(option.value());
}
if (values.size() > expectedSize) {
throw new TooManyArgumentsException(option.value());
}
return Optional.of(values);
}
-
检查抽取为方法
private static void checkSize(Option option, int expectedSize, List<String> values) { if (values.size() < expectedSize) { throw new InsufficientArgumentEception(option.value()); } if (values.size() > expectedSize) { throw new TooManyArgumentsException(option.value()); } }
-
通过新特性去重
protected static Optional<List<String>> valuesFrom(List<String> arguments, Option option, int expectedSize) { return valuesFrom(arguments, option).map(it ->{ checkSize(option, expectedSize, it); return it; }); }
-
进一步优化
考虑到值已经传递进来了,因此直接返回就可以了
protected static Optional<List<String>> valuesFrom(List<String> arguments, Option option, int expectedSize) { return valuesFrom(arguments, option).map(it -> checkSize(option, expectedSize, it)); } private static List<String> checkSize(Option option, int expectedSize, List<String> values) { if (values.size() < expectedSize) { throw new InsufficientArgumentEception(option.value()); } if (values.size() > expectedSize) { throw new TooManyArgumentsException(option.value()); } return values; }
总结
TDD的三个好处:
- 通过拆分任务转为测试,使整个开发流程有序,小步快跑逐步迭代的思想
- 修改代码的时候,同时测试验证功能,能够快速定位错误,及时发现问题所在
- 能够时刻感受到需求的变化,最终实现的完成
学习资源来源:如果要购买这课,可以私信我,有返利,或联系QQ:3421793724
徐昊·TDD项目实战70讲