TDD项目实战-命令行参数解析

2023-05-16

  • 认识
    • 1.基本规则
    • 2.三步骤
    • 3.任务分解法
    • 总结
  • 项目1命令行参数解析
    • 01 | 任务分解法与整体工作流程
      • 1.API 构思与组件划分
      • 2.功能分解与任务列表
      • 3.红绿灯循环
    • 02 | 识别坏味道与代码重构
      • 1.引入多态接口
      • 2.使用“抽象工厂”模式的变体来替换分支(消除分支)
      • 3.使用Function特性(消除代码重复)
      • 4.利用工厂方法,消除代码
    • 03 | 按测试策略重组测试
      • 1.调整任务列表
      • 2.红绿灯循环
        • BooleanOptionParserTest
        • SingleValuedOptionParserTest
      • 3.按照测试策略重组测试
      • 4.红绿灯循环
    • 04 |实现对于列表参数的支持
      • 1.不易察觉的坏味道
        • 定义方法代替注释
        • 变化实现方式,使实现直观
        • Optional消除重复代码
        • 重组代码结构
      • 2.列表参数解析
        • 任务列表划分
        • 红绿循环
        • 重构
        • 问题
        • 去除重复代码
    • 总结

认识

1.基本规则

  1. 当且仅当存在失败的自动化测试时,才开始编写生产代码
  2. 消除重复(消除坏味道)

2.三步骤

红 / 绿 / 重构

  1. 红:编写一个失败的小测试,甚至可以是无法编译的测试
  2. 绿:让这个测试快速通过,甚至不惜犯下任何罪恶
  3. 重构:消除上一步中产生的所有重复(坏味道)

3.任务分解法

  1. 构思软件被使用的方式
  2. 构思功能的实现方式,划分所需组件以及组件间的关系(没思路,可以不划分)
  3. 根据需求的功能描述拆分功能点,功能点考虑正确路径(Happy Path),边界路径(Side Path)
  4. 依照组件以及组件间的关系,将功能拆分到对应组件
  5. 针对拆分的结果编写测试,进入红 / 绿 / 重构循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7pErGbuY-1647857672466)(./images/TDD整体工作流程.jpg)]

总结

  1. TDD是三项已有技术的重组:先大概设计,再落地测试,再重构出最终代码

    1. 设计能力:软件设计原则/思想/模式
    2. 测试能力:测试技术/方法/工具
    3. 重构能力:代码坏味道,重构方法/工具
  2. 需求分析:

    1. 任务列表:从无到有实现各个功能点

      任务列表是一个随代码结构(重构)、测试策略(在哪个范围内测试)、代码实现情况(存在哪些缺陷)等因素而动态调整的列表。它的内容体现了我们最新的认知,它的变化记录了我们认知改变的过程。

    2. 测试列表:通过所有测试即表示实现功能

  3. 为什么一定要先看到红灯?

    1. 看到测试以预期的方式出错
    2. 红灯表示缺少功能/实现错误
  4. 为什么一定要看到绿灯?

    1. 用尽可能简洁的代码使当前所有测试通过
  5. 为什么一定要重构?

    1. 弥补为了快速看到绿灯所犯的过错
    2. 每次重构都要运行所有测试,确保绿灯!一旦红灯,回退到绿灯再重构!

项目1命令行参数解析

需求:

  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 构思与组件划分

  1. 确定API形式:使用者将以什么方式使用这个代码,这段代码的整体对外接口部分

  2. 思考功能如何实现

    示例:该实战可以采用如下两种甚至你能想到的其他最简实现方式

    // 第一种:通过数组下标的形式
    [-l],[-p, 8080],[-d, /usr/logs]
    // 第二种:采用Map的形式
    {-l:[], -p:8080, -d: /us/logs}
    

