TDD实践精益编程

苹果筛选

1. 筛选绿色的苹果

    @Test
    public void givenAppleRepoWhenFilterGreenAppleThenReturn3(){
        //given
        Apple apple1 = new Apple("green", "70");
        Apple apple2 = new Apple("red", "100");
        Apple apple3 = new Apple("red", "80");
        Apple apple4 = new Apple("green", "170");
        Apple apple5 = new Apple("green", "60");
        List appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
        AppleFilter filter = new AppleFilter();
        //when
        List greenApples = filter.findGreenApple(appleRepo);
        //then
        assertThat(greenApples.size(),is(3));
    }
public class AppleFilter {
    public List findGreenApple(List appleRepo) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals("green")){
                result.add(apple);
            }
        }
        return result;
    }
}

2.筛选红色的苹果

    @Test
    public void givenAppleRepoWhenFilterRedAppleThenReturn2(){
        //given
        Apple apple1 = new Apple("green", "70");
        Apple apple2 = new Apple("red", "100");
        Apple apple3 = new Apple("red", "80");
        Apple apple4 = new Apple("green", "170");
        Apple apple5 = new Apple("green", "60");
        List appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
        AppleFilter filter = new AppleFilter();
        //when
        List redApples = filter.findRedApple(appleRepo);
        //then
        assertThat(redApples.size(),is(2));
    }
    @Test
    public void givenAppleRepoWhenFilterGreenAppleThenReturn3(){
        //given
        Apple apple1 = new Apple("green", "70");
        Apple apple2 = new Apple("red", "100");
        Apple apple3 = new Apple("red", "80");
        Apple apple4 = new Apple("green", "170");
        Apple apple5 = new Apple("green", "60");
        List appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
        AppleFilter filter = new AppleFilter();
        //when
        List greenApples = filter.findGreenApple(appleRepo);
        //then
        assertThat(greenApples.size(),is(3));
    }
public List findGreenApple(List appleRepo) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals("green")){
                result.add(apple);
            }
        }
        return result;
    }
public List findRedApple(List appleRepo) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals("red")){
                result.add(apple);
            }
        }
        return result;
    }

**Duplicated is evil **
通过参数化,消除hard code 和重复
测试代码同样需要重构,TDD很难被普及的一个原因之一,就是没有持续的对Test进行重构

public class AppleFilterTest {
    private  List appleRepo;
    private  AppleFilter filter;
    @Before
    public void givenAppleRepo(){
        //given
        Apple apple1 = new Apple("green", "70");
        Apple apple2 = new Apple("red", "100");
        Apple apple3 = new Apple("red", "80");
        Apple apple4 = new Apple("green", "170");
        Apple apple5 = new Apple("green", "60");
        appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
        filter = new AppleFilter();
    }
    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = filter.findApple(appleRepo,"green");
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = filter.findApple(appleRepo,"red");
        //then
        assertThat(redApples.size(),is(2));
    }
}
public class AppleFilter {
    public List findApple(List appleRepo,String color) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals(color)){
                result.add(apple);
            }
        }
        return result;
    }
}

3.筛选重量大于100克的苹果

public class AppleFilterTest {
    private  List appleRepo;
    private  AppleFilter filter;
    @Before
    public void givenAppleRepo(){
        //given
        Apple apple1 = new Apple("green", "70");
        Apple apple2 = new Apple("red", "100");
        Apple apple3 = new Apple("red", "80");
        Apple apple4 = new Apple("green", "170");
        Apple apple5 = new Apple("green", "60");
        appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
        filter = new AppleFilter();
    }
    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = filter.findApple(appleRepo,"green");
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = filter.findApple(appleRepo,"red");
        //then
        assertThat(redApples.size(),is(2));
    }
   @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = filter.findApple(appleRepo,100);
        assertThat(gt100.size(),is(2));
    }
}
   

 public List findApple(List appleRepo,String color) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals(color)){
                result.add(apple);
            }
        }
        return result;
    }
public List findApple(List appleRepo,int weight) {
        List result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getWeight()>=weight){
                result.add(apple);
            }
        }
        return result;
    }


