A piece of code was refactored six times by the boss, and my mentality collapsed

preface

Hi, Hello, I'm Milo. I'm back 🙈

Come in and gossip for everyone and see what I've done myself? Recently, the company received a new project of agricultural products trading website. Because of a code reconstruction problem, it almost got into trouble with the boss. I thought it was the boss who deliberately made trouble for me. Finally, I found that I was too delicious 😏, It's like this!

The boss told us that it was mainly used in the provincial agricultural products trade fair last week. The first one was to sell the Yellow River honey in Gansu Province, so I was arranged to sell melons! Oh, no, I'm responsible for developing the function of selling melons 🤣; Soon I designed the following classes to define Melon melon class:

/**
 * melon
 * @author Milo Lee
 * @date 2021-04-07 13:21
 */
public class Melon {
    /**varieties*/
    private final String type;
    /**weight*/
    private final int weight;
    /**Place of Origin*/
    private final String origin;

    public Melon(String type, int weight, String origin) {
        this.type = type;
        this.weight = weight;
        this.origin = origin;
    }
    // getters, toString() method omitted
}

After a CRUD operation, I finished the addition, deletion, modification and inspection of melons, and handed over to work 🤗.

Filter melons by type for the first time

The next day, the boss asked me a question, saying that we could filter melons by melon type. Isn't that simple? So I created a Filters class and implemented a filterMelonByType method

/**
 * @author Milo Lee
 * @date 2021-04-07 13:25
 */
public class Filters {

    /**
     * Filter melons by type
     * @param melons Melon 
     * @param type type
     * @return
     */
    public static List<Melon> filterMelonByType(List<Melon> melons, String type) {

        List<Melon> result = new ArrayList<>();
        for (Melon melon: melons) {
            if (melon != null && type.equalsIgnoreCase(melon.getType())) {
                result.add(melon);
            }
        }
        return result;
    }
}


Done, let's test it

    public static void main(String[] args) {
        ArrayList<Melon> melons = new ArrayList<>();
        melons.add(new Melon("Croissant honey", 1, "Thailand"));
        melons.add(new Melon("watermelon", 2, "Sanya"));
        melons.add(new Melon("Yellow river honey", 3, "Lanzhou"));
        List<Melon> melonType = Filters.filterMelonByType(melons, "Yellow river honey");
        melonType.forEach(melon->{
        System.out.println("Melon type:"+melon.getType());
        });
    }

No problem. Show it to the boss. The boss looked at my code and said: if I ask you to add a melon screened by weight, what are you going to write? Go back and think about it. This guy won't deliberately pick on me? 😪

Second screening of melons by weight

When I returned to my seat, I thought that last time I had realized the screening of melons by type, so I'll give him a copy to change!

As follows:

    /**
     * Filter melons by weight
     * @param melons
     * @param weight
     * @return
     */
    public static List<Melon> filterMelonByWeight(List<Melon> melons, int weight) {

        List<Melon> result = new ArrayList<>();
        for (Melon melon: melons) {
            if (melon != null && melon.getWeight() == weight) {
                result.add(melon);
            }
        }
        return result;
    }

public static void main(String[] args) {
     	ArrayList<Melon> melons = new ArrayList<>();
        melons.add(new Melon("Croissant honey", 1, "Thailand"));
        melons.add(new Melon("watermelon", 2, "Sanya"));
        melons.add(new Melon("Yellow river honey", 3, "Lanzhou"));
        List<Melon> melonType = Filters.filterMelonByType(melons, "Yellow river honey");
        melonType.forEach(melon->{
            System.out.println("Melon type:"+melon.getType());
        });

        List<Melon> melonWeight = Filters.filterMelonByWeight( melons,3);
        melonWeight.forEach(melon->{
            System.out.println("Melon weight:"+melon.getWeight());
        });
    }

Programmer's favorite way to do it, ha ha, CV. However, I found that filterByWeight() is very similar to filterByType(), except that the filtering conditions are different. I thought to myself, the boss won't let me write a melon by type and weight. Take my code and show it to the boss. Sure enough, you'll get what you're afraid of 😟.

