Open source practice 1: learn flexible application design patterns by analyzing Java JDK source code

Posted by phpwiz on Thu, 03 Feb 2022 14:48:00 +0100

76 | open source practice I (Part I): learn flexible application design patterns by analyzing Java JDK source code

From today on, we will officially enter the actual combat link. The actual combat link includes two parts: one is the actual combat of open source projects, and the other is the actual combat of projects.

In the practical part of open source projects, I will take you to analyze the design principles, ideas and patterns used in several classic open source projects, including the analysis of five open source projects such as Java JDK, Unix, Google Guava, Spring and MyBatis. In the actual combat part of the project, we carefully selected several actual combat projects and took you hand in hand to analyze, design and code implement them by using the previously learned design principles, ideas and patterns, including authentication permission flow, idempotent retry and gray publishing.

In the next two classes, we will focus on analyzing several common design patterns used in Java JDK. The purpose of learning is to let you realize that in the real project development, you should learn to learn and use flexibly. You must not be too rigid and mechanically copy the design and implementation of design patterns. In addition, for each model, we can't analyze it in detail like the previous theoretical knowledge, many of which are so far. On the premise of having the previous theoretical knowledge, I think you can follow my guidance to study. If you don't understand anything, you can also look back at the previous theoretical explanation.

Application of factory pattern in Calendar Class

When talking about the Factory pattern earlier, most Factory classes are named with Factory as the suffix, and the Factory class is mainly responsible for creating objects. However, in the actual project development, the design of Factory class is more flexible. Let's take a look at an application of Factory pattern in Java JDK: Java util. Calendar. From the naming, we can't see that it is a Factory class.

The Calendar class provides a large number of date related function codes. At the same time, it also provides a getInstance() Factory method to create different Calendar subclass objects according to different timezones and locales. That is, the function code and the Factory method code are coupled in a class. Therefore, even if we check its source code, if we are not careful, it is difficult to find that it uses the Factory mode. At the same time, because it is not just a Factory class, it is not named with Factory as a suffix.

The relevant code of Calendar class is as follows. Most of the code has been omitted. I only give the code implementation of getInstance() factory method. From the code, we can see that getInstance() method can create different Calendar subclass objects according to different timezones and locales, such as BuddhistCalendar, Japanese Imperial Calendar and GregorianCalendar. These details are completely encapsulated in the factory method, and the user only needs to pass the current time zone and address, You can get a Calendar class object to use, and the user doesn't care which Calendar subclass object the obtained object is.

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
  //...
  public static Calendar getInstance(TimeZone zone, Locale aLocale){
    return createCalendar(zone, aLocale);
  }

  private static Calendar createCalendar(TimeZone zone,Locale aLocale) {
    CalendarProvider provider = LocaleProviderAdapter.getAdapter(
        CalendarProvider.class, aLocale).getCalendarProvider();
    if (provider != null) {
      try {
        return provider.getInstance(zone, aLocale);
      } catch (IllegalArgumentException iae) {
        // fall back to the default instantiation
      }
    }

    Calendar cal = null;
    if (aLocale.hasExtensions()) {
      String caltype = aLocale.getUnicodeLocaleType("ca");
      if (caltype != null) {
        switch (caltype) {
          case "buddhist":
            cal = new BuddhistCalendar(zone, aLocale);
            break;
          case "japanese":
            cal = new JapaneseImperialCalendar(zone, aLocale);
            break;
          case "gregory":
            cal = new GregorianCalendar(zone, aLocale);
            break;
        }
      }
    }
    if (cal == null) {
      if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
        cal = new BuddhistCalendar(zone, aLocale);
      } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") {
        cal = new JapaneseImperialCalendar(zone, aLocale);
      } else {
        cal = new GregorianCalendar(zone, aLocale);
      }
    }
    return cal;
  }
  //...
} 

Application of builder pattern in Calendar Class