结构性重复,只是算法不一样而已
通过策略模式,将算法封装起来,消除重复,使应用满足SRP和OCP

public interface Specification {
    boolean satisfy(Apple apple);
}

public class ColorSpec implements Specification {
    private String color;
    public ColorSpec(String color) {
        this.color = color;
    }
    @Override
    public boolean satisfy(Apple apple) {
        return apple.getColor().equals(color);
    }
}

public class WeightGtSpec implements Specification{
    private int weight;
    public WeightGtSpec(int weight) {
        this.weight = weight;
    }
    @Override
    public boolean satisfy(Apple apple) {
        return apple.getWeight()>=weight;
    }
}
   @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = filter.findApple(appleRepo,new ColorSpec("green"));
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = filter.findApple(appleRepo,new ColorSpec("red"));
        //then
        assertThat(redApples.size(),is(2));
    }

    @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = filter.findApple(appleRepo,new WeightGtSpec(100));
        assertThat(gt100.size(),is(2));
    }
 public List findApple(List appleRepo, Specification spec) {
        List result = new ArrayList<>();
        for (Apple apple : appleRepo) {
            if (spec.satisfy(apple)) {
                result.add(apple);
            }
        }
        return result;
    }

4.筛选不是红色的苹果

    @Before
    public void givenAppleRepo(){
        //given
        Apple apple1 = new Apple("green", 70);
        Apple apple2 = new Apple("red", 100);
        Apple apple3 = new Apple("red", 80);
        Apple apple4 = new Apple("green", 170);
        Apple apple5 = new Apple("green", 60);
        Apple apple6 = new Apple("blue", 60);
        appleRepo = asList(apple1,apple2,apple3,apple4,apple5,apple6);
        filter = new AppleFilter();
    }

    @Test
    public void whenFilterNotRedThenReturn4(){
        List notRed = filter.findApple(appleRepo,new NotSpec(new ColorSpec("red")));
        assertThat(notRed.size(),is(4));
    }

通过组合模式,完成原子策略模式的复用

public class NotSpec implements Specification {
    private Specification spec;
    public NotSpec(Specification spec) {
        this.spec = spec;
    }
    @Override
    public boolean satisfy(Apple apple) {
        return !spec.satisfy(apple);
    }
}

5.筛选既是红色又大于100克的苹果

  @Test
  public void whenFilterRedAndGt100ThenReturn1(){
        List redAndGt100 = filter.findApple(appleRepo,new ColorAndWeightGtSpec(100,"red"));
        assertThat(redAndGt100.size(),is(1));
  }
public class ColorAndWeightGtSpec implements Specification {
    private int weight;
    private String color;
    public ColorAndWeightGtSpec(int weight, String color) {
        this.weight = weight;
        this.color = color;
    }
    @Override
    public boolean satisfy(Apple apple) {
        return apple.getWeight()>=weight&& apple.getColor().equals(color);
    }
}

当出现And语义时,一般都是违反了SRP,而且该实现也比较僵化,如果需求新增即是红色,又小于100克,该实现无法满足需求。
利用组合模式继续拆分

    @Test
    public void whenFilterRedAndGt100ThenReturn1(){
        List redAndGt100 = filter.findApple(appleRepo,new AndSpec(new ColorSpec("red"),new WeightGtSpec(100)));
        assertThat(redAndGt100.size(),is(1));
    }
public class AndSpec implements Specification{
    private Specification[] specs;
    public AndSpec(Specification ...specs) {
        this.specs = specs;
    }
    @Override
    public boolean satisfy(Apple apple) {
        for (Specification spec: specs)
        {
        if(!spec.satisfy(apple)){
               return false;
            }
        }
        return true;
    }
}

6.筛选红色或者蓝色的苹果

    @Test
    public void whenFilterRedOrBlueThenReturn3(){
        List redOrBlue = filter.findApple(appleRepo,new OrSpec(new ColorSpec("red"),new ColorSpec("blue")));
        assertThat(redOrBlue.size(),is(3));
    }