Third screening of melons by type and weight

In order to fulfill the task of the boss, I combined the above code and quickly wrote the following code

    /**
     * Melons are screened by type and weight
     * @param melons
     * @param type
     * @param weight
     * @return
     */
    public static List<Melon> filterMelonByTypeAndWeight(List<Melon> melons, String type, int weight) {

        List<Melon> result = new ArrayList<>();
        for (Melon melon: melons) {
            if (melon != null && type.equalsIgnoreCase(melon.getType()) && melon.getWeight() == weight) {
                result.add(melon);
            }
        }
        return result;
    }

The boss looked at my code and said, it seems that you still don't understand what I mean. If not only I, but also customers continue to demand like this today.

Then Filters will have many similar methods, that is, they write a lot of template code (code redundancy but have to write);

In our programmer's opinion, this is unacceptable. If you continue to add new filter criteria, the code will become difficult to maintain and error prone. You can learn about lambda expressions and functional interfaces, and then modify your code. I'm sure he just can't get along with me 😤

Pass the behavior as a parameter for the fourth time

After three twists and turns above. I found that theoretically, any attribute of the Melon class can be used as a Filter condition. In this way, our Filter class will have a lot of template code, and some methods will be very complex.

In fact, we can find that every method we write corresponds to a query behavior, and the query behavior must correspond to a filter condition. Is there a way for us to write a method that passes the query behavior as a parameter to return our results?

So we named it behavior parameterization, which is explained in the figure below (the left shows what we have now; the right shows what we want). Do you find that the template code will be significantly reduced 🤔

If we treat filtering conditions as a behavior, it is very intuitive to treat each behavior as the implementation of the interface. After analysis, we find that all the above behaviors have one thing in common: filtering conditions and boolean return. Abstract an interface as follows

public interface MelonPredicate {
  boolean test(Melon melon);
}

For example, filtering yellow river honey can be written as follows: hhmmelonpredict.

public class HHMMelonPredicate implements MelonPredicate {

     @Override
     public boolean test(Melon melon) {
       return "Yellow river honey".equalsIgnoreCase(melon.getType());

     }

}

By analogy, we can also filter melons with a certain weight:

public class WeightMelonPredicate implements MelonPredicate {

     @Override
     public boolean test(Melon melon) {
       return melon.getWeight() > 5000;
     }

}


In fact, students familiar with design patterns should know that this is: strategic design pattern.

The main idea is to let the system dynamically select the methods to be called at runtime. Therefore, we can think that the MelonPredicate interface unifies all algorithms dedicated to filtering melon classes, and each implementation is a strategy, and we can also understand it as a behavior.

At present, we use the policy design pattern to abstract the query behavior. We also need a method to receive the MelonPredicate parameter. So I defined the filterMelons() method as follows:

public static List<Melon> filterMelons(List<Melon> melons, MelonPredicate predicate) {
    
    List<Melon> result = new ArrayList<>();
    for (Melon melon: melons) {
      if (melon != null && predicate.test(melon)) {
        result.add(melon);
      }

    }  
    return result;
}


It's done. Test it. It's much better than before. Let the boss have a look

List<Melon> hhm = Filters.filterMelons(melons, new HHMMelonPredicate());

List<Melon> weight = Filters.filterMelons(melons, new WeightMelonPredicate());


For the fifth time, 100 filter conditions were added at one time

Just when I was complacent, the boss poured a basin of cold water on him. He said that you think our platform will buy yellow river honey. If there are dozens of melon varieties, I'll list 100 filtering conditions for you. What do you do?

Ten thousand grass mud horses are galloping in my heart 😫! Is the boss trying to get along with me! Although my code is flexible enough after the last modification, if I suddenly add 100 filter conditions, I still need to write 100 policy classes to implement each filter condition. Then we need to pass the policy to the filterMelons() method.

Is there a way that you don't need to create these classes? Smart, I soon found that I could use java anonymous inner classes.

As follows:

List<Melon> europeans = Filters.filterMelons(melons, new MelonPredicate() {

     @Override

     public boolean test(Melon melon) {

       return "europe".equalsIgnoreCase(melon.getOrigin());

     }

});


