[Open Source and Project Combat: Open Source Combat] 82 | Open Source Combat III (Part 2): Analysis of Several Design Patterns Used in Google Guava

In the last class, we used an excellent open source library such as Google Guava to explain how to find common functional modules that have nothing to do with business and can be reused in business development, and extract them from business code, design Developed as an independent class library, framework or functional component.

Today, let's learn about several classic design patterns used in Google Guava: Builder pattern, Wrapper pattern, and the Immutable pattern that we haven't talked about before.

Without further ado, let's officially start today's study!

Application of Builder pattern in Guava

In project development, we often use caching. It can improve access speed very effectively.
Commonly used caching systems include Redis, Memcache, etc. However, if the data to be cached is relatively small, there is no need for us to deploy a cache system independently in the project. After all, all systems have a certain probability of error. The more systems included in the project, the combination will increase the probability of error in the project as a whole and reduce the usability. At the same time, introducing one more system requires maintaining one more system, and the cost of project maintenance will increase.

Instead, we can build a memory cache inside the system and integrate it with the system for development and deployment. So how to build memory cache? We can develop memory cache from scratch based on classes provided by JDK, such as HashMap. However, developing a memory cache from scratch involves more work, such as cache elimination strategies. In order to simplify the development, we can use the ready-made caching tools com.google.common.cache.* provided by Google Guava.
Using Google Guava to build an in-memory cache is very simple, I wrote an example and posted it below, you can take a look.

public class CacheDemo {
  public static void main(String[] args) {
    Cache<String, String> cache = CacheBuilder.newBuilder()
            .initialCapacity(100)
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();
    cache.put("key1", "value1");
    String value = cache.getIfPresent("key1");
    System.out.println(value);
  }
}

From the above code, we can find that the Cache object is created through a Builder class such as CacheBuilder. Why should the Cache object be created by the Builder class? I think this question should not be difficult for you.

You can think about it first, and then read my answer. To build a cache, you need to configure n multiple parameters, such as expiration time, elimination strategy, maximum cache size, and so on. Correspondingly, the Cache class will contain n multi-member variables. We need to set the values ​​of these member variables in the constructor, but not all values ​​must be set, which values ​​to set is up to the user to decide. To meet this requirement, we need to define multiple constructors with different parameter lists.

In order to avoid too long parameter lists of constructors and too many different constructors, we generally have two solutions. Among them, one solution is to use the Builder mode; another solution is to first create an object through a no-argument constructor, and then set the required member variables one by one through the setXXX() method.

Then let me ask you another question, why did Guava choose the first solution instead of the second? Is it also possible to use the second solution? The answer is no. As for why, we can see the source code clearly. I copied the build() function in the CacheBuilder class to the following, you can read it first.

public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
  this.checkWeightWithWeigher();
  this.checkNonLoadingCache();
  return new LocalManualCache(this);
}
private void checkNonLoadingCache() {
  Preconditions.checkState(this.refreshNanos == -1L, "refreshAfterWrite requires a LoadingCache");
}
private void checkWeightWithWeigher() {
  if (this.weigher == null) {
    Preconditions.checkState(this.maximumWeight == -1L, "maximumWeight requires weigher");
  } else if (this.strictParsing) {
    Preconditions.checkState(this.maximumWeight != -1L, "weigher requires maximumWeight");
  } else if (this.maximumWeight == -1L) {
    logger.log(Level.WARNING, "ignoring weigher specified without maximumWeight");
  }
}

After reading the code, do you have the answer? In fact, the answer we have already talked about when we talked about the Builder pattern. Now, let's talk about it again in combination with the source code of CacheBuilder.
The main reason why the Builder mode must be used is that when actually constructing the Cache object, we must do some necessary parameter verification, which is the work to be done by the first two lines of code in the build() function. If you adopt the scheme of no-argument default constructor plus setXXX() method, these two checks will have nowhere to be placed. Without verification, the created Cache object may be illegal and unusable.

Application of Wrapper mode in Guava

Under the collection package path of Google Guava, there is a set of classes named starting with Forwarding. I cut some of these classes and posted them below, you can take a look.

insert image description here
This set of Forwarding classes is numerous, but the implementations are all similar. I have copied some of the codes in the ForwardingCollection to here, you can look at the code first, and then think about what this group of Forwarding classes are used for.

