苹果筛选
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.