Common design patterns for Android Development

Posted by PHP Newb on Mon, 27 Dec 2021 00:01:44 +0100

In the book design patterns: the basis of reusable object-oriented software published by GoF, 23 design patterns are recorded. The essence of these design patterns is the understanding of encapsulation, inheritance and polymorphism of object-oriented design. There are 8 design patterns used in app development.

Singleton mode

Singleton pattern is a design pattern that we often use in development. The class created in singleton mode has only one instance in the current process and has a global entry to access it.

1. Advantages of singleton mode

  • There is only one object instance in memory, which saves memory space.
  • It avoids the performance consumption caused by frequent instance creation.
  • Provides a global access entry, such as reading configuration information.

2. Disadvantages of singleton mode

  • General static classes do not provide interface implementation, abstract methods and other functions, and have poor expansion ability. Modification can only be carried out in this singleton class.
  • Because static mode uses static global variables, life cycle references are involved, which can easily lead to memory leakage. For example, an Activity class is passed in. At this time, we need to pass in Application Context with the same length as static life cycle. Otherwise, do not use singleton mode. For example, do not use singleton mode for Dialog dialogue.

3. Applicable scenarios of singleton mode

  • Object needs to save some state information.
  • Avoid multiple read and write operations. For example, multiple instances read the same resource file, and subsequent operations involve writing synchronization to the resource file.

Singleton mode implementation

//There are many implementations of singleton mode, and the most commonly used implementation is shown here. The code is as follows:
public class SingletonDemo {
    
    private static volatile SingletonDemo sInstance = null;
    private SingletonDemo(){}
    
    public static SingletonDemo getInstance(){
        if(sInstance == null){
            synchronized (SingletonDemo.class){
                if(sInstance == null){
                    sInstance = new SingletonDemo();
                }
            }
        }
        return sInstance;
    }
    
    public void  printSomething(){
        System.out.println("this is a singleton");
    }
}

The advantages of this writing are as follows.

  • The constructor private cannot directly create a new object. It is guaranteed to be created through the getInstance method.
  • Since the new object cannot be directly, the getInstance method must be a static method; Static methods cannot access non static member variables, so the instance variable must also be static.
  • Double check lock, using volatile keyword, reordering is prohibited, and all write operations will occur before the operation.
//Implementation using static inner classes
public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
       return SingletonHolder.sInstance;
    }
    private static class SingletonHolder{//This class is static because sInstance is static. If it is not set to static, an error will be reported
       private static final Singleton sInstance=new Singleton();//The focus is on determining which class is the singleton
    }
    public void printSomething(){
        System.out.println("this is a singleton");
    }
}

Static class

You should be familiar with static classes. Methods or variables modified with static can be called directly, which is convenient and fast.

public class StaticDemo{
    public static void printSomething(){
        System.out.println("this is a singleton");
    }
}

This is the simplest static class that provides a static method printSomething().

1. Advantages of static class

  • Methods of static classes can be called directly without new an instance object.
  • The performance of static classes is better because the methods of static classes are bound during compilation.

2. Disadvantages of static classes

  • Static class methods cannot be replicated and have no extensibility.
  • Static classes cannot be lazy loaded.

Selection of singleton class and static class

A singleton represents a class, and a static class represents a method.

  • If you need the extension capability of the class, such as Override, select the singleton mode.
  • If the class is heavy, consider lazy loading and select the singleton mode.
  • If there is a need to maintain status information, select the singleton mode.
  • If you need to access resource files, select the singleton mode.
  • If you need to put some methods together, choose a static class.

Factory mode

Generally speaking, the so-called factory is the place where products are produced. From the perspective of code, products are instance objects of specific classes, and factories are also instance objects used to produce these instance products. Factory pattern is to solve the problem of instantiating objects.

Simple factory

1. Advantages

The factory class is responsible for creating all products. As long as there are product instances you want to create, they can be implemented in the factory class. The factory class is known as the "universal class".

2. Disadvantages

  • As long as a new product is added, the factory class will be modified, which violates the "opening and closing principle" in the design mode, that is, the modification is closed (the factory class needs to be modified for new products) and the extension is open (no extension).
  • The factory will become huge with the increase of product types, and it is not easy to manage and maintain.
package com.brett.myapplication;


public class Main {
    private interface ICar{
        void move();
    }

    private static class Benz implements ICar{

        @Override
        public void move() {

        }
    }

    private static class BMW implements ICar{

        @Override
        public void move() {

        }
    }

    private static class SimpleFactory{
        public static ICar getCar(int carType){
            switch (carType){
                case 0:
                    return new Benz();
                case 1:
                    return new BMW();
            }
            return null;
        }
    }


    public static void main(String[] args) {
        ICar car = SimpleFactory.getCar(0);
        car.move();
    }
}