2.功能分解与任务列表

  1. 根据上面的构思,得到的是一个大的需求,但是由于这样跨度太大,开发过程中可能会导致很多细节的问题被忽略,因此需要将他划分为更小的粒度。

    该实战,将功能分解为单个值功能和列表功能

  2. 在根据功能的分解拆分为任务列表,通过TODO列表,在划分为一个个小的TODO,示例:

    1. 单个值情况
    2. 列表情况
    3. 边界情况:只有一个值后面没跟值,有多个值
    4. 默认情况(默认值)
    // 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.红绿灯循环

  1. Single Option做红绿灯测试

    示例:

    1. 刚开始parse返回null,测试红灯
    2. 根据已知条件,添加如下代码为绿灯
    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) {}
    
    1. 红灯

      @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);
        }
      
      }
      
    2. 在对Integer和String进行相同的测试

      1. 先编写测试,红灯
      2. 在进行代码修改,绿灯
      3. 重复上述步骤直到功能点完成
    3. 对一组数据进行测试

      1. 对一组数据编写测试,红灯

        @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());
        }
        
      2. 修改,绿灯

        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;
            }
        }
        
    4. 总结

      通过红绿灯循环,逐步的完善功能

      前面对boolean、int、string的测试,最终成为解决针对不同类型参数方法的处理

      从而最终实现对一组数据的解析

    5. 最终代码

      @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 | 识别坏味道与代码重构

重构:保持小步骤且稳定的节奏,逐步完成重构,而不是按照目标对代码进行重写

完成前面的测试有两个选择:继续完善功能;进入重构

前提条件:

  1. 测试都是绿的
  2. 坏味道足够明细

目前项目问题:

  1. 存在多个分支条件,随着支持类型越多,分支越多

    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.引入多态接口

  1. 将分支里的逻辑抽为方法(隔离需要变化的地方)

  2. 提取为接口,并实现接口,实现多态替换

  3. 注意在这个过程中,也需要进行测试,

    这里虽然没有修改代码逻辑,但是由于修改了代码结构,避免出错,因此最好还是进行测试

        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));
            }
        }
    
    
  4. 在这个过程中,将代码进一步逻辑,使目光最终放在主要的处理逻辑上

    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修改,因此不能使用多态进行重构,只能利用工厂来重构

  1. 查表法:

        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);
    }
}
  1. 将不变的抽取为方法

    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);
        }
    }
    
  2. 通过继承,重写的方式,实现消除代码

    注:需要修改访问权限为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);
        }
    }
    
    
  3. 通过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

  1. 工厂方法替换构造函数 && 替换为接口类型

    class StringOptionParser extends IntOptionParser {
        private StringOptionParser() {
            super(String::valueOf);
        }
        public static OptionParser createStringOptionParser() {
            return new StringOptionParser();
        }
    }
    
  2. 可以发现,只有createStringOptionParser,使用了StringOption的构造函数,由于接受类型是个接口,因此可以写成下面格式

    class StringOptionParser extends IntOptionParser {
    
        public static OptionParser createStringOptionParser() {
            return new IntOptionParser(String::valueOf);
        }
    }
    
  3. 在将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)
    );
    
    
  4. 同理,修改IntOptionParser

    目的:去除无参构造函数

  5. 最终代码

    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接口和两个实现类。

因此当再去测试时,存在两个不同选择:

  1. 继续针对 Args 进行测试

  2. 直接对 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());
    }
    
  3. 这两个测试,测试的功能是一样的,但是测试范围不同

    可以选择粒度更小的测试,这样更有益于问题的定位

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxrsQxbn-1647857672468)(./images/命令行参数重构测试-范围.jpg)]

  4. 修改任务列表

    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

  1. 多个值

    @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());
    }
    
  2. 默认值情况

    @Test
    public void should_set_default_value_to_false_if_option_not_present() {
      	assertFalse(new BooleanOptionParser().parse(asList(),  option("l")));
    }
    

SingleValuedOptionParserTest

  1. 多个值

        @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());
        }
    
  2. -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());
        }
    
  3. 默认值

    @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")));
    }
    
  4. 多值字符串

    可以直接通过,原因:重构后实现都是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.按照测试策略重组测试

重构测试,更加清晰展示意图

  1. 重构后,整数类型和字符串类型的异常场景中,差异仅仅在于如何构造 SingleValuedOptionParser:
new SingleValuedOptionParser(0, Integer:parseInt)
new SingleValuedOptionParser("", String::valueOf)
  1. 仅仅是测试代码的差别,而被测试的代码则没有任何区别。按照任务列表,再构造其他场景的测试,也仅仅是不同测试数据的重复而已。所以将剩余任务从列表中取消就好了。

  2. 对比经过重构之后新写的测试,就会发现对于类似的功能,测试的出发点测试的范围都有不同,这是一种坏味道。需要对测试进行重构,以消除这些不一致:

    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.红绿灯循环