public class OrSpec implements Specification {
    private Specification[] specs;
    public OrSpec(Specification ...specs) {
        this.specs = specs;
    }
    @Override
    public boolean satisfy(Apple apple) {
        for (Specification spec : specs){
            if(spec.satisfy(apple)){
                return true;
            }
        }
        return false;
    }
}

从OO Design的角度来说,目前的实现已经非常不错了,符合SRP,OCP,能够以组合的方式实现更复杂的需求变化。
但是从调用的客户端角度,调用的方式并不简单,需要new大量的spec对象,阅读性不是很友好。

7.通过引入Specifications辅助类和匿名类来简化客户端调换用

我们的价值观是客户第一,个人理解,从程序的角度来说,我们的第一客户是调用我们程序的客户端代码(有可能是你自己,也有可能是别人写的代码。调用你程序的代码被称为客户端代码,也许就是这样的理解而被大家这么命名的吧),所以我们有责任让我们的客户使用起来更简单方便。

一般接口类都对应一个工具类,例如Collection 对应 Collections,Array对应Arrays以辅助客户端调用。基于此,我们实现我们自己的Specifications辅助类。

    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = filter.findApple(appleRepo,color("green"));
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = filter.findApple(appleRepo,color("red"));
        //then
        assertThat(redApples.size(),is(2));
    }
    @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = filter.findApple(appleRepo,gtWeight(100));
        assertThat(gt100.size(),is(2));
    }
    @Test
    public void whenFilterNotRedThenReturn4(){
        List notRed = filter.findApple(appleRepo,not(color("red")));
        assertThat(notRed.size(),is(4));
    }
    @Test
    public void whenFilterRedAndGt100ThenReturn1(){
        List redAndGt100 = filter.findApple(appleRepo,and(color("red"),gtWeight(100)));
        assertThat(redAndGt100.size(),is(1));
    }
    @Test
    public void whenFilterRedOrBlueThenReturn3(){
        List redOrBlue = filter.findApple(appleRepo,or(color("red"),color("blue")));
        assertThat(redOrBlue.size(),is(3));
    }
public class Specifications {
    public static Specification color(String color){
        return new Specification() {
            @Override
            public boolean satisfy(Apple apple) {
                return apple.getColor().equals(color);
            }
        };
    }
    public static Specification gtWeight(int weight){
        return new Specification() {
            @Override
            public boolean satisfy(Apple apple) {
                return apple.getWeight()>=weight;
            }
        };
    }
    public static Specification or(Specification ...specs){
        return new Specification() {
            @Override
            public boolean satisfy(Apple apple) {
                for (Specification spec : specs){
                    if(spec.satisfy(apple)){
                        return true;
                    }
                }
                return false;       
            }
        };
    }
    public static Specification and(Specification ...specs){
        return new Specification() {
            @Override
            public boolean satisfy(Apple apple) {
                for (Specification spec : specs){
                    if(!spec.satisfy(apple)){
                        return false;
                    }
                }
                return true;
            }
        };
    }
    public static Specification not(Specification spec){
        return new Specification() {
            @Override
            public boolean satisfy(Apple apple) {
                return !spec.satisfy(apple);
            }
        };
    }
}

Java 8之前,这基本上已经是非常漂亮的实现了。我们使用辅助类抽象出了dsl语句,来灵活的并且非常有表现力的实现我们的需求。
Java 8以后,接口可以有static方法和default方法。来是我们的类进一步减少。
同时引入lamda继续简化我们的代码,使其更具有表现力

8.引入lamda

public class Specifications {
    public static Specification color(String color){
        return apple -> apple.getColor().equals(color);
    }
    public static Specification gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
    public static Specification or(Specification ...specs){
        return apple -> {
            for (Specification spec : specs){
                if(spec.satisfy(apple)){
                    return true;
                }
            }
            return false;           
       };
    }
    public static Specification and(Specification ...specs){
        return apple -> {
            for (Specification spec : specs){
                if(!spec.satisfy(apple)){
                    return false;
                }
            }
            return true;
        };
    }
    public static Specification not(Specification spec){
        return apple -> !spec.satisfy(apple);
    }
}

