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

认识

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讲

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
测试驱动的编程是 XP 困扰程序员的一个方面。对于测试驱动的编程意味着什么以及如何去做,大多数人都做出了不正确的假设。这个月,XP 方面的讲师兼 Java 开发人员 Roy Miller 谈论了测试驱动的编程是什么,它为什么可以使程序员的生产力和质量发生巨大变化,以及编写测试的原理。请在与本文相随的 论坛中提出您就本文的想法,以飨笔者和其他读者。(您也可以单击本文顶部或底部的“讨论”来访问该论坛。) 最近 50 年来,测试一直被视为项目结束时要做的事。当然,可以在项目进行之中结合测试,测试通常并不是在 所有编码工作结束后才开始,而是一般在稍后阶段进行测试。然而,XP 的提倡者建议完全逆转这个模型。作为一名程序员,应该在编写代码 之前编写测试,然后只编写足以让测试通过的代码即可。这样做将有助于使您的系统尽可能的简单。 先编写测试 XP 涉及两种测试: 程序员测试和 客户测试。测试驱动的编程(也称为 测试为先编程)最常指第一种测试,至少我使用这个术语时是这样。测试驱动的编程是让 程序员测试(即单元测试 ― 重申一下,只是换用一个术语)决定您所编写的代码。这意味着您必须在编写代码之前进行测试。测试指出您 需要编写的代码,从而也 决定了您要编写的代码。您只需编写足够通过测试的代码即可 ― 不用多,也不用少。XP 规则很简单:如果不进行程序员测试,则您不知道要编写什么代码,所以您不会去编写任何代码。 测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术、Tips 等方面。 背景 一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从。最近兴起的一些软件开发过程相关的技术,提供一些比较高效、实用的软件过程开发方法。其中比较基础、关键的一个技术就是测试驱动开发(Test-Driven Development)。虽然TDD光大于极限编程,但测试驱动开发完全可以单独应用。下面就从开发人员使用的角度进行介绍,使开发人员用最少的代价尽快理解、掌握、应用这种技术。下面分优势,原理,过程,原则,测试技术,Tips等方面进行讨论。 1. 优势 TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值