@GwtCompatible
public abstract class ForwardingCollection<E> extends ForwardingObject implements Collection<E> {
  protected ForwardingCollection() {
  }
  protected abstract Collection<E> delegate();
  public Iterator<E> iterator() {
    return this.delegate().iterator();
  }
  public int size() {
    return this.delegate().size();
  }
  @CanIgnoreReturnValue
  public boolean removeAll(Collection<?> collection) {
    return this.delegate().removeAll(collection);
  }
  public boolean isEmpty() {
    return this.delegate().isEmpty();
  }
  public boolean contains(Object object) {
    return this.delegate().contains(object);
  }
  @CanIgnoreReturnValue
  public boolean add(E element) {
    return this.delegate().add(element);
  }
  @CanIgnoreReturnValue
  public boolean remove(Object object) {
    return this.delegate().remove(object);
  }
  public boolean containsAll(Collection<?> collection) {
    return this.delegate().containsAll(collection);
  }
  @CanIgnoreReturnValue
  public boolean addAll(Collection<? extends E> collection) {
    return this.delegate().addAll(collection);
  }
  @CanIgnoreReturnValue
  public boolean retainAll(Collection<?> collection) {
    return this.delegate().retainAll(collection);
  }
  public void clear() {
    this.delegate().clear();
  }
  public Object[] toArray() {
    return this.delegate().toArray();
  }
  
  //...省略部分代码...
}

Just looking at the code implementation of ForwardingCollection, you may not think of its function. Let me give you another hint and give an example of its usage, as follows:

public class AddLoggingCollection<E> extends ForwardingCollection<E> {
  private static final Logger logger = LoggerFactory.getLogger(AddLoggingCollection.class);
  private Collection<E> originalCollection;
  public AddLoggingCollection(Collection<E> originalCollection) {
    this.originalCollection = originalCollection;
  }
  @Override
  protected Collection delegate() {
    return this.originalCollection;
  }
  @Override
  public boolean add(E element) {
    logger.info("Add element: " + element);
    return this.delegate().add(element);
  }
  @Override
  public boolean addAll(Collection<? extends E> collection) {
    logger.info("Size of elements to add: " + collection.size());
    return this.delegate().addAll(collection);
  }
}

Combining the source code and examples, I think you should know the function of this group of Forwarding classes, right?
In the above code, AddLoggingCollection is a proxy class implemented based on the proxy mode. It adds the logging function for "add" related operations on the basis of the original Collection class.
As we mentioned earlier, the proxy mode, decorator, and adapter mode can be collectively referred to as the Wrapper mode, and the original class is encapsulated twice through the Wrapper class. Their code implementations are also very similar, and they can all be implemented by entrusting the function implementation of the Wrapper class to the function of the original class by way of combination.