Factory method

1. Advantages

  • Weaken the general concept of a factory class, and hand over the responsibility of producing products to their respective product factories, that is, each product has a factory class, which is responsible for completing the production of its own products.
  • In accordance with the "opening and closing principle", it is closed for modification (there is no need to modify the factory class) and open for extension (the factory class corresponding to the new product).

2. Disadvantages

  • The factory method implements multiple factory classes, which is more complex to use than a simple factory.
  • The lack of the function of forming product family can be solved in the abstract factory pattern.
package com.brett.myapplication;


public class Main {
    private interface ICar{
        void move();
    }
    
    private interface IFactory{
        ICar getCar();
    }

    private static class Benz implements ICar{

        @Override
        public void move() {

        }
    }

    private static class BMW implements ICar{

        @Override
        public void move() {

        }
    }
    
    private class BenzFactory implements IFactory{

        @Override
        public ICar getCar() {
            return new Benz();
        }
    }
    
    private class BMWFactory implements IFactory{

        @Override
        public ICar getCar() {
            return new BMW();
        }
    }

    public void get(){
        IFactory factory = new BenzFactory();
        ICar car = factory.getCar();
        car.move();
    }
}

3. Implementation of factory method: Generic

package com.brett.myapplication;

public class CarFactory {

    private interface ICar{
        void move();
    }

    private interface IFactory{
        ICar getCar();
    }

    private static class Benz implements ICar {

        @Override
        public void move() {

        }
    }

    private static class BMW implements ICar {

        @Override
        public void move() {

        }
    }
    
