The following content from: “Android source code design pattern analysis and practice”
When it comes to six principles for dealing with a partner, most people can probably name one or two. But how to apply it to your own code is not a small problem. This article will use a practical example, and use six principles to transform, in the process of transformation experience.
Let’s look at a common Android function module – picture loading. Without using any existing frameworks, you might write something like this:
Public class ImageLoader {// LruCache<String, Bitmap> mImageCache; // a thread pool with a fixed number of threads The number of threads for CPU ExecutorService mExecutorService = Executors. NewFixedThreadPool (Runtime. The getRuntime (). AvailableProcessors ()); publicImageLoader() {
initImageCache();
}
private void initImageCache() {// Calculate the maximum memory available int maxMemory = (int) (runtime.geTruntime ().maxMemory() / 1024); Int cacheSize = maxMemory /4; mImageCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) {returnvalue.getRowBytes() * value.getHeight() * 1024; }}; } public void displayImage(final String url, final ImageView imageView) { imageView.setTag(url); mExecutorService.submit(newRunnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
if(imageView.getTag().equals(url)) { imageView.setImageBitmap(bitmap); } mImageCache.put(url, bitmap); }}); } private Bitmap downloadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); }returnbitmap; }}Copy the code
This does the job of loading images, but the ImageLoader is heavily coupled and has no design, let alone scalability or flexibility. All the functionality is in a single class, and as more functionality is added, the ImageLoader class gets bigger and the code gets more complex, making the image loading system more vulnerable. Next we will try to adapt the following ImageLoader with the single responsibility principle.
Single responsibility principle
**Single Responsibility Principle **
Definition: There should be only one reason for a class to change. In simple terms, a class should be a collection of functions and data that are highly correlated.
Although everyone has their own opinion on how to divide the responsibilities of a class and a function, it depends on personal experience and specific business logic. But two completely different functions should not be in the same class. So from a single responsibility point of view, ImageLoader can obviously be divided into two parts, one is image loading; The other is image caching. So we remodel it like this:
Public ImageLoader {// ImageCache mImageCache = new ImageCache(); // a thread pool with a fixed number of threads The number of threads for CPU ExecutorService mExecutorService = Executors. NewFixedThreadPool (Runtime. The getRuntime (). AvailableProcessors ()); public void displayImage(final String url, final ImageView imageView) { Bitmap bitmap = mImageCache.get(url);if(bitmap ! = null) { imageView.setImageBitmap(bitmap);return;
}
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
if(imageView.getTag().equals(url)) { imageView.setImageBitmap(bitmap); } mImageCache.put(url, bitmap); }}); } private Bitmap downloadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); }returnbitmap; }}Copy the code
Extract ImageCache to handle ImageCache:
public class ImageCache {
LruCache<String, Bitmap> mImageCache;
public ImageCache() {
initImageCache();
}
private void initImageCache() {// Calculate the maximum memory available int maxMemory = (int) (runtime.geTruntime ().maxMemory() / 1024); Int cacheSize = maxMemory /4; mImageCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) {returnvalue.getRowBytes() * value.getHeight() * 1024; }}; } public void put(String url, Bitmap bitmap) { mImageCache.put(url, bitmap); } public Bitmap get(String url) {returnmImageCache.get(url); }}Copy the code
ImageLoader is only responsible for image loading logic, and ImageCache is only responsible for image caching logic, so ImageLoader code is less, the responsibility is also clear; There is no need to modify the ImageLoader class when the cache-specific logic needs to be changed, and the image loading logic needs to be changed without affecting the cache processing logic.
The open closed principle
Open Close Principe
Definition: Objects (classes, modules, functions, etc.) in software should be open for extension, but closed for modification. During the software life cycle, when the original software code needs to be modified due to changes, upgrades, and maintenance, errors may be introduced into the original tested code, damaging the original system. Therefore, when software needs to change, we should try to achieve the change by extension rather than by modifying the existing code. Of course, in the real development, only through inheritance to upgrade and maintain the original system is just an ideal vision, therefore, in the actual development process, modify the original code, extension code often exist at the same time.
Let’s look at ImageLoader. Although the memory cache solves the problem of loading images from the network every time, Android applications have limited memory and are volatile, which means that when the application is restarted, the images that have been loaded will be lost, so you need to download again after restarting! This leads to slow loading and heavy user traffic. To solve this problem, let’s add an SD card cache class:
public class DiskCache {
static String cacheDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/";
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cacheDir + url);
}
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(fileOutputStream ! = null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }Copy the code
Then change the ImageLoader code accordingly and add the SD card cache:
Public class ImageLoader {// ImageCache mImageCache = new ImageCache(); //SD card cache DiskCache mDiskCache = new DiskCache(); // Whether to use SD card cache Boolean isUseDiskCache =false; // a thread pool with a fixed number of threads The number of threads for CPU ExecutorService mExecutorService = Executors. NewFixedThreadPool (Runtime. The getRuntime (). AvailableProcessors ()); Public void displayImage(final String URL, final ImageView ImageView) {// Determine which cache to use Bitmap Bitmap = isUseDiskCache? mDiskCache.get(url) : mImageCache.get(url);if(bitmap ! = null) { imageView.setImageBitmap(bitmap);return;
}
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(url)) {
imageView.setImageBitmap(bitmap);
}
if (isUseDiskCache) {
mDiskCache.put(url, bitmap);
} else{ mImageCache.put(url, bitmap); }}}); } public void useDiskCache(boolean useDiskCache) { isUseDiskCache = useDiskCache; } private Bitmap downloadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); }returnbitmap; }}Copy the code
We added a DiskCache class. You can use the useDiskCache method to specify which cache to use, for example:
ImageLoader imageLoader = new ImageLoader(); Imageloader. useDiskCache(true); Imageloader. useDiskCache(false);
Copy the code
However, you cannot use memory cache and SD card cache at the same time. Memory cache should be used first, SD card cache should be used if there are no images in memory cache, SD card cache should be used last if there are no images in SD card cache, this is the best cache strategy. We are adding a double-cached class called DoubleCache:
public class DoubleCache {
ImageCache mMemoryCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
public Bitmap get(String url) {
Bitmap bitmap = mMemoryCache.get(url);
if (bitmap == null) {
bitmap = mDiskCache.get(url);
}
returnbitmap; } public void put(String url, Bitmap bitmap) { mMemoryCache.put(url, bitmap); mDiskCache.put(url, bitmap); }}Copy the code
And the same applies to ImageLoader:
Public class ImageLoader {// ImageCache mImageCache = new ImageCache(); //SD card cache DiskCache mDiskCache = new DiskCache(); // DoubleCache DoubleCache mDoubleCache = new DoubleCache(); // Using SD card cache Boolean isUseDiskCache =false; // Use double cache Boolean isUseDoubleCache =false; // a thread pool with a fixed number of threads The number of threads for CPU ExecutorService mExecutorService = Executors. NewFixedThreadPool (Runtime. The getRuntime (). AvailableProcessors ()); Public void displayImage(final String URL, final ImageView ImageView) {// Determine which cache to use Bitmap Bitmap = null;if (isUseDoubleCache) {
bitmap = mDoubleCache.get(url);
} else if (isUseDiskCache) {
bitmap = mDiskCache.get(url);
} else {
bitmap = mImageCache.get(url);
}
if(bitmap ! = null) { imageView.setImageBitmap(bitmap);return; } imageView.setTag(url); } public void useDiskCache(Boolean useDiskCache) {isUseDiskCache = useDiskCache; } public void useDoubleCache(boolean useDoubleCache) { isUseDoubleCache = useDoubleCache; }}Copy the code
You can now use either an in-memory cache or an SD card cache, or both, but there are still some problems. Looking at the current code, ImageLoader uses Boolean variables to let the user choose which cache to use, so there are various if-else statements that determine which cache to use. As this logic is introduced, the code becomes more complex and fragile, and if you accidentally write an if condition wrong, it takes more time to resolve, and the entire ImageLoader class becomes more and more bloated. Also, users cannot implement cache injection into ImageLoader themselves, which is not scalable.
This code clearly does not meet the open and close principle. To meet the requirements mentioned above, we can continue to adapt using the policy pattern. Let’s take a look at the UML diagram:
Next, we reworked the code to UML diagrams so that ImageCache became an interface and had three implementation classes: MemoryCache, DiskCache, and DoubleCache.
public interface ImageCache {
void put(String url, Bitmap bitmap);
Bitmap get(String url);
}
public class MemoryCache implements ImageCache {
...
}
public class DiskCache implements ImageCache {
...
}
public class DoubleCache implements ImageCache {
...
}
Copy the code
ImageLoader looks like this:
Public class ImageLoader {// cache ImageCache mImageCache = new MemoryCache(); // a thread pool with a fixed number of threads The number of threads for CPU ExecutorService mExecutorService = Executors. NewFixedThreadPool (Runtime. The getRuntime (). AvailableProcessors ()); public voidsetImageCache(ImageCache imageCache) {
mImageCache = imageCache;
}
public void displayImage(final String url, final ImageView imageView) {
Bitmap bitmap = mImageCache.get(url);
if(bitmap ! = null) { imageView.setImageBitmap(bitmap);return; } // The image is not cached, submit to the thread pool to download submitLoadRequest(URL, imageView); } private void submitLoadRequest(final String url, final ImageView imageView) { imageView.setTag(url); mExecutorService.submit(newRunnable() {
@Override
public void run() {
Bitmap bitmap = downloadImage(url);
if (bitmap == null) {
return;
}
if(imageView.getTag().equals(url)) { imageView.setImageBitmap(bitmap); } mImageCache.put(url, bitmap); }}); } private Bitmap downloadImage(String imageUrl) { Bitmap bitmap = null; try { URL url = new URL(imageUrl); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); connection.disconnect(); } catch (Exception e) { e.printStackTrace(); }returnbitmap; }}Copy the code
The setImageCache(ImageCache ImageCache) function is used to set the cache implementation, also known as dependency injection:
ImageLoader = new ImageLoader(); / / use the SD card cache imageLoader. SetImageCache (new DiskCache ()); / / using double cache imageLoader. SetImageCache (new DoubleCache ()); / / use a custom cache imageLoader. SetImageCache (newImageCache(){ @Ovrride public void put(String url, @ovrride public Bitmap get(String url){retrun /* Get the image from the cache */; }});Copy the code
Now let’s see, whatever cache is added later, we can implement the ImageCache interface and inject it through the setImageCache method without changing the ImageLoader class code. This is the open closed principle: open for extension, closed for modification. Design patterns, however, are a better way to adhere to principles.
Richter’s substitution principle
Liskov Substitution Principle
Definition: Type S is a subtype of type T if for every object O1 of type S, there is an exclusive O2 of type T, such that the behavior of all programs P defined by T does not change when all objects O1 is replaced by O2.
Another way to describe it is that all references to a base class must be able to transparently use objects from its subclasses.
Let’s look at the relationship between Windows and Views in Android:
Window depends on View, and View defines a View abstraction. Measure is a method shared by all subclasses. Subclasses achieve their own features by copying the Draw method of View, that is, drawing their own content. Any subclass that inherits from the View class can be set to the show method. With inline substitution, you can customize various, ever-changing views and pass them to the Window, which organizes the View and displays it on the screen.
The core principle of Li substitution is abstraction, and abstraction relies on inheritance. In OOP, the advantages and disadvantages of inheritance are obvious:
- Code reuse, reducing the cost of creating classes, with each subclass owning the methods and attributes of its parent class;
- The subclass is similar to the parent class, but different from the parent class.
- Improve code extensibility.
Disadvantages of inheritance:
- Inheritance is intrusive and must have all the attributes and methods of the parent class;
- May result in redundant subclass code and reduced flexibility, because subclasses must have the attributes and methods of their parent class.
Everything always has two sides, how to weigh the pros and cons is to make a choice according to the specific situation and deal with it.
MemoryCache, DiskCache, and DoubleCache can all replace the work of ImageCache and ensure correct behavior. ImageCache establishes the interface specification for obtaining and saving cached images, and MemoryCache and other relevant functions are realized according to the interface specification. Users only need to specify specific cache objects when using to dynamically replace the cache policy in ImageLoader. This makes ImageLoader’s caching system infinitely scalable.
The open and closed principle and the in-type replacement principle are often life and death dependent, never abandon, through the in-type replacement to achieve the expansion of the open, closed to modify the effect. However, both principles also emphasize abstraction, an important feature of OOP, so using abstraction in your development process is an important step toward code optimization.
Dependency inversion principle
Dependence Inversion Principe
Definition: is a special form of decoupling in which a high-level module is not dependent on the implementation details of a low-level module for purposes that are reversed. What exactly does that mean? The dependency inversion principle has the following key points:
- A high-level module should not depend on a low-level module; both should depend on its abstraction;
- Abstraction should not depend on details;
- Details should depend on abstractions.
In Java, abstraction is an interface or abstract class, neither of which can be instantiated directly; A detail is an implementation class, and a class that implements an interface or inherits an abstract class is a detail that can be instantiated directly. The high-level module is the calling side, and the low-level module is the concrete implementation class. The principle of dependency inversion is expressed in Java language as follows: dependencies between modules occur through abstraction, while there are no direct dependencies between implementation classes. Their dependencies are generated through interfaces or abstract classes. In plain English, this is interface (abstract) programming.
If classes depend directly on the details, there is direct coupling between them, and when the implementation needs to change, that means modifying the code of that dependent at the same time, which limits the extensibility of the system. The ImageLoader example initially relies directly on MemoryCache as a concrete implementation rather than an abstract class or interface. This leads us to change the code of the ImageLoader class when we later modify other caching implementations.
The final version of ImageLoader is a good example of relying on abstraction. We extract the ImageCache interface and define two methods. ImageLoader relies on abstraction (the ImageCache interface) rather than a concrete implementation class. When requirements change, we can replace the original implementation with another implementation.
Interface Isolation Principle
InterfaceSegregation Principe
Definition: A client should not rely on interfaces it does not need. Another definition: Dependencies between classes should be recommended on the smallest interface. The interface isolation principle breaks down very large, bloated interfaces into smaller and more specific interfaces, so that customers will only need to know the methods they are interested in. The purpose of the interface isolation principle is to decouple systems so they can be easily refactored, changed, and redeployed.
The principle of interface isolation is to keep the interface on which the client depends as small as possible. This may seem a bit abstract, but let’s use an example to illustrate:
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(cacheDir + url);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if(fileOutputStream ! = null) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); }}}}Copy the code
This code is very unreadable, the various try-catch nesting is simple code, but it severely affects the readability of the code, and multiple levels of curly braces can easily write code into the wrong level. Let’s see how we can solve these problems. We may know that There is a Closeable interface in Java that identifies a Closeable object with only a close method. FileOutputStream implements this interface. We could try writing a utility class that specifically turns off the implementation of the Closeable interface.
public final class CloseUtils {
private CloseUtils() {
}
public static void closeQuietly(Closeable closeable) {
if(closeable ! = null) { try { closeable.close(); } catch (IOException e) { e.printStackTrace(); }}}}Copy the code
Let’s apply this utility class to the example above to see what happens:
public void put(String url, Bitmap bitmap) { FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(cacheDir + url); bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { CloseUtils.closeQuietly(fileOutputStream); }}Copy the code
Isn’t that much simpler?! And the closeQuietly method can be applied to a variety of closed objects, ensuring code reuse. Why is the close method defined in a FileOutputStream (or its parent) instead of a separate interface for this method? From the user’s point of view, the CloseUtils utility class only cares about the close method, not the other methods of FileOutputStream. Without such a Closeable interface, The closeQuietly method has to be defined as a FileOutputStream, which exposes other methods that are not necessary, and other classes that also have a close method cannot be closed using closeQuietly.
Think of OnClickListener and OnLongClickListener in Android. Although both are clicks, they are not defined in one interface, but in two, because most of the time, users only care about one or the other.
Demeter principle
Law of Demeter or Least Konwledge Principe
Definition: An object should know the least about other objects. In layman’s terms, a class should know the least about the classes it needs to couple or call, and how the class is implemented internally has nothing to do with the caller or dependent. The closer the relationship between classes, the greater the degree of coupling, when one class changes, the greater the impact on the other class. Now let’s use renting as an example to talk about the application of Demeter’s principle. Room:
public class Room {
public float area;
public float price;
public Room(float area, float price) {
this.area = area;
this.price = price;
}
@Override
public String toString() {
return "Room [area=" + area + ",price=" + price + "]"; }}Copy the code
Room:
public class Mediator {
List<Room> mRooms = new ArrayList<>();
public Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(14 + i, (14 + i) * 1500));
}
}
public List<Room> getAllRooms() {
returnmRooms; }}Copy the code
Tenant:
public class Tenant {
public float roomArea;
public float roomPrice;
public static final floatDiffPrice = 100.0001 f; public static finalfloatDiffArea = 0.00001 f; public void rentRoom(Mediator mediator) { List<Room> allRooms = mediator.getAllRooms();for (Room room : allRooms) {
if (isSuitable(room)) {
System.out.println("Got a room!" + room);
}
}
}
private boolean isSuitable(Room room) {
returnMath.abs(room.price - roomPrice) < diffPrice && Math.abs(room.area - roomArea) < diffArea; }}Copy the code
As you can see from the code, Tenant not only relies on the Mediator class, but also interacts frequently with the Room class. If the detection criteria are all in the Tenant class, the mediation class is weakened, leading to the coupling of Tenant to Room, because Tenant must know many details about the Room, and Tenant must change as the Room changes. As described in UML below:
Since the coupling is too severe, we have to decouple. The first thing to be clear is that we only communicate with the necessary classes to remove Tenant and Room dependencies. We make the following modifications:
public class Mediator {
List<Room> mRooms = new ArrayList<>();
public Mediator() {
for (int i = 0; i < 5; i++) {
mRooms.add(new Room(14 + i, (14 + i) * 1500));
}
}
public Room rentOut(float area, float price) {
for (Room room : mRooms) {
if (isSuitable(area, price, room)) {
returnroom; }}return null;
}
private boolean isSuitable(float area, float price, Room room) {
returnMath.abs(room.price - price) < Tenant.diffPrice && Math.abs(room.area - area) < Tenant.diffArea; }}Copy the code
public class Tenant {
public float roomArea;
public float roomPrice;
public static final floatDiffPrice = 100.0001 f; public static finalfloatDiffArea = 0.00001 f; public void rentRoom(Mediator mediator) { System.out.println("Got a room!"+ mediator.rentOut(roomArea, roomPrice)); }}Copy the code
The reconstructed UML diagram is as follows:
It just moved the judgment operation of Room to Mediator, which should have been the responsibility of Mediator. According to the conditions set by the tenant, mediators searched for houses meeting the requirements and handed the result to the tenant. Tenants don’t need to know too much about Room.