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