目前问题:

  1. parseOption获取的注解不在返回空指针

TDD:

  1. 在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());

定义方法代替注释

  1. 一般都会通过添加注释来说明,如下:

    // -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());
    
  2. 可以采用添加代码注释的方式(抽取方法,让方法名成为注释)

    随着代码写注释,可以通过定义方法的方式实现

    需要很强的实现细节,才编写文档

      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();
        }
    

变化实现方式,使实现直观

  1. 问题:方法不直观,通过变化实现方式使本身直观

    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消除重复代码

  1. BooleanOptionParser 和 SingleValuedOptionParser 之间存在隐含的重复的代码

    很显然Boolean和Single两者之间是有相似代码的

    1. 可以通过构造interface消除

    2. 通过JDK8新特性Optional消除

      1. 修改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);
            }
        
      2. 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,使其更紧凑

  1. 通过抽取一个基类,继承下来Boolean和Single
  2. java新特性,将接口转为匿名的lambda(使用)
    1. 无构造函数,工厂方法替换
    2. 直接通过匿名函数返回
    3. 重命名,搬移到Single,删除Boolean

问题:

  1. 两个同样的功能,使用了不同的方法:一个构造子类;一个通过匿名函数表达式

    重构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;
        }
    
    }
    
    
  2. 修改测试

    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.列表参数解析

  1. 目前的代码结构中,如果需要增加不同类型的单值型参数,只需要修改 Args 类中的类型注册表,提供默认值以及解析函数即可

    private static Map<Class<?>, OptionParser> PARSERS = Map.of(
            boolean.class, bool(),
            int.class, unary(0, Integer::parseInt),
            String.class, unary("", String::valueOf));
    
  2. 需要支持除布尔或者单值型参数,则需要实现 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));
}

  1. 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")));
    }
    
  2. 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);
    }
    
  3. 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());
    }
    

重构

问题

  1. 还没有在查表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)
  1. 数值负数问题
@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;
}

去除重复代码

  1. 观察如下代码,可以发现,两者区别:就多了个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);
    }
  1. 检查抽取为方法

        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());
            }
        }
    
  2. 通过新特性去重

    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;
      });
    }
    
  3. 进一步优化

    考虑到值已经传递进来了,因此直接返回就可以了

    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的三个好处:

  1. 通过拆分任务转为测试,使整个开发流程有序,小步快跑逐步迭代的思想
  2. 修改代码的时候,同时测试验证功能,能够快速定位错误,及时发现问题所在
  3. 能够时刻感受到需求的变化,最终实现的完成

学习资源来源:如果要购买这课,可以私信我,有返利,或联系QQ:3421793724
徐昊·TDD项目实战70讲

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

TDD项目实战-命令行参数解析 的相关文章