It is also the calendar class just now. It uses not only the factory mode, but also the Builder mode. As we know, there are two ways to implement the Builder pattern. One is to define a Builder class separately, and the other is to implement the Builder as an internal class of the original class. Calendar adopts the second implementation idea. Let's look at the code first and then explain it. I've posted the relevant code below.

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {
  //...
  public static class Builder {
    private static final int NFIELDS = FIELD_COUNT + 1;
    private static final int WEEK_YEAR = FIELD_COUNT;
    private long instant;
    private int[] fields;
    private int nextStamp;
    private int maxFieldIndex;
    private String type;
    private TimeZone zone;
    private boolean lenient = true;
    private Locale locale;
    private int firstDayOfWeek, minimalDaysInFirstWeek;

    public Builder() {}
    
    public Builder setInstant(long instant) {
        if (fields != null) {
            throw new IllegalStateException();
        }
        this.instant = instant;
        nextStamp = COMPUTED;
        return this;
    }
    //... Omit n multiple set() methods
    
    public Calendar build() {
      if (locale == null) {
        locale = Locale.getDefault();
      }
      if (zone == null) {
        zone = TimeZone.getDefault();
      }
      Calendar cal;
      if (type == null) {
        type = locale.getUnicodeLocaleType("ca");
      }
      if (type == null) {
        if (locale.getCountry() == "TH" && locale.getLanguage() == "th") {
          type = "buddhist";
        } else {
          type = "gregory";
        }
      }
      switch (type) {
        case "gregory":
          cal = new GregorianCalendar(zone, locale, true);
          break;
        case "iso8601":
          GregorianCalendar gcal = new GregorianCalendar(zone, locale, true);
          // make gcal a proleptic Gregorian
          gcal.setGregorianChange(new Date(Long.MIN_VALUE));
          // and week definition to be compatible with ISO 8601
          setWeekDefinition(MONDAY, 4);
          cal = gcal;
          break;
        case "buddhist":
          cal = new BuddhistCalendar(zone, locale);
          cal.clear();
          break;
        case "japanese":
          cal = new JapaneseImperialCalendar(zone, locale, true);
          break;
        default:
          throw new IllegalArgumentException("unknown calendar type: " + type);
      }
      cal.setLenient(lenient);
      if (firstDayOfWeek != 0) {
        cal.setFirstDayOfWeek(firstDayOfWeek);
        cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
      }
      if (isInstantSet()) {
        cal.setTimeInMillis(instant);
        cal.complete();
        return cal;
      }

      if (fields != null) {
        boolean weekDate = isSet(WEEK_YEAR) && fields[WEEK_YEAR] > fields[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
          throw new IllegalArgumentException("week date is unsupported by " + type);
        }
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
          for (int index = 0; index <= maxFieldIndex; index++) {
            if (fields[index] == stamp) {
              cal.set(index, fields[NFIELDS + index]);
              break;
             }
          }
        }

        if (weekDate) {
          int weekOfYear = isSet(WEEK_OF_YEAR) ? fields[NFIELDS + WEEK_OF_YEAR] : 1;
          int dayOfWeek = isSet(DAY_OF_WEEK) ? fields[NFIELDS + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
          cal.setWeekDate(fields[NFIELDS + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        cal.complete();
      }
      return cal;
    }
  }
} 

After reading the above code, I have a question for you to think about: since there is already a getInstance() factory method to create Calendar class objects, why use Builder to create Calendar class objects? What is the difference between the two?

In fact, when we talked about these two modes, we made a detailed comparison between them. Now, let's review them again. Factory mode is used to create objects of different but related types (a group of subclasses inheriting the same parent class or interface). The given parameters determine which type of object to create. Builder mode is used to create a type of complex object. Different objects are created "customized" by setting different optional parameters.

There is a classic example on the Internet that well explains the difference between the two.

When customers enter a restaurant to order, we use the factory model to make different foods according to users' different choices, such as pizza, hamburger and salad. For pizza, users can customize various ingredients, such as cheese, tomato and cheese. We make different pizza according to different ingredients selected by users through the builder mode.

If you take a rough look at the build() method of the Builder class of Calendar, you may think it is a bit like factory mode. You feel right. The first half of the code is indeed similar to the getInstance() factory method, creating different Calendar subclasses according to different type s. In fact, the latter half of the code belongs to the standard Builder mode. The Calendar subclass object just created is customized according to the parameters set by the setXXX() method.

You might say, is this still a builder model? Let me answer you with a paragraph from paragraph 46:

We should not be too academic. We have to distinguish the factory model and the builder model so clearly. What we need to know is why each model is so designed and what problems can be solved. Only by understanding these most essential things can we not copy mechanically and flexibly, and even mix various modes to create new modes to solve the problems of specific scenes.

In fact, from the example of Calendar, we can also learn that we should not apply the principles and implementations of various modes too rigidly, and do not dare to make any changes. The mode is dead, and the person who uses it is alive. In the actual project development, not only various modes can be mixed together, but also the specific code implementation can be flexibly adjusted according to the specific functional requirements.

Application of decorator pattern in Collections class

As we mentioned earlier, Java IO class library is a very classic application of decorator pattern. In fact, the Java Collections class also uses the decorator pattern.

Collections class is a Collection container tool class, which provides many static methods to create various Collection containers. For example, UnmodifiableCollection class objects are created through the unmodifiableColletion() static method. The UnmodifiableCollection class, CheckedCollection and SynchronizedCollection classes in these container classes are the decorator classes for the Collection class.

Because the three decorator classes mentioned just now are almost the same in code structure, we only take the UnmodifiableCollection class as an example. UnmodifiableCollection class is an internal class of Collections class. I have copied the relevant code below. You can have a look first.

public class Collections {
  private Collections() {}
    
  public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
    return new UnmodifiableCollection<>(c);
  }

  static class UnmodifiableCollection<E> implements Collection<E>,   Serializable {
    private static final long serialVersionUID = 1820017752578914078L;
    final Collection<? extends E> c;

    UnmodifiableCollection(Collection<? extends E> c) {
      if (c==null)
        throw new NullPointerException();
      this.c = c;
    }

    public int size()                   {return c.size();}
    public boolean isEmpty()            {return c.isEmpty();}
    public boolean contains(Object o)   {return c.contains(o);}
    public Object[] toArray()           {return c.toArray();}
    public <T> T[] toArray(T[] a)       {return c.toArray(a);}
    public String toString()            {return c.toString();}

    public Iterator<E> iterator() {
      return new Iterator<E>() {
        private final Iterator<? extends E> i = c.iterator();

        public boolean hasNext() {return i.hasNext();}
        public E next()          {return i.next();}
        public void remove() {
          throw new UnsupportedOperationException();
        }
        @Override
        public void forEachRemaining(Consumer<? super E> action) {
          // Use backing collection version
          i.forEachRemaining(action);
        }
      };
    }

    public boolean add(E e) {
      throw new UnsupportedOperationException();
    }
    public boolean remove(Object o) {
       hrow new UnsupportedOperationException();
    }
    public boolean containsAll(Collection<?> coll) {
      return c.containsAll(coll);
    }
    public boolean addAll(Collection<? extends E> coll) {
      throw new UnsupportedOperationException();
    }
    public boolean removeAll(Collection<?> coll) {
      throw new UnsupportedOperationException();
    }
    public boolean retainAll(Collection<?> coll) {
      throw new UnsupportedOperationException();
    }
    public void clear() {
      throw new UnsupportedOperationException();
    }

    // Override default methods in Collection
    @Override
    public void forEach(Consumer<? super E> action) {
      c.forEach(action);
    }
    @Override
    public boolean removeIf(Predicate<? super E> filter) {
      throw new UnsupportedOperationException();
    }
    @SuppressWarnings("unchecked")
    @Override
    public Spliterator<E> spliterator() {
      return (Spliterator<E>)c.spliterator();
    }
    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> stream() {
      return (Stream<E>)c.stream();
    }
    @SuppressWarnings("unchecked")
    @Override
    public Stream<E> parallelStream() {
      return (Stream<E>)c.parallelStream();
    }
  }
}

After reading the above code, please think about why UnmodifiableCollection class is the decorator class of Collection class? Can these two be regarded as simple interface implementation relationship or class inheritance relationship?

As we mentioned earlier, the decorator class in the decorator pattern is an enhancement of the function of the original class. Although the UnmodifiableCollection class can be regarded as a function enhancement of the Collection class, it is not convincing enough to conclude that UnmodifiableCollection is the decorator class of the Collection class.

In fact, the key point is that the constructor of UnmodifiableCollection receives a Collection class object and wraps all its functions: re implementation (such as add() function) or simple encapsulation (such as stream() function). A simple interface implementation or inheritance does not implement the UnmodifiableCollection class in this way. Therefore, from the perspective of code implementation, the UnmodifiableCollection class is a typical decorator class.

Application of adapter pattern in Collections class

In lecture 51, we mentioned that the adapter pattern can be used to be compatible with old version interfaces. At that time, we gave an example of JDK. Here we take a closer look.

The old version of JDK provided the Enumeration class to traverse the container. The new version of JDK uses the Iterator class instead of the Enumeration class to traverse the container. In order to be compatible with the old client code (using the old version of JDK code), we retain the Enumeration class, and in the Collections class, we still retain the Enumeration () static method (because we usually create a container's Enumeration class object through this static function).

However, the Enumeration class and enumeration() function are reserved for compatibility. In fact, they have nothing to do with the adapter. Which part is the adapter?

In the new version of JDK, the Enumeration class is the adapter class. It adapts to the client code (using the Enumeration class) and the new Iterator iterator class in the new version of JDK. However, from the perspective of code implementation, the code implementation of this adapter mode is slightly different from that of the classic adapter mode. The logic of the enumeration() static function is coupled with the code of the Enumeration adapter class. The enumeration() static function directly creates anonymous class objects through new. The specific codes are as follows:

/**
 * Returns an enumeration over the specified collection.  This provides
 * interoperability with legacy APIs that require an enumeration
 * as input.
 *
 * @param  <T> the class of the objects in the collection
 * @param c the collection for which an enumeration is to be returned.
 * @return an enumeration over the specified collection.
 * @see Enumeration
 */
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
  return new Enumeration<T>() {
    private final Iterator<T> i = c.iterator();

    public boolean hasMoreElements() {
      return i.hasNext();
    }

    public T nextElement() {
      return i.next();
    }
  };
}

Key review

Well, that's all for today. Let's summarize and review what you need to master.

Today, I focus on the application of factory mode, builder mode, decorator mode and adapter mode in Java JDK. The main purpose is to show you how to flexibly apply design patterns in real projects.

From today's explanation, we can learn that although we have talked about the classic code implementation of each mode in the previous theoretical explanation, in the real project development, the application of these modes is more flexible and the code implementation is more free. The code implementation can be greatly adjusted according to the specific business scenarios and functional requirements, It may even adjust the design idea of the model itself.

For example, the Calendar class in Java JDK is coupled with three types of code: business function code, factory method and builder class. Moreover, in the build() method of builder class, the first half is the code implementation of factory method, and the second half is the code implementation of real builder model. This also tells us that the application of design mode in the project must not be mechanically applied. It is too academic. We should learn to make flexible adjustments in combination with the actual situation, so that there is no sword in our heart.

Topics: Design Pattern