9.接口静态方法

public interface Specification {
    boolean satisfy(Apple apple);
    static Specification color(String color){
        return apple -> apple.getColor().equals(color);
    }
    static Specification gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
    static Specification or(Specification ...specs){
        return apple -> {
            for (Specification spec : specs){
                if(spec.satisfy(apple)){
                    return true;
                }
            }
            return false;   
        };
    }
    static Specification and(Specification ...specs){
        return apple -> {
            for (Specification spec : specs){
                if(!spec.satisfy(apple)){
                    return false;
                }
            }
            return true;
        };
    }
    static Specification not(Specification spec){
        return apple -> !spec.satisfy(apple);
    }
}

10.接口默认方法

and(color("red"),gtWeight(100))这样的语义是不符合我们的人类语言方式的。
color("red").and(gtWeight(100))这样的语义更像人类语言的表达方式。

public interface Specification {
    boolean satisfy(Apple apple);
    static Specification color(String color){
        return apple -> apple.getColor().equals(color);
    }
    static Specification gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
    default Specification or(Specification spec){
        return apple -> satisfy(apple) || spec.satisfy(apple);
    }
    default Specification and(Specification spec){
        return apple -> satisfy(apple) && spec.satisfy(apple);
    }
    default Specification not(){
        return apple -> !satisfy(apple);
    }
}
    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = filter.findApple(appleRepo,color("green"));
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = filter.findApple(appleRepo,color("red"));
        //then
        assertThat(redApples.size(),is(2));
    }
    @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = filter.findApple(appleRepo,gtWeight(100));
        assertThat(gt100.size(),is(2));
    }
    @Test
    public void whenFilterNotRedThenReturn4(){
        List notRed = filter.findApple(appleRepo,color("red").not());
        assertThat(notRed.size(),is(4));
    }
    @Test
    public void whenFilterRedAndGt100ThenReturn1(){
        List redAndGt100 = filter.findApple(appleRepo,color("red").and(gtWeight(100)));
        assertThat(redAndGt100.size(),is(1));
    }
    @Test
    public void whenFilterRedOrBlueThenReturn3(){
        List redOrBlue = filter.findApple(appleRepo,color("red").or(color("blue")));
        assertThat(redOrBlue.size(),is(3));
    }

11. 泛型实现通用筛选

public interface Specification {
    boolean satisfy(T apple);
    default Specification or(Specification spec) {
        return t -> satisfy(t) || spec.satisfy(t);
    }
    default Specification and(Specification spec) {
        return t -> satisfy(t) && spec.satisfy(t);
    }
    default Specification not() {
        return t -> !satisfy(t);
    }
}

public interface AppleSpec extends Specification {
    static Specification color(String color){
        return apple -> apple.getColor().equals(color);
    }
    static Specification gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
}

通过泛型化我们的Specification不仅仅可以过滤Apple了,同时可以过滤所有的对象类型。

12. Predicate接口

很多我们遇到的问题,前人也都遇到过并且已经解决了。我们不需要重复发明轮子。只需要站在巨人的肩膀继续前行。

T -> boolean : Specification实际上是这样一种函数,给定一个参数,返回一个boolean类型。而这个函数式接口是java8中提供的函数式接口中最常用的之一: Predicate接口。

public interface AppleSpec extends Predicate {
    static Predicate color(String color){
        return apple -> apple.getColor().equals(color);
    }
    static Predicate gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
}

13.Stream内循环