随机推荐

  • ubuntu 18.04源码安装mysql 5.7.18

    一 安装依赖包 sudo apt update sudo apt install cmake bison libncurses5 dev build essential 二 下载 mysql 5 7 18源码 源码 传送门 选择那个包含bo
  • 什么叫做装箱和拆箱?一看就懂系列

    有很多同学对与装箱和拆箱还是有点搞不太明白 首先讲一下概念 xff1a 1 装箱指的是把值类型转化为引用类型 2 拆箱当然指的就是把引用类型转化为值类型 估计很多同学可能还分不清哪些是属于引用类型和值类型 这个就需要自己去强化记忆一下了 光
  • java 和javascript的区别?你是否还在把他们混为一谈呢

    1 javascript是基于对象的 xff0c 它是一种脚本语言 xff0c 是一种基于对象和事件驱动的编程语言 xff0c 因而它本身提供了非常丰富的内部对象供设计人员使用 而Java是面向对象的 xff0c 即Java是一种真正的面向
  • vs2019智能提示设置为中文

    官网修改中文提示地址 xff1a https docs microsoft com zh cn dotnet core install localized intellisense 1 去官网下载intellisense语言包 下载链接 x
  • .net中的定时任务

    FluentScheduler是什么 xff1f FluentScheduler是 net中的任务调度框架 xff0c 也就是你如果想在 net程序跑一段代码 xff0c 同时又不影响主程序的运行时 就可以使用FluentScheduler
  • 实用的Visual Studio插件

    打开Visual Studio 扩展 管理扩展 安装自己需要的插件 01 CodeMaid CodeMaid快速整理代码文件 xff0c 规范你的代码 xff0c 提高代码阅读体验 代码自动对齐 xff0c 格式化代码 xff08 ps x
  • mysql 数据库信息探索

    mysql 数据库信息探索 xff08 1 xff09 查询数据库的表数量 SELECT COUNT TABLES table schema FROM information schema TABLES GROUP BY table sch
  • .net 6 基于AspNetCoreRateLimit的限流

    1 安装包 AspNetCoreRateLimit 2 在appsetting cs中加入IpRateLimiting配置节点 span class token comment 限流配置 span span class token stri
  • 关于汉字转拼音并排序解决方案

    使用方法 xff1a 写一个静态帮助类 span class token keyword public span span class token keyword static span span class token keyword c
  • .NET Core6 中使用AutoMapper

    1 引入AutoMapper包 2 新建一个类 xff1a MappingProfile xff0c 类名自定义 xff0c 但是必须要继承 Profile类 用于创建映射规则 如图 xff1a Student为源数据 xff08 我这里是
  • 使用Python调用百度OCR

    使用Python调用百度OCR 注册 登录百度智能云创建应用安装python SDK接口说明代码实现 xff08 本地图片 xff09 代码实现 xff08 使用url上的图片并使用可选参数 xff09 注册 登录百度智能云 注册请点击 登
  • 001 超全C语言程序设计概念

    前言 此笔记主要参考自赵海英老师的C语言课程 xff0c 此笔记是在考研重新学习C语言的情况下进行的整理 xff0c 主要用于后续的C语言概念温故知新 第一章 基础知识 1 数制及转换 四种数制 xff1a 二进制 十进制 八进制 十六进制
  • 使用@Autowired注解警告Field injection is not recommended

    问题 xff1a 在使用变量方式依赖注入时 xff0c 提示Field injection is not recommended 64 Autowired LogService logService 虽然变量方式注入非常简洁 xff0c 但
  • mybatis动态数据源,分页插件失效

    mybatis动态数据源 xff0c 分页插件失效 发表于 xff1a 2020 08 18 20 42 47 阅读量 xff1a 9 作者 xff1a 黄叶 原因 xff1a 使用动态数据源 xff1a 数据正常但是total为0 解决
  • mybatis动态数据源配置使用事务不生效

    原因 xff1a 因为我使用的是配置的方式来加载数据源 xff0c 因此我们还需要对事务管理器进行一个配置 解决 xff1a 在代码中添加 配置事物 64 param dataSource 64 return 64 Bean public
  • Caffeine cache实现本地缓存(简单又清楚)

    Caffeine cache实现本地缓存题 缓存填充策略 手动加载 介绍 xff1a 使用方式 xff1a 同步加载 介绍 xff1a 使用方式 xff1a 异步加载 介绍 xff1a 注意 xff1a 异步和同步使用方式相似 这里的话主要
  • 商城后台系统 — 新手练习 —毕业设计

    商城后台系统 新手练习 毕业设计 业务功能介绍项目地址 xff1a 一 商品管理1 商品列表 描述 效果 2 添加商品 描述 效果 3 商品分类 描述 效果 4 商品类型 描述 效果 二 订单管理1 订单列表 描述 效果 2 订单设置 描述
  • CASE WHEN函数@sql学习

    mysql中可以使用CASE WHEN函数完成数据分组 CASE WHEN函数用来对数据进行判断和分组 来自MySQL触发器里的流程控制语句 知识 CASE WHEN是SQL编程中常用的条件控制语句 CASE WHEN的功能 xff1a 新
  • @Autowired注入为null — 4种常见情况

    64 Autowired注入为null 情况一 使用过滤器 原因解决 情况二 没有添加注解 原因解决 情况三 xff08 没有被扫描到 xff09 原因解决 情况四 xff08 手动new xff09 原因解决 情况一 使用过滤器 原因 因
  • TDD项目实战-命令行参数解析

    认识1 基本规则2 三步骤3 任务分解法总结 项目1命令行参数解析01 任务分解法与整体工作流程1 API 构思与组件划分2 功能分解与任务列表3 红绿灯循环 02 识别坏味道与代码重构1 引入多态接口2 使用 抽象工厂 模式的变体来替换分