public interface Interf {
  void f1();
  void f2();
}
public class OriginalClass implements Interf {
  @Override
  public void f1() { //... }
  @Override
  public void f2() { //... }
}
public class WrapperClass implements Interf {
  private OriginalClass oc;
  public WrapperClass(OriginalClass oc) {
    this.oc = oc;
  }
  @Override
  public void f1() {
    //...附加功能...
    this.oc.f1();
    //...附加功能...
  }
  @Override
  public void f2() {
    this.oc.f2();
  }
}

In fact, this ForwardingCollection class is a "default Wrapper class" or "default Wrapper class". This is similar to the FilterInputStream default decorator class covered in the lesson on Decorator Patterns. You can re-read the relevant content of the 50th lecture on the decorator pattern.
If we do not use this ForwardinCollection class, but let the AddLoggingCollection proxy class directly implement the Collection interface, then all the methods in the Collection interface must be implemented in the AddLoggingCollection class, and only add() and addAll( ) two functions, the implementation of other functions is just like the implementation of f2() function in Wrapper class, simply delegated to the corresponding function of the original collection class object.

In order to simplify the code implementation of the Wrapper pattern, Guava provides a series of default Forwarding classes. When users implement their own Wrapper class, based on the extension of the default Forwarding class, they can only implement the methods they care about, and other methods that they don't care about use the default Forwarding class implementation, just like the implementation of AddLoggingCollection class.

Application of Immutable mode in Guava

Immutable mode, called invariant mode in Chinese, does not belong to the classic 23 design modes, but as a more commonly used design idea, it can be summarized as a design mode to learn. In the theoretical part before, we only mentioned the Immutable mode a little bit, but did not explain it in detail independently. Here we will use Google Guava to explain it again.
The state of an object does not change after the object is created, which is the so-called immutable mode. The class involved is the immutable class (Immutable Class), and the object is the immutable object (Immutable Object). In Java, the most commonly used immutable class is the String class, and a String object cannot be changed once it is created.

Immutable patterns can be divided into two categories, one is ordinary immutable pattern, and the other is deep immutable pattern (Deeply Immutable Pattern). The ordinary invariant mode means that the reference object contained in the object can be changed. Unless otherwise specified, the invariant mode we usually refer to refers to the ordinary invariant mode. The deep invariant mode means that the referenced objects contained by the object are also not mutable. The relationship between the two is somewhat similar to the relationship between shallow copy and deep copy mentioned earlier. I took an example to explain further, the code is as follows:

// 普通不变模式
public class User {
  private String name;
  private int age;
  private Address addr;
  
  public User(String name, int age, Address addr) {
    this.name = name;
    this.age = age;
    this.addr = addr;
  }
  // 只有getter方法,无setter方法...
}
public class Address {
  private String province;
  private String city;
  public Address(String province, String city) {
    this.province = province;
    this.city= city;
  }
  // 有getter方法,也有setter方法...
}
// 深度不变模式
public class User {
  private String name;
  private int age;
  private Address addr;
  
  public User(String name, int age, Address addr) {
    this.name = name;
    this.age = age;
    this.addr = addr;
  }
  // 只有getter方法,无setter方法...
}
public class Address {
  private String province;
  private String city;
  public Address(String province, String city) {
    this.province = province;
    this.city= city;
  }
  // 只有getter方法,无setter方法..
}

In a certain business scenario, if an object conforms to the characteristic that it will not be modified after creation, then we can design it as an invariant class. Explicitly enforces it to be immutable so that it cannot be accidentally modified. So how to make an immutable class? The method is very simple, as long as the class satisfies: all member variables are set at one time through the constructor, without exposing any method of modifying member variables such as set. In addition, because the data remains unchanged, there is no concurrent read and write problem, so the invariant mode is often used in a multi-threaded environment to avoid thread locking. Therefore, the invariant pattern is often classified as a multi-threaded design pattern.

Next, let's look at a special kind of immutable class, that is, immutable collections. Google Guava provides corresponding immutable collection classes (ImmutableCollection, ImmutableList, ImmutableSet, ImmutableMap...) for collection classes (Collection, List, Set, Map...). As we just said, the invariant mode is divided into two types, the normal invariant mode and the depth invariant mode. The immutable collection class provided by Google Guava belongs to the former, that is to say, the objects in the collection will not be added or deleted, but the member variables (or attribute values) of the objects can be changed.
In fact, Java JDK also provides immutable collection classes (UnmodifiableCollection, UnmodifiableList, UnmodifiableSet, UnmodifiableMap...). So what is the difference between it and the immutable collection class provided by Google Guava? Let me give you an example to understand, the code is as follows:

public class ImmutableDemo {
  public static void main(String[] args) {
    List<String> originalList = new ArrayList<>();
    originalList.add("a");
    originalList.add("b");
    originalList.add("c");
    List<String> jdkUnmodifiableList = Collections.unmodifiableList(originalList);
    List<String> guavaImmutableList = ImmutableList.copyOf(originalList);
    //jdkUnmodifiableList.add("d"); // 抛出UnsupportedOperationException
    // guavaImmutableList.add("d"); // 抛出UnsupportedOperationException
    originalList.add("d");
    print(originalList); // a b c d
    print(jdkUnmodifiableList); // a b c d
    print(guavaImmutableList); // a b c
  }
  private static void print(List<String> list) {
    for (String s : list) {
      System.out.print(s + " ");
    }
    System.out.println();
  }
}

key review

Well, that's all for today's content. Let's summarize and review together, what you need to focus on.
Today we learned several design patterns used in Google Guava: Builder pattern, Wrapper pattern, Immutable pattern. Again, the content itself is not important, and you don't have to memorize Google Guava's certain class that uses such and such design patterns. In fact, I want to convey to you the following things through the analysis of these source codes.

When we read the source code, we have to ask ourselves, why is it designed like this? Isn't it okay to design it this way? Is there a better design? In fact, many people lack this "questioning" spirit, especially when facing authority (classic books, famous source code, authority figures).

I think I am the most questioning person. I like to challenge authority and convince people with reason. Just like in today's explanation, I understand ForwardingCollection and other classes as the default Wrapper class, which can be used in the three Wrapper modes of decorator, proxy, and adapter to simplify code writing. If you look at Google Guava's Wiki on GitHub, you will find that its understanding of the ForwardingCollection class is different from mine. It simply understands the ForwardingCollection class as the default decorator class, which is only used in the decorator mode. I personally think my understanding is better, what do you think?

In addition, at the beginning of the column, I also mentioned that learning design patterns can help you better read and understand source code. If we don't have previous theoretical study, then the reading of many source codes may only stay at the superficial level, and we will not be able to learn its essence at all. This is like the CacheBuilder mentioned today. I think most people know that it uses the Builder mode, but if they don't have a deep understanding of the Builder mode, few people can explain why they use the Builder mode instead of using the constructor and the set method.

class disscussion

From the last piece of code, we can find that neither the JDK immutable collection nor the Google Guava immutable collection can add or delete data. However, when the original collection increases the data, the data of the JDK immutable collection increases, but the data of Google Guava's immutable collection does not increase. This is the biggest difference between the two. So how do the bottom layers of the two achieve invariance?

Guess you like

Origin blog.csdn.net/qq_32907491/article/details/131365587