教你选苹果-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<Apple> appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
    AppleFilter filter = new AppleFilter();
    //when
    List<Apple> greenApples = filter.findGreenApple(appleRepo);
    //then
    assertThat(greenApples.size(),is(3));
}
public class AppleFilter {
    public List<Apple> findGreenApple(List<Apple> appleRepo) {
        List<Apple> 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<Apple> appleRepo = asList(apple1,apple2,apple3,apple4,apple5);
    AppleFilter filter = new AppleFilter();
    //when
    List<Apple> redApples = filter.findRedApple(appleRepo);
    //then
    assertThat(redApples.size(),is(2));
}
public List<Apple> findRedApple(List<Apple> appleRepo) {
    List<Apple> result = new ArrayList<>();
    for(Apple apple : appleRepo){
        if(apple.getColor().equals("red")){
            result.add(apple);
        }
    }
    return result;
}

重构说明
"Duplicated is evil,重复是万恶之源"。通过参数化,消除hard code和重复。测试代码同样需要重构,没有对应用代码和测试代码进行持续重构是TDD很难被普及的原因之一。

public class AppleFilterTest {
    private List<Apple> appleRepo;
    private AppleFilter filter;

    @Before
    public void givenAppleRepo(){
        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(){
        List<Apple> greenApples = filter.findApple(appleRepo,"green");
        assertThat(greenApples.size(),is(3));
    }

    @Test
    public void whenFilterRedAppleThenReturn2(){
        List<Apple> redApples = filter.findApple(appleRepo,"red");
        assertThat(redApples.size(),is(2));
    }
}
public class AppleFilter {
    public List<Apple> findApple(List<Apple> appleRepo,String color) {
        List<Apple> result = new ArrayList<>();
        for(Apple apple : appleRepo){
            if(apple.getColor().equals(color)){
                result.add(apple);
            }
        }
        return result;
    }
}

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

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

重构说明
"结构性重复",只是算法不一样而已,通过策略模式,将算法封装起来,消除重复,使代码实现满足SRP(单一职责)OCP(开闭原则)

@Test
public void whenFilterGreenAppleThenReturn3(){
    List<Apple> greenApples = filter.findApple(appleRepo,new ColorSpec("green"));
    assertThat(greenApples.size(),is(3));
}

@Test
public void whenFilterRedAppleThenReturn2(){
    List<Apple> redApples = filter.findApple(appleRepo,new ColorSpec("red"));
    assertThat(redApples.size(),is(2));
}

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

4. 筛选不是红色的苹果

@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辅助类和匿名类来简化客户端调用

调用端的代码又被称为客户端代码,从"客户第一"的角度来说我们有责任让"客户"使用起来更简单方便。

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

@Test
public void whenFilterGreenAppleThenReturn3(){
    List<Apple> greenApples = filter.findApple(appleRepo,color("green"));
    assertThat(greenApples.size(),is(3));
}

@Test
public void whenFilterRedAppleThenReturn2(){
    List<Apple> redApples = filter.findApple(appleRepo,color("red"));
    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(领域特定语言:domain-specific language,这里指color,gtWeight,and,or,not),来灵活的并且非常有表现力的实现我们的需求。

Java 8以后,接口可以有static方法和default方法。使我们可以减少类,避免类爆炸,同时引入lambda继续简化我们的代码,使其更具有表现力

8. 引入lambda

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. 接口静态方法

Java8提供的接口静态方法使我们不在需要Specifications的辅助类。

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 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<T> {
    boolean satisfy(T apple);

    default Specification<T> or(Specification<T> spec) {
        return t -> satisfy(t) || spec.satisfy(t);
    }

    default Specification<T> and(Specification<T> spec) {
        return t -> satisfy(t) && spec.satisfy(t);
    }

    default Specification<T> not() {
        return t -> !satisfy(t);
    }
}

public interface AppleSpec extends Specification<Apple> {
    static Specification<Apple> color(String color){
        return apple -> apple.getColor().equals(color);
    }

    static Specification<Apple> gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
}

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

12. Predicate接口

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

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

public interface AppleSpec extends Predicate<Apple> {
    static Predicate<Apple> color(String color){
        return apple -> apple.getColor().equals(color);
    }

    static Predicate<Apple> gtWeight(int weight){
        return apple -> apple.getWeight()>=weight;
    }
}

13. Stream内循环

目前为止我们的代码不仅简洁,而且非常灵活。AppleFilter通过for循环过滤Predicate。

Java 8的Stream提供了内部循环,消除for循环并减少了临时变量,AppleFilter自然也就不再需要了。

@Test
public void whenFilterGreenAppleThenReturn3(){
    List<Apple> greenApples = appleRepo.stream().filter(color("green")).collect(Collectors.toList());
    assertThat(greenApples.size(),is(3));
}

@Test
public void whenFilterRedAppleThenReturn2(){
    List<Apple> redApples = appleRepo.stream().filter(color("red")).collect(Collectors.toList());
    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(color("blue"))).collect(Collectors.toList());
    assertThat(redOrBlue.size(),is(3));
}

14. 结束了吗?

目前为止我们只写了一个实现了Predicate<T>接口的AppleSpec类。

函数式编程中,函数第一,所以其实我们连AppleSpec也没有必要实现。

@Test
public void whenFilterGreenAppleThenReturn3(){
    List<Apple> greenApples = appleRepo.stream().filter(apple -> apple.getColor().equals("green")).collect(Collectors.toList());
    assertThat(greenApples.size(),is(3));
}

@Test
public void whenFilterRedAppleThenReturn2(){
    List<Apple> redApples = appleRepo.stream().filter(apple -> apple.getColor().equals("red")).collect(Collectors.toList());
    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<Apple> red = apple -> apple.getColor().equals("red");
    Predicate<Apple> 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<Apple> red = apple -> apple.getColor().equals("red");
    Predicate<Apple> blue = apple -> apple.getColor().equals("blue");
    List redOrBlue = appleRepo.stream().filter(red.or(blue)).collect(Collectors.toList());
    assertThat(redOrBlue.size(),is(3));
}

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

Write Less,Do more


资料引用

  • Test Driven Java Development
  • Test Driven Development with Mockito
  • Pragmatic Unit Testing in Java 8 with Junit
  • Java 8 in action
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,560评论 0 4
  • 行为参数化:帮助你处理频繁变更的需求的一种软件开发模式,即可以将代码块作为参数传递给另一个方法,稍后再去执行它。此...
    夏与清风阅读 286评论 0 2
  • 微微一笑 发丝在指尖盘绕 阳光踩着棉云 红着脸说不小心迟到 来往之间 故事已悄然翻篇 冰淇淋们的甜蜜 已然遗落在那...
    弦汀阅读 227评论 1 2
  • 在犹太人种族里,当他们的孩子刚刚懂事的时候,孩子的母亲会将蜂蜜滴在书本上,让孩子们去尝一尝书上的那一滴蜂蜜的味道…...
    才爸家庭教育阅读 352评论 0 4
  • regular expression : RegExp用来处理字符串的规则 只能处理字符串 它是一个规则:可以验证...
    Leonard被注册了阅读 324评论 0 1

友情链接更多精彩内容