- 认识
-
- 项目1命令行参数解析
- 01 | 任务分解法与整体工作流程
- 1.API 构思与组件划分
- 2.功能分解与任务列表
- 3.红绿灯循环
- 02 | 识别坏味道与代码重构
- 1.引入多态接口
- 2.使用“抽象工厂”模式的变体来替换分支(消除分支)
- 3.使用Function特性(消除代码重复)
- 4.利用工厂方法,消除代码
- 03 | 按测试策略重组测试
- 1.调整任务列表
- 2.红绿灯循环
- BooleanOptionParserTest
- SingleValuedOptionParserTest
- 3.按照测试策略重组测试
- 4.红绿灯循环
- 04 |实现对于列表参数的支持
- 1.不易察觉的坏味道
- 定义方法代替注释
- 变化实现方式,使实现直观
- Optional消除重复代码
- 重组代码结构
- 2.列表参数解析
-
- 总结
认识
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]
{-l:[], -p:8080, -d: /us/logs}
2.功能分解与任务列表
-
根据上面的构思,得到的是一个大的需求,但是由于这样跨度太大,开发过程中可能会导致很多细节的问题被忽略,因此需要将他划分为更小的粒度。
该实战,将功能分解为单个值功能和列表功能
-
在根据功能的分解拆分为任务列表,通过TODO列表,在划分为一个个小的TODO,示例:
- 单个值情况
- 列表情况
- 边界情况:只有一个值后面没跟值,有多个值
- 默认情况(默认值)
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));
}
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 进行测试
@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());
}
@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:
SingleValuedOptionParserTest:
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
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")));
}
@Test
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
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
@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
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
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 {
@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());
定义方法代替注释
-
一般都会通过添加注释来说明,如下:
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());
-
可以采用添加代码注释的方式(抽取方法,让方法名成为注释)
随着代码写注释,可以通过定义方法的方式实现
需要很强的实现细节,才编写文档
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 ((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
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
@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
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
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
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
public void should_set_default_value_to_false_if_option_not_present() {
assertFalse(OptionParsers.bool().parse(asList(), option("l")));
}
@Test
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:
分解成一组更小的任务:
红绿循环
实现:
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讲
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)