    public static ICar createCar(Class<? extends ICar>c){//All products must implement the ICar interface
        try{
            return c.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
    
    ICar bnw = CarFactory.createCar(BMW.class);
}

4. Implementation of factory method: Enum

 enum EnumCarFactory{
        Benz{
            @Override
            public ICar create(){
                return new Benz();
            }
        },
        BMW{
            @Override
            public ICar create(){
                return new BMW();
            }
        };
        
        public abstract ICar create(); 
    }
    
    private void create(){
        try {
            ICar ACar = EnumCarFactory.valueOf("Benz").create();
            ACar.move();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

Abstract factory

1. Introduction

Abstract factory is developed from the concept of "product family".
A product has more than one function. For example, we formulate a set of travel plan for users, which is equipped with vehicles, clothes, etc. these functions together become the functions of the product "crowd". If only vehicles are equipped, it is the same as the factory method mode, with only one function, which is an extreme case.
The so-called abstract factory means that the factory can not only produce a specific product, but also expand to produce a series of products.

package com.brett.myapplication;

public class CarFactory {

    private interface ICar{
        void move();
    }
    
    private interface  IClothes{
        void wear();
    }
    
    private class Gucci implements IClothes{

        @Override
        public void wear() {
            
        }
    }
    
    private class Prada implements IClothes{

        @Override
        public void wear() {
            
        }
    }

    private interface IAbsFactory{
        ICar getCar();
        IClothes getClothes();
    }
    
    private class ZhangSan implements IAbsFactory{

        @Override
        public ICar getCar() {
            return new BMW();
        }

        @Override
        public IClothes getClothes() {
            return new Gucci();
        }
    }
    
    private class LiSi implements IAbsFactory{

        @Override
        public ICar getCar() {
            return new Benz();
        }

        @Override
        public IClothes getClothes() {
            return new Prada();
        }
    }

    private class Benz implements ICar {

        @Override
        public void move() {

        }
    }

    private class BMW implements ICar {

        @Override
        public void move() {

        }
    }
    
    void get(){
        IAbsFactory absFactory = new ZhangSan();
        ICar car = absFactory.getCar();
        car.move();
        IClothes clothes = absFactory.getClothes();
        clothes.wear();
    }
}

Builder mode

Why use Builder mode

The Builder mode is mainly used to solve the problem that there are too many types of constructors and it is not easy to manage when initializing a class (that is, when new is an instance of a class).

Implementation of Builder pattern

Our idea is that there should not be so many constructors for Student class, but we should meet the needs of initializing Student class variables. You can consider designing an internal class. The parameters of this internal class are the same as those of the Student class, and the parameters of the constructor of the Student class are set as this internal class. Therefore, you only need to initialize the variables of this internal class.
When setting internal class variables, we use chain structure, which can be set through setxx() The setxx () form is written all the time.

public class Student{
        private String name;
        private int age;
        private boolean sex;
        
        public Student(){}
        
        public static StudentBuilder newInstance(){
            return new StudentBuilder();
        }
        
        public static class StudentBuilder{
            private String name;
            private int age;
            private boolean sex;
            
            public StudentBuilder setName(String name){
                this.name = name;
                return this;
            }
            
            public StudentBuilder setAge(int age){
                this.age = age;
                return this;
            }
            
            public StudentBuilder setSex(boolean sex){
                this.sex = sex;
                return this;
            }
            
            public Student build(){
                return new Student();
            }
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", sex=" + sex +
                    '}';
        }
    }

Template mode

Introduction to template mode

Template pattern is a design pattern based on code reuse. Implementation requires collaboration between architects and developers. The architect constructs the implementation process and outline, and the developer completes the specific implementation process.

The functions of the parent class abstract template are as follows.

  • The abstract qualifier method is defined and implemented by subclasses.
  • Define non private methods and defer them to subclass implementation.

The functions of subclass implementation template are as follows.

  • Implement the abstract method of the parent class.
  • You can override non private methods of the parent class.
public abstract class Car{
        void startUp(){
            System.out.println("start-up!");
        }
        
        abstract void move();//Mandatory implementation
        
        void stop(){
            System.out.println("Flameout!");
        }
        
        public final void operation(){//It is defined as final to prevent it from being rewritten
            startUp();
            move();
            stop();
        }
        
        public class BMW extends Car{

            @Override
            void move() {
                System.out.println("BWM move!!!");
            }
        }
        
        public class Benz extends Car{

            @Override
            void move() {
                System.out.println("Benz move!!!");
            }
        }

Observer mode

Observer mode, including observer and observed. Observers inform the observed of their needs, and the observed is responsible for notifying the observer.

Java's own observer

package com.brett.myapplication;


import java.util.Observable;
import java.util.Observer;

public class Main {

    //Observed
    public static class Server extends Observable{
        private int time;

        public Server(int time){
            this.time = time;
        }

        public void setTime(int time){
            if(this.time == time){
                setChanged();//It must be marked to indicate that the data changes and the observer needs to be notified
                notifyObservers(time);
            }
        }
    }

    public static class Client implements Observer{

        private String name;
        public Client(String name){
            this.name = name;
        }
        @Override
        public void update(Observable observable, Object o) {
            if(observable instanceof Server){
                // TODO
                System.out.println("Changed");
            }
        }
    }

    public static void main(String[] args) {

        Server server = new Server(2019);
        Client client1 = new Client("Zhang San");
        Client client2 = new Client("Li Si");
        server.addObserver(client1);
        server.addObserver(client2);
        server.setTime(2020);

    }
}

Note that there is no need to remove the observer

 if(server!=null){
    server.deleteObservers();
 }

Implement observer mode yourself

For details, please refer to this blog
Common designer mode observer mode

Adapter mode

Where does the adapter mode need to be used?
The existing system expansion requires access to A third-party system, that is, access to A third-party API: for example, if Class has three fields A, B and C, two fields of the external Class OuterClass need to be added, and they need to be added without affecting the current Class.
The solutions provided by the adapter mode are as follows.

  • To be compatible with the original class, the original class needs interface oriented programming, that is, the interface implementation of the original class.
  • The purpose of the adapter is to be compatible with the original class, so the adapter must also implement the interface of the original class.
  • The adapter implements specific adaptation schemes internally.
    public interface IAmericanCharger{
        void charge4American();
    }
    
    public class AmericanCharger implements IAmericanCharger{

        @Override
        public void charge4American() {
            System.out.println("do American charge!");
        }
    }

    public interface IChineseCharger{
        void charge4Chinese();
    }

    public class ChineseCharger implements IChineseCharger{

        @Override
        public void charge4Chinese() {
            System.out.println("do American charge!");
        }
    }
    
    public class AmericanDevice{
        private IAmericanCharger iAmericanCharger;
        
        public AmericanDevice(IAmericanCharger iAmericanCharger){
            this.iAmericanCharger = iAmericanCharger
        }
        
        public void work(){
            iAmericanCharger.charge4American();
        }
    }
    
    public class Adapter implements IAmericanCharger{
        private IChineseCharger iChineseCharger;
        public Adapter(IChineseCharger iChineseCharger){
            this.iChineseCharger = iChineseCharger
        }

        @Override
        public void charge4American() {
            iChineseCharger.charge4Chinese();
        }
    }
    
    public void get(){
        IChineseCharger chineseCharger = new ChineseCharger();
        Adapter adapter = new Adapter(chineseCharger);
        AmericanDevice device = new AmericanDevice(adapter);
        device.work();
    }

Strategy mode

Each Android module has many solutions, such as okhttp, volley, etc; Picture modules include Glide, Picaso, etc. During normal development, a module may be selected. For example, Glide is used for pictures, and then Glide's interface is directly called in the project code to complete picture processing.
If we want to change another implementation method, the usual approach is to replace the API of all the original modules used in the project with the API of the newly introduced module. This is not only a huge amount of work, but also easy to cause new problems; Moreover, the API of the new module also needs to be re understood and familiar.

Implementation of policy pattern

According to the "opening and closing principle" of the design pattern, we should try our best to close the modification. If the solution mentioned above is used, it is equivalent to strong coupling between business and modules.
So how to solve this "pre coupling"? The answer is interface oriented programming. When using module functions, we try not to directly use the API interface provided by the module, but use the methods provided by the interface we define.
The specific solutions are as follows.

  • Define an interface whose methods are called in our project.
  • All referenced modules must implement this interface. Although the module itself has its own API, we do not directly use the module API, but use the interface we define. So this module must implement the interface we defined.
  • Provide a usage class, usually singleton mode. This usage class is directly called in our project. This usage class can implement or not implement the interface we defined.
  • Specify the referenced third-party module when using class type.
    The advantage of this solution is that no matter how to replace the third-party module, the functions of this module do not need to be changed in the project. Only the third-party module used needs to be set in the configuration.
 public interface ILogProcessor{
        void v(String log);
        void d(String log);
        void i(String log);
        void e(String log);
    }
    
    public class DefaultLogProcessor implements ILogProcessor{

        @Override
        public void v(String log) {
            Log.v("DefaultLogProcessor",log);
        }

        @Override
        public void d(String log) {
            Log.d("DefaultLogProcessor",log);
        }

        @Override
        public void i(String log) {
            Log.i("DefaultLogProcessor",log);
        }

        @Override
        public void e(String log) {
            Log.e("DefaultLogProcessor",log);
        }
    }

    //Provide a usage class
    public class LogLoader implements ILogProcessor{//You can customize the external method name without implementing ILogProcessor
        private static volatile LogLoader sInstance = null;
        private static ILogProcessor sILogProcessor;
        
        private LogLoader(){}
        
        public static LogLoader getInstance(){
            if(sInstance == null){
                synchronized (LogLoader.class){
                    if(sInstance == null){
                        sInstance = new LogLoader();
                    }
                }
            }
            return sInstance;
        }
        
        //Select which log function class to use
        public static ILogProcessor load(ILogProcessor logProcessor){
            return sILogProcessor = logProcessor;
        }

        @Override
        public void v(String log) {
            sILogProcessor.v(log);
        }

        @Override
        public void d(String log) {
            sILogProcessor.d(log);
        }

        @Override
        public void i(String log) {
            sILogProcessor.i(log);
        }

        @Override
        public void e(String log) {
            sILogProcessor.e(log);
        }
    }

proxy pattern

Agent is a middleman, which shields the direct contact between the visiting party and the entrusting party. In other words, the accessor cannot directly call this object of the entrusting party, but must instantiate an agent with the same interface as the entrusting party to complete the call to the entrusting party through this agent.
When does proxy mode need to be used?

  • The visiting party does not want to have direct contact with the entrusting party, or the direct contact is difficult.
  • The visitor's access to the entrusting party requires additional processing, such as processing before and after the access.

There are two types of agent modes: static agent and dynamic agent. For specific implementation methods, please refer to the following blog.
Agent model of design pattern

Agent mode application: simple factory

class ProxyFactory<T>{
    private T client;//Target object
    private IBefore before;
    private IAfter after;
    
    public void setClient(T client){
        this.client = client;
    }
    
    public void setBefore(IBefore before){
        this.before = before;
    }
    
    public void setAfter(IAfter after){
        this.after = after;
    }
    
    public <T> T createProxy(){
        ClassLoader loader = client.getClass().getClassLoader();
        Class[] interfaces = client.getClass().getInterfaces();
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                if("getName".equals(method.getName())){
                    //Filter method based on name value
                }
                if(before != null){
                    before.doBefore();
                }
                Object result = method.invoke(client,objects);//Execute the target method of the target object
                if (after != null){
                    after.doAfter();
                }
                return result;
            }
        };
        return (T) Proxy.newProxyInstance(loader,interfaces,h);
    }
    
    //call
    void get(){
        ProxyFactory factory = new ProxyFactory();
        factory.setBefore(new IBefore() {
            @Override
            public void doBefore() {
                System.out.println("doBefore");
            }
        });
        factory.setClient(new Benz());
        factory.setAfter(new IAfter() {
            @Override
            public void doAfter() {
                System.out.println("doAfter");
            }
        });
        ICar car = (ICar)factory.createProxy();
        car.move();
    }
}

Application of dynamic agent: AOP (aspect oriented programming)

One of the ways to implement AOP is dynamic agent. In short: AOP can dynamically cut the code into the specified location and realize programming at the specified location, so as to achieve the purpose of dynamically changing the original code. The above IBefore and IAfter interfaces actually simply implement AOP, such as inserting some operations before and after the invoke specific method.

Topics: Android Design Pattern