目前为止我们的代码不仅简洁,而且非常灵活。我们通过AppleFilter来迭代按Predicate进行过滤。
Java 8的Stream提供了内部循环,减少了临时变量,并且提供了免费的并行化。

    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = appleRepo.stream().filter(color("green")).collect(Collectors.toList());
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = appleRepo.stream().filter(color("red")).collect(Collectors.toList());
        //then
        assertThat(redApples.size(),is(2));
    }
    @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = appleRepo.stream().filter(gtWeight(100)).collect(Collectors.toList());
        assertThat(gt100.size(),is(2));
    }
    @Test
    public void whenFilterNotRedThenReturn4(){
        List notRed = appleRepo.stream().filter(color("red").negate()).collect(Collectors.toList());
        assertThat(notRed.size(),is(4));
    }
    @Test
    public void whenFilterRedAndGt100ThenReturn1(){
        List redAndGt100 = appleRepo.stream().filter(color("red").and(gtWeight(100))).collect(Collectors.toList());
        assertThat(redAndGt100.size(),is(1));
    }
    @Test
    public void whenFilterRedOrBlueThenReturn3(){
        List redOrBlue = appleRepo.stream().filter(color("red").or(gtWeight(100))).collect(Collectors.toList());
        assertThat(redOrBlue.size(),is(3));
    }

14.结束了吗?

目前为止我们只写了一个实现了Predicate接口的AndSpec接口。
函数式编程中,函数第一,所以其实我们连AndSpec也没有必要实现。

    @Test
    public void whenFilterGreenAppleThenReturn3(){
        //when
        List greenApples = appleRepo.stream().filter(apple -> apple.getColor().equals("green")).collect(Collectors.toList());
        //then
        assertThat(greenApples.size(),is(3));
    }
    @Test
    public void whenFilterRedAppleThenReturn2(){
        //when
        List redApples = appleRepo.stream().filter(apple -> apple.getColor().equals("red")).collect(Collectors.toList());
        //then
        assertThat(redApples.size(),is(2));
    }
    @Test
    public void whenFilterGt100ThenReturn2(){
        List gt100 = appleRepo.stream().filter(apple -> apple.getWeight()>=100).collect(Collectors.toList());
        assertThat(gt100.size(),is(2));
    }
    @Test
    public void whenFilterNotRedThenReturn4(){
        List notRed = appleRepo.stream().filter(apple -> !apple.getColor().equals("red")).collect(Collectors.toList());
        assertThat(notRed.size(),is(4));
    }

    @Test
    public void whenFilterRedAndGt100ThenReturn1(){
        Predicate red = apple -> apple.getColor().equals("red");
        Predicate gt100 = apple -> apple.getWeight()>=100;
        List redAndGt100 = appleRepo.stream().filter(red.and(gt100)).collect(Collectors.toList());
        assertThat(redAndGt100.size(),is(1));
    }
    @Test
    public void whenFilterRedOrBlueThenReturn3(){
        Predicate red = apple -> apple.getColor().equals("red");
        Predicate gt100 = apple -> apple.getWeight()>=100;
        List redOrBlue = appleRepo.stream().filter(red.or(gt100)).collect(Collectors.toList());
        assertThat(redOrBlue.size(),is(3));
    }

自此,从服务代码的角度来说,我们一行代码也没有写,即完成了客户的需求。

资料引用

  • https://codingstyle.cn/topics/12
  • Test Driven Java Development
  • Test Driven Development with Mockito
  • Pragmatic Unit Testing in Java 8 with Junit
  • Java 8 in action

Git Repository

https://github.com/jerrywalker0435/apple-filter

对应分支

  • 1-filter-green-apple
  • 2-filter-red-apple-1
  • 2-filter-red-apple-2
  • 3-filter-with-specification
  • 3-filter-with-weight-1
  • 4-filter-not-spec
  • 5-filter-color-and-weight-1
  • 5-filter-color-and-weight-2
  • 6-red-or-blue
  • 7-specifications
  • 8-lambda
  • 9-static-method
  • 10-default-method
  • 11-generic-specification
  • 12-predicate
  • 13-stream
  • 14-no-code

后记

  • 马丁福勒2017年中国行给程序员的三个建议:

    • 学习业务领域知识
    • 将学习重点放在软件开发原则与模式上,而不是具体技术上
    • 提升自身与其他团队成员之间的沟通能力
  • TDD发明人之一Kent Beck的三个建议

    • No matter the circumstance you can always improve.
    • You can always start improving with yourself.
    • You can always start improving today.

你可能感兴趣的:(TDD实践精益编程)