Functional programming makes you forget design patterns

Functional programming makes you forget design patterns

This article is a reading note of "Java 8 Actual Combat", it takes about 5 minutes to read.

It's a bit of a headline, but this is really my feelings about using Lambda expressions recently. Design patterns are a summary of some good experiences and routines in the past, but good language features can make developers not consider these design patterns. Common object-oriented design patterns include strategy mode, template method, observer mode, chain of responsibility mode, and factory mode. Using Lambda expressions (functional programming thinking) helps avoid those fixed codes in object-oriented development. Below we have selected two cases of strategy mode and chain of responsibility mode for analysis.

Case 1: Strategy mode

When we solve a problem with different solutions, we don't want customers to perceive the details of these solutions. In this case, the strategy model is suitable. The strategy model consists of three parts:

  • Algorithm to solve the problem (Strategy in the figure above);
  • One or more specific implementations of this type of algorithm (ConcreteStrategyA, ConcreteStrategyB and ConcreteStrategyC in the figure above)
  • One or more customer usage scenarios (ClientContext in the figure above)

Object-oriented thinking

First define the strategy interface, which means the sorting strategy:

public interface ValidationStrategy {
    boolean execute(String s);
}
 

Then define specific implementation classes (that is, different sorting algorithms):

public class IsAllLowerCase implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

public class IsNumberic implements ValidationStrategy {
    @Override
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

 

Finally define the customer usage scenario, the code is shown in the figure below. Validator is the context environment used when providing services to customers. Each Valiator object encapsulates a specific Strategy object. In actual work, we can upgrade customer service by replacing specific Strategy objects without requiring customers Upgrade.

public class Validator {

    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy strategy) {
        this.strategy = strategy;
    }

    /**
     *  
     */
    public boolean validate(String s) {
        return strategy.execute(s);
    }
}

public class ClientTestDrive {

    public static void main(String[] args) {
        Validator numbericValidator = new Validator(new IsNumberic());
        boolean res1 = numbericValidator.validate("7780");
        System.out.println(res1);

        Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
        boolean res2 = lowerCaseValidator.validate("aaaddd");
        System.out.println(res2);
    }
}
 

Functional programming ideas

If you consider using Lambda expressions, you will find that ValidationStrategy is a functional interface (also has the same function description as Predicate), then there is no need to define the above implementation classes, you can directly replace them with the following code, because the Lambda expression These classes have been encapsulated internally.

public class ClientTestDrive {

    public static void main(String[] args) {
        Validator numbericValidator = new Validator((String s) -> s.matches("\\d+"));
        boolean res1 = numbericValidator.validate("7789");
        System.out.println(res1);

        Validator lowerCaseValidator = new Validator((String s) -> s.matches("[a-z]+"));
        boolean res2 = lowerCaseValidator.validate("aaaddd");
        System.out.println(res2);
    }
}
 

Case 2: Chain of Responsibility Model

In some scenarios, a series of tasks need to be done on an object, and these tasks are completed by different classes. At this time, it is more suitable to use the chain of responsibility model. The main components of the chain of responsibility model include three:

  • An abstract class that manages the sequence of operations, in which there will be an object that records the subsequent operation objects of the current object;
  • Some specific operation objects, these operation objects will be organized in the form of a linked list
  • For a client component that uses this pattern, the component only needs to deal with one component, and it does not need to be coupled with many operation objects.

Object-oriented thinking

First look at the abstract class ProcessingObject defined here, where the successor field is used to manage the subsequent operation objects of the object; the handle interface is used as an interface to provide services to the outside; handleWork is used as the operation method of the actual processing object.

public abstract class ProcessingObject<T> {

    protected ProcessingObject<T> successor;
    
    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }

    public T handler(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handler(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}
 

Next, you can define two specific operation objects, as shown in the following code. PS: The replaceAll method used in the book "Java 8 Actual Combat" is not appropriate. For this point, you can refer to our previous article- 020: Give a few String APIs and cases .

public class HeaderTextProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String input) {
        return "From Raoul, Mario and Alan: " + input;
    }
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
    @Override
    protected String handleWork(String input) {
        return input.replace("labda", "lambda");
    }
}
 

Finally, you can combine the above two specific operation class objects into an operation sequence in the Client, see the following code:

public class Client {
    public static void main(String[] args) {
        ProcessingObject<String> p1 = new HeaderTextProcessing();
        ProcessingObject<String> p2 = new SpellCheckerProcessing();

        p1.setSuccessor(p2);

        String result = p1.handler("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}
 

Functional programming ideas

If you use functional programming thinking, then the chain of responsibility model is straightforward-y=f(x) and z=g(x) are both methods to process x, then if these two functions are combined in Together, it will form a situation of r=f(g(x)), that is, you can use addThen in the Lambda expression to connect multiple processes in series.

public class ClientWithLambda {
    public static void main(String[] args) {
        UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;

        UnaryOperator<String> spellCheckProcessing = (String text) -> text.replace("labda", "lambda");

        Function<String, String> function = headerProcessing.andThen(spellCheckProcessing);

        String result = function.apply("Aren't labdas really sexy?!!");
        System.out.println(result);

        UnaryOperator<String> hhhhhProcessing = (String text) -> text.concat("hhhh");
        Function<String, String> function1 = function.andThen(hhhhhProcessing);
        String result1 = function1.apply("Aren't labdas really sexy?!!");
        System.out.println(result1);
    }
}
 

The above is the chain of responsibility pattern implemented using Java's native Lambda expression. We can also use the previous article- vavr: Let you write the vavr library introduced in Java like writing Scala . The code is as follows:

public class ClientWithVavr {
    public static void main(String[] args) {
        Function1<String, String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
        Function1<String, String> specllCheckProcessing = (String text) -> text.replace("labda", "lambda");

        Function1<String, String> function = headerProcessing.compose(specllCheckProcessing);
        String result = function.apply("Aren't labdas really sexy?!!");
        System.out.println(result);
    }
}
 

summary

It can be seen that functional programming thinking is different from object-oriented programming thinking and has stronger expressive power. Therefore, as a developer, it is time to learn functional programming thinking seriously. As a Java developer, I am going to start with Start learning Lambda expressions, and then try to learn the functional programming features in Scala or Kotlin.

Reference

  1. "Java Programming Practice"
  2. "The Zen of Design Patterns"

This number focuses on topics such as back-end technology, JVM troubleshooting and optimization, Java interview questions, personal growth and self-management, and provides readers with the work and growth experience of first-line developers. I hope you can gain something here.