Why do you always use bad design patterns?

Posted by Jimmy79 on Mon, 20 Dec 2021 17:54:22 +0100

I often see some articles on design patterns, write a lot of content, and give some "vivid" examples.

But it may have the same problem as the Head First design pattern: after reading it, I will, but it doesn't seem to be useful? Or hard cover design pattern. ​

Take a few extreme examples I've seen:

  1. Two fields, and a Builder
  2. 3 if and 1 strategy mode
  3. 5 lines of code is also very simple initialization, but also to get a Factory
  4. ......

As for why this problem occurs... Let me talk about my opinion

reason

Most R & D personnel do business function development, which is often called CRUD. It's just that CRUD has different complexity in different business scenarios. ​

However, for business code, it is not easy to apply design patterns in many cases, or it is impossible to apply design patterns well. ​

I usually see the most articles about the strategy model. Why? ​

I guess it's because it's best written. It's relatively simple to apply in business code; For any slightly more complex scenario, you can apply the policy pattern to split multiple if into multiple classes. ​

Indeed, the appropriate use of policy patterns in business code can reduce complexity; But even if you use it, you have to live for a degree. Don't change the if in various businesses to the policy mode, otherwise the code will explode

I've seen a project before. Although it's an internal xx system, the R & D brother may be possessed. More than 80 policy classes were selected, and these more than 80 classes were divided into more than a dozen groups, with 78 policy classes in each group, but the code in each class was only more than ten or twenty lines, and there were duplicate codes. ​

I asked him: are you raising poisonous insects? ​

This R & D brother is a counterexample. He abused the design pattern and excessively inserted all kinds of branch code into the design pattern. But I guess he may be learning to apply it

Other patterns such as principal-agent status are more difficult to apply in business code, because there are not so many suitable scenarios. But the strategy mode is different. You can try it wherever there is an if

However, design patterns are used to solve problems and reduce / transfer complexity, not increase complexity.

Design patterns in non business code

After jumping out of business code, or even pure business code, it's easier to apply design patterns. You don't even need to apply them. When you encounter problems, you will naturally think of using design patterns to solve them.

Take a chestnut

The system generally needs a traceId/requestId to string the whole link to cooperate with log printing or centralized APM extraction. ​

For a single application, the traceId is usually bound with the MDC of the logging framework. In the Filter or some AOP s, give the MDC a traceId, and the whole calling link can use this ID. when printing the log, you can distinguish different requests according to the traceId, like this:

2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - Request step 0
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - Request step 1
2021-06-10 18:31:44.227 [ThreadName] [000] INFO loggerName - Request step 2

2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - Request step 0
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - Request step 1
2021-06-10 18:31:44.227 [ThreadName] [111] INFO loggerName - Request step 2

...

The traceId 000 / 111 can be used to distinguish which request it is. ​

However, MDC stores data through ThreadLocal, which is bound to threads after all. What if thread pool processing is used in the link? When the child thread in the thread pool prints the log, the MDC cannot obtain the traceId of the main thread, but for this request, the main child thread is a link

Remember this sentence?

Any problem in the field of computer science can be solved by adding an indirect middle layer. "

Here, a middle layer is added with the help of the delegation mode, and the problem is well solved. ​

Since it is the data transfer problem of the main sub thread, you only need to take out the traceId in the MDC from the main thread and pass it to the new sub thread when creating the sub thread, like this:

public class MDCDelegateRunnable implements Runnable{

    private Runnable target;

    private String traceId;

    public MDCDelegateRunnable(Runnable target, String traceId) {
        this.target = target;
        this.traceId = traceId;
    }

    @Override
    public void run() {
        MDC.put("traceId",traceId);
        target.run();
        MDC.remove("traceId");
    }
}

Then a delegate mode thread pool will be created, and the execute method will be overridden. Wrap the original Runnable object in the thread pool as the MDCDelegateRunnable just now. When creating, pass the traceId through the construction parameter

public class MDCDelegateExecutorService extends AbstractExecutorService {

    public MDCDelegateExecutorService(AbstractExecutorService target) {
        this.target = target;
    }

    private AbstractExecutorService target;

    @Override
    public void shutdown() {
        target.shutdown();
    }

    //...

    @Override
    public void execute(@NotNull Runnable command) {
        target.execute(new MDCDelegateRunnable(command, MDC.get("traceId")));
    }

}

Done, let's test:

public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
    MDC.put("traceId","111");
    new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("runnable: "+MDC.get("traceId"));
        }
    });
    Future<String> future = new MDCDelegateExecutorService((AbstractExecutorService) Executors.newFixedThreadPool(5)).submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return MDC.get("traceId");
        }
    });

    System.out.println("callable: "+future.get());

    System.in.read();
}

//output
runnable: 111
callable: 111

Perfect. The troublesome traceId transfer problem is now solved through a simple delegation mode. There is no need to modify the caller code and no code that destroys the thread pool.

Delegation mode in JDK

Remember the Executors#newSingleThreadExecutor method for creating a single thread pool. Under what circumstances do you need a single thread pool? ​

For example, I just need an asynchronous operation to get the return value. If I directly start the new thread, it is not convenient to get the return value. It is convenient to use the Callable/Runnable + Future of the thread pool:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Future<String> future = executorService.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        // do sth...
        return data;
    }
});

String data = future.get();

executorService.shutdown();

For the single thread asynchronous scenario, you don't even need to maintain a singleton thread pool. Each new/shutdown can also be used. But I'm single threaded. Is it a little inconvenient to shut down every time? If I forget to shut down, it's not over

JDK designers also thought of this problem, and they have solved it. Similar to the above example, this problem can be solved perfectly by using a simple delegation mode:

public static ExecutorService newSingleThreadExecutor() {
    //Create FinalizableDelegatedExecutorService delegate class
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

// In the delegate class, close the thread pool automatically by finalizing the shutdown method of the delegated thread pool object
static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

// Public abstract delegate thread pool
static class DelegatedExecutorService extends AbstractExecutorService {
    private final ExecutorService e;
    
    DelegatedExecutorService(ExecutorService executor) { e = executor; }
    
    public void execute(Runnable command) { e.execute(command); }
    
    public void shutdown() { e.shutdown(); }
    //...
}

In this way, you don't even need to display shutdown when using newSingleThreadExecutor

Note: Although JDK helps us turn off... It is still recommended to manually shut down and take this mechanism of JDK as a fool proof design. In case you forget that JDK can be automatically shut down to avoid leakage

summary

Combined with the above two examples, once you jump out of the scope of business code, does the application design pattern become very simple? You don't even need to apply design patterns. When you encounter problems, you will naturally think of using design patterns to solve problems, rather than using design patterns to raise bugs in the code

In pure business code, the benefits of proper splitting and keeping the code clean and readable are far better than a set of design patterns

Repeat: design patterns are used to solve problems and reduce / transfer complexity, not increase complexity

The above are only personal opinions. If you have different opinions, please leave a message in the comment area

 

Topics: Java Design Pattern Programmer