There are many programmers who write code, but not so many programmers who write good code (may you and I be on the way). Today I’m going to share a case of avoiding a lot of if-else and making progress together.

This article is a problem I encountered with the Flutter project, so the sample code is Dart language. Please don’t burden the reader, language is not the key, the key is the idea.

Origin of the problem

Here are two pieces of actual business code:

  1. Before the purchase operation can proceed to the actual purchase process, the necessary conditions need to be verified

    _onBuyButtonClick() {
      /// 1.User block verification
      final user = getUserInfo();
      if (user.isForbidden) {
        showForbiddenDialog();
        return;
      }
      /// 2.Unpaid order quantity check
      final orders = getUserWaitingPayOrders();
      if (orders.length >= limit) {
        showTooMuchOrderWaitingPayDialog();
        return;
      }
      /// 3. xxx
      /// 4. xxx
    
      /// The buying process
    }
    Copy the code
  2. The sale operation, followed by the actual sale process, requires the necessary condition verification

    _onSellButtonClick() {
      /// 1.User block verification
      final user = getUserInfo();
      if (user.isForbidden) {
        showForbiddenDialog();
        return;
      }
      /// 2.The shop is forbidden to check
      /// 3. xxx
      /// 4. xxx
      
      /// Selling process
    }
    Copy the code

So the process of verification, we all have10! Items to be verified for each process2 ~ 5A range. Please experience the requirements document:

Each graph here represents a process, and each yellow square is a checksum item. Of course, the process can’t be repeated, but each check item can be repeated. So we can abstract the question as: How do I add M pre-validations to N operations? Such as:

  1. operationN1 (purchase)Need to checkM1 (whether the user is blocked), M2 (not too many orders waiting for payment)...
  2. operationN2 (On sale)Need to checkM1, M3 (whether the store is banned), M4 (whether the goods being sold have reached the upper limit)...
  3. operationN3Need to verifyM5, M6, M8.

How can we argue against such a perfectly reasonable demand? 😁 So, let’s kill it!

The solution

Show me the code. Give the current solution in use first, then analyze the details inside.

Our final result is as follows (take the purchase process as an example) :

_onBuyButtonClick() {
  /// Use the CheckController to control which conditions need to be checked
  final anyChecker = CheckController().check([
    Requirement.account,
    Requirement.orderBuffer,
  ]);
  /// If there is something to deal with, deal with it
  if(anyChecker ! =null) {
    anyChecker.handle();
    return;
  }
  /// Previous purchase process
}
Copy the code

As you can see, we’ve reduced what was originally a few dozen lines of validation code (the longest eight validation items, that is, eight if judgments) to a few short lines. In contrast, the scheme has many advantages:

  1. No duplicate code. beforeN2The validation code in the process is totallyN1theCopy. Now even if two processes have the same checksum, it is only the same enumerationcaseOn.
  2. Readability is improved and maintainability is greatly improved. In a large number ofif-elseFiguring out what it does isn’t very challenging, but it does take time. Especially after a period of time, without detailed comments.
  3. Maintainability is greatly improved. Check items of a process correspond exactly to the elements of the array, including add, delete, change and check check items. Imagine changing the priorities of two items in a process, which two you need to read beforeifIt’s what you care about, and then you can adjust. Now, all you have to do is find the corresponding in the arraycaseYou can. And now that they’re absolutely clustered, part of the previous code might be visible on the screen and part of it might not be at all!

How to implement

If you’re interested in the above implementation, here’s how it works.

Stage 1 – Reduce repetition

If we want to reuse M checks, we have to separate the checks from the code. Taking the purchase inspection as an example, we can find that the whole process can be divided into two steps:

  1. Condition checking
  2. The result processing

All M checks can be regarded as check XXX condition, if not meet XXX. Here we wrap each check into a separate class, using the example of a check to see if a user is blocked from purchasing:

class AccountForbiddenChecker {
  /// Returns whether the user is banned based on the condition
  bool match() {
    return false;
  }
  /// Specific actions that a user is banned from doing, such as pop-up warnings
  void handle() {}
}
Copy the code

For example, an order waiting for payment should not be inspected too much:

class OrderWaitingPayBufferChecker {
  /// Determine whether the customer has too many unpaid orders
  bool match() {
    return false;
  }

  /// Specific actions such as pop-up warnings for excessive unpaid orders
  void handle() {}
}
Copy the code

Like this, we can encapsulate all of these M checks in concrete classes. Copy-and-paste in multiple process condition checks is avoided. But there is a burden on the user who needs to remember, or at least remember, the name of every Checker. So, we use enumerations to represent each check item:

/// Items to be verified
enum Requirement {
  account,
  orderBuffer,
  // ...
}
Copy the code

We also need to have a conversion process from enumerations to concrete check classes. Switch is used here:

extension Mapper on Requirement {
    RequirementChecker toChecker() {
        switch(this) {
            case Requirement.account: return AccountForbiddenChecker();
            case Requirement.orderBuffer: return OrderWaitingPayBufferChecker();
            // ...}}}Copy the code

Stage 2 – Increase reproducibility

When you need to coordinate multiple classes, you need a manager.

/// Check the item manager
class CheckController {
  /// Based on the enumeration passed in, determine whether a specific item matches and, if so, return the corresponding inspector.
  RequirementChecker? check(List<Requirement> items) {
    for (final item in items) {
      final checker = item.toChecker();
      if (checker.match()) {
        returnchecker; }}return null; }}Copy the code

RequirementChecker is also required, which is an interface responsible for standardizing each Checker:

abstract class RequirementChecker {
  bool match();
  void handle();
}
Copy the code

Each specific Checker then implements this interface, so that managers, as well as outsiders, can uniformly use multiple Checkers.

At this point, we have implemented the above solution. For each process, we only need CV method, and then modify the check items slightly to achieve the effect. Bingo!

Related to recommend

Today’s solution is not the author’s startup 🤣. The idea comes from the responsibility chain pattern in the design pattern. Wall crack recommends this site.

For each pattern, there are plenty of diagrams, problems, and solutions. For example,Chain of Responsibility modelChapter one: It’s not great!

All right, I got all the secrets. I hope you will reach your peak soon!