Although a big step forward, it seems to be of no help. I still need to write a lot of code to meet this requirement. The purpose of designing anonymous inner classes is to facilitate Java programmers to pass code as data. Sometimes, anonymous inner classes are more complicated. At this time, I suddenly remember the lambda expression that the boss asked me to learn. I can use it to simplify it

List<Melon> europeansLambda = Filters.filterMelons(
  melons, m -> "europe".equalsIgnoreCase(m.getOrigin())
);

Sure enough, it's much more handsome!!!, In this way, I successfully completed the task again. I excitedly took the code and asked the boss to have a look.

Introducing generics for the sixth time

The boss looked at my code and said, well, good! The brain finally opened up. Now consider that our platform is for agricultural products, that is, there must be more than melons. If you change to other fruits, how can you modify your code?

At present, our mellonpredicte only supports the Mellon class. What's the matter with this guy? Maybe one day he wants to buy vegetables and sea cucumbers, but he can't create many more interfaces similar to MelonPredicate. At this time, I suddenly remembered the generics that the teacher said. It's time for it to work!

So I defined a new interface predict

@FunctionalInterface
public interface Predicate<T> {

  boolean test(T t);

}


Next, we rewrite the filterMelons() method and rename it filter():

public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
  
	List<T> result = new ArrayList<>();

    for (T t: list) {

      if (t != null && predicate.test(t)) {

        result.add(t);

      }

    }  

    return result;

}


Now, we can filter melons like this:

List<Melon> watermelons = Filters.filter(

  melons, (Melon m) -> "Watermelon".equalsIgnoreCase(m.getType()));


Similarly, we can do the same thing with numbers:

List<Integer> numbers = Arrays.asList(1, 13, 15, 2, 67);

List<Integer> smallThan10 = Filters.filter(numbers, (Integer i) -> i < 10);


Looking back, we find that the code has changed significantly since using Java 8 functional interface and lambda expression.

I wonder if the careful partner has found that there is an annotation on @ FunctionalInterface on our Predicate interface, which marks the functional interface.

So far, through the evolution of requirements, we understand the concepts of lambda and functional interface, and deepen our understanding of them. In fact, friends familiar with java8 know that in our Java util. The function package contains more than 40 such interfaces

Functional interfaces and lambda expressions form a strong team. According to the above example, we know that functional interface is a high abstraction of our behavior, and we can see an example of the concrete implementation of this behavior through lambda expression.

Predicate<Melon> predicate = (Melon m)-> "Watermelon".equalsIgnoreCase(m.getType());

In short, Lambda

lambda expression consists of three parts, as shown in the following figure:

The following is a description of each part of the lambda expression:

  • To the left of the arrow is the parameter used in the body of the lambda expression.
  • To the right of the arrow is the main body of lambda.
  • The arrow is just a separator between the lambda parameter and the body.

The anonymous class version of this lambda is as follows:

List<Melon> europeans = Filters.filterMelons(melons, new Predicate<Melon>() {

 @Override

 public boolean test(Melon melon) {

   return "Watermelon".equalsIgnoreCase(melon.getType());

 }

});

Now, if we look at the lambda expression and its anonymous class version, we can describe the lambda expression from the following four aspects

We can define lambda expression as a concise and transitive anonymous function. First, we need to make it clear that lambda expression is essentially a function. Although it does not belong to a specific class, it has parameter list, function body, return type, and even throw exceptions; Secondly, it is anonymous. Lambda expression has no specific function name; Lambda expressions can be passed like parameters to simplify code writing.

Lambda supports behavior parameterization, which we have proved in the previous example. Finally, remember that lambda expressions can only be used in the context of functional interfaces.

summary

In this paper, we focus on the use and availability of functional interfaces. We will study how to evolve the code from the original template code to the flexible implementation based on functional interfaces. I hope it will help you understand the functional interface. Thank you.

Keywords: Java Lambda java8

Added by QbertsBrother on Fri, 18 Feb 2022 14:11:07 +0200