Android源码设计模式探索与实战【面向对象六大基本原则】

IT行业,一直讲一句话,拼到最后都拼的是“内功”,而内功往往就是指我们处理问题的思路、经验、想法,而对于开发者来说,甚至对于产品也一样,都离不开一个“宝典”,就是设计模式。今天我们一起借助Android源码去探索一下设计的六大基本原则。同时结合我工作经验中的两个例子,来总结实践一下。

1.背景&定义

定义:
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

理解:
设计模式是什么?设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

所以,以我自己的理解,设计模式就是一套为了让我们开发写出复用性、可靠性、扩展性更好的代码的经验集合。

2.六大原则定义

2.1. 开闭原则(Open Close Principle )

简言之,对扩展开放,对修改关闭。我们在想要对一个类的功能进行变更的时候,可以通过扩展的方式实现,而不是通过修改这个类本身。那会带来很多灾难性的事故,因为依赖这个类的功能模块可能有很多,一处修改可能导致多处需要修改、同时这个带来的风险是您所不能承受的。

2.2. 单一职责原则(Single Responsibility Priciple )

这个很好理解,就是一个类尽量只负责一个功能。例如:图片加载控件,需要将图片的加载和缓存进行分开。

2.3. 里氏代换原则(Liskov Substitution Principle)

任何基类出现的地方,都可以用子类来替换。这不就是指,Java的多态性之一继承吗?

2.4. 依赖倒置原则(Dependence Inversion Principle)

针对接口编程,依赖于抽象而不依赖于具体。我们在代码编程中,如果想做到以不变应万变,这个原则是基础。您试想,如果一个功能、框架、APP的时候,一层一层的都是对象嵌套调用,那您想想,如果想要修改中间某个对象的功能,此时是不是灾难性的事故现场。所以在编程中,尽量把一个类的进行抽象,其他依赖于这个对象的功能模块,可以去调用这个接口去完成对象具体功能的调用。

2.5. 接口隔离原则(Interface Segregation Principle)

**使用多个隔离的接口,比使用单个接口要好。**既然上一个依赖倒置原则,让我们在代码设计开发中,要把一个类的功能通过接口公开出去,从而让其他依赖模块,可以依赖接口编程。那么自然而然有另外一个问题,这个对象很负责,他有很多功能,那此时我们应该把这个对象的多个功能,分别封装成不同的接口,而不是写在一个接口类中。

2.6. 最少知道(迪米特)原则(Demeter Principle)

一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

3.实战到底

3.1 ImageLoader框架

大家在APP开发中,应该99.9%的开发人员,都用过了各种图片加载框架,例如Picasso、Glide、Fresco,您有没有过这样的冲动去自己写一个类似的图片加载框架呢?接下来,我们就以此为例,边实战边去熟悉一下六大基本原则。
整体设计模式Demo代码

3.1.1 V1.0 基础需求

基础需求:实现一个图片加载框架,支持传入url以及Imageview,可以去下载完成,并且展示,如果此图片已经下载过,则直接加载,不用再次下载。
ImageLoader .java

package com.itbird.design.principle;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.Image;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 自定义图片加载框架
 * Created by itbird on 2022/3/28
 */
public class ImageLoader {
    private static final String TAG = ImageLoader.class.getSimpleName();
    //这里使用LruCanche算法,做内存存储,原理其实就是基于hashmap,存储key、value
    private LruCache<String, Bitmap> mImageCache = null;
    //下载线程池
    private ExecutorService mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    private Handler mHandler;
    private static volatile ImageLoader mInstance = null;

    private ImageLoader(Context context) {
        mHandler = new Handler(context.getMainLooper());
        initLruCache();
    }

    /**
     * 初始化LruCanche算法
     * 需要注意两点,一个是最大内存大小,一个sizeOf每个value的大小
     * 必须指定这两个,因为会涉及到LruCache的存儲于回收
     */
    private void initLruCache() {
        //获取虚拟机最大内存
        int memorySize = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //以最大内存的四分之一去作为算法缓存
        mImageCache = new LruCache<String, Bitmap>(memorySize / 4) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //每张图片占用的内存大小
                return value.getAllocationByteCount() / 1024;
            }
        };
    }

    public static ImageLoader getInstance(Context context) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(context);
                }
            }
        }
        return mInstance;
    }

    /**
     * 给view设置图片
     *
     * @param url
     * @param imageView
     */
    public void setImageView(String url, ImageView imageView) {
        if (TextUtils.isEmpty(url) || imageView == null) {
            return;
        }

        Bitmap bitmap = mImageCache.get(url);
        Log.e(TAG, "" + bitmap);
        if (bitmap == null) {
            downloadImage(url, imageView);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    /**
     * 下载图片
     *
     * @param url
     * @param imageView
     */
    private void downloadImage(String url, ImageView imageView) {
        mExecutor.execute(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadUrlBitmap(url);
                Log.e(TAG, "url = " + url);
                Log.e(TAG, "" + bitmap.getByteCount());

                //cackback主线程
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        imageView.setImageBitmap(bitmap);
                    }
                });
                if (mImageCache != null) {
                    mImageCache.put(url, bitmap);
                }
            }
        });
    }

    /**
     * 真正的下图图片函数
     *
     * @param urlString
     * @return
     */
    private Bitmap downloadUrlBitmap(String urlString) {
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        Bitmap bitmap = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            bitmap = BitmapFactory.decodeStream(in);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
            byte[] bytes = bos.toByteArray();
            bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }
}


测试Activity代码MainActivity.java

package com.itbird.design;

import android.os.Bundle;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

import com.itbird.design.principle.ImageLoader;


public class MainActivity extends AppCompatActivity implements Observer, UIHandler.IHandler {
    Button button;
    TextView textView;
    ImageView imageView;
    UIHandler mUIHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        mUIHandler = new UIHandler(this);

        //测试六大基本原则
        testPrinciple();
    }

    private void initView() {
        button = findViewById(R.id.button);
        textView = findViewById(R.id.textview);
        imageView = findViewById(R.id.imageview);
    }

    /**
     * 图片地址集合
     */
    private final String url[] = {
            "https://img-blog.csdn.net/20160903083245762",
            "https://img-blog.csdn.net/20160903083252184",
            "https://img-blog.csdn.net/20160903083257871",
            "https://img-blog.csdn.net/20160903083311972",
            "https://img-blog.csdn.net/20160903083319668",
            "https://img-blog.csdn.net/20160903083326871"
    };

    private final int MSG_SET_IMAGE_VIEW = 1;

    @Override
    public void handleMessage(Message message) {
        if (message == null) {
            return;
        }

        switch (message.what) {
            case MSG_SET_IMAGE_VIEW:
                ImageLoader.getInstance(this).setImageView((String) message.obj, imageView);
                break;
        }
    }
}

运行一下
Android源码设计模式探索与实战【面向对象六大基本原则】_第1张图片
观察代码,功能的确都是了,但是大家是否发现一个问题,这个多的功能怎么都写在了一个类中,这个类耦合性好高。最简单的,我们可以分为图片下载、图片缓存、图片加载三个功能。

3.1.2 V1.2 优化需求-单一职责优化

Android源码设计模式探索与实战【面向对象六大基本原则】_第2张图片
可以从图中看出,我们分别把图片下载、图片缓存、图片加载分为了三个功能类,图片加载单例类中,通过持有另外两个类的对象,而分别调用下载、缓存功能。
代码如下:

package com.itbird.design.principle;

import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ImageView;

/**
 * 自定义图片加载框架
 * Created by itbird on 2022/3/28
 */
public class ImageLoader implements ImageDownload.DownloadCallback {
    private static final String TAG = ImageLoader.class.getSimpleName();
    private static volatile ImageLoader mInstance = null;
    private ImageCache mImageCache;
    private ImageDownload mImageDownload;

    private ImageLoader(Context context) {
        mImageCache = new ImageCache();
        mImageDownload = new ImageDownload(context, this);
        mImageCache.initLruCache();
    }

    public static ImageLoader getInstance(Context context) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(context);
                }
            }
        }
        return mInstance;
    }

    /**
     * 给view设置图片
     *
     * @param url
     * @param imageView
     */
    public void setImageView(String url, ImageView imageView) {
        if (TextUtils.isEmpty(url) || imageView == null) {
            return;
        }

        Bitmap bitmap = mImageCache.getCache(url);
        Log.e(TAG, "" + bitmap);
        if (bitmap == null) {
            mImageDownload.downloadImage(url, imageView);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    @Override
    public void downloadSuccess(String url, Bitmap bitmap) {
        if (mImageCache != null) {
            mImageCache.putCache(url, bitmap);
        }
    }
}

3.1.3 V1.3 自定义缓存框架需求-依赖倒置&里氏代换

这时我们在基础需求上,增加一个需求,现在要求,不单单要有内存缓存,还要用硬盘缓存,而且还需要支持外界去设置自己的缓存实现框架

这时重新回来审视我们的代码,是不是感觉一团糟?因为虽然单一职责了,但是需求稍微有点变动,我们的主要功能类代码都需要去修改,因为三个功能类之间,完全是依赖于具体实现的。
这是去思考一个问题,缓存嘛,不过硬盘、内存、网盘,有的共同点是什么?不就是put、get、remove,最多再加一个init,所以我们把这部分缓存的共同操作,抽象为统一的行为接口。
ICache.java

package com.itbird.design.principle.v3;

/**
 * 缓存功能接口
 * Created by itbird on 2022/3/28
 */
public interface ICache<T> {
    /**
     * 初始化缓存
     */
    public void init();

    /**
     * 获取缓存
     *
     * @param url
     * @return
     */
    public T getCache(String url);

    /**
     * 添加缓存
     *
     * @param url
     * @param bitmap
     */
    public void putCache(String url, T bitmap);

    /**
     * 移除缓存
     *
     * @param url
     */
    void removeCache(String url);
}

相关的实现类为:

package com.itbird.design.principle.v3;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;

import com.itbird.design.APP;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 硬盘缓存实现
 * Created by itbird on 2022/2/28
 */
public class DiskCache implements ICache<Bitmap> {
    private String TAG = DiskCache.class.getSimpleName();
    private File mRootDir = null;

    @Override
    public void init() {
        mRootDir = APP.getInstance().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    }

    @Override
    public Bitmap getCache(String url) {
        return BitmapFactory.decodeFile(mRootDir.getAbsolutePath() + url.substring(url.lastIndexOf("/")) + ".png");
    }

    @Override
    public void putCache(String url, Bitmap bitmap) {
        File file = new File(mRootDir.getAbsolutePath() + url.substring(url.lastIndexOf("/")) + ".png");
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        Log.d(TAG, file.getAbsolutePath());
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            e.printStackTrace();
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void removeCache(String url) {
        if (TextUtils.isEmpty(url)) {
            return;
        }

        File file = new File("/sdcard/Pictures/" + url);
        if (file == null || !file.exists()) {
            return;
        }
        file.delete();
    }
}

package com.itbird.design.principle.v3;

import android.graphics.Bitmap;
import android.text.TextUtils;
import android.util.LruCache;

/**
 * 内存缓存功能类
 * Created by itbird on 2022/3/28
 */
public class MemoryCache implements ICache<Bitmap> {
    //这里使用LruCanche算法,做内存存储,原理其实就是基于hashmap,存储key、value
    private LruCache<String, Bitmap> mImageCache = null;

    /**
     * 初始化LruCanche算法
     * 需要注意两点,一个是最大内存大小,一个sizeOf每个value的大小
     * 必须指定这两个,因为会涉及到LruCache的存儲于回收
     */
    @Override
    public void init() {
        //获取虚拟机最大内存
        int memorySize = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //以最大内存的四分之一去作为算法缓存
        mImageCache = new LruCache<String, Bitmap>(memorySize / 4) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //每张图片占用的内存大小
                return value.getAllocationByteCount() / 1024;
            }
        };
    }

    public Bitmap getCache(String url) {
        if (mImageCache != null) {
            mImageCache.get(url);
        }
        return null;
    }

    public void putCache(String url, Bitmap bitmap) {
        if (mImageCache != null) {
            mImageCache.put(url, bitmap);
        }
    }

    @Override
    public void removeCache(String url) {
        if (mImageCache != null && TextUtils.isEmpty(url)) {
            mImageCache.remove(url);
        }
    }
}

ImageLoader修改为

package com.itbird.design.principle.v3;

import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.util.Log;
import android.widget.ImageView;

/**
 * 自定义图片加载框架
 * Created by itbird on 2022/3/28
 */
public class ImageLoader implements ImageDownload.DownloadCallback {
    private static final String TAG = ImageLoader.class.getSimpleName();
    private static volatile ImageLoader mInstance = null;
    private ICache mImageCache;
    private ImageDownload mImageDownload;
    private ICache<Bitmap> mDefaultCache = new MemoryCache();

    private ImageLoader(Context context) {
        mImageDownload = new ImageDownload(context, this);
    }

    public void setImageCache(ICache cache) {
        mImageCache = cache;
        mImageCache.init();
    }

    public static ImageLoader getInstance(Context context) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(context);
                }
            }
        }
        return mInstance;
    }

    /**
     * 给view设置图片
     *
     * @param url
     * @param imageView
     */
    public void setImageView(String url, ImageView imageView) {
        if (TextUtils.isEmpty(url) || imageView == null) {
            return;
        }

        if (mImageCache == null) {
            mImageCache = mDefaultCache;
            mImageCache.init();
        }

        Bitmap bitmap = (Bitmap) mImageCache.getCache(url);
        Log.e(TAG, "" + bitmap);
        if (bitmap == null) {
            mImageDownload.downloadImage(url, imageView);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    @Override
    public void downloadSuccess(String url, Bitmap bitmap) {
        if (mImageCache != null) {
            mImageCache.putCache(url, bitmap);
        }
    }
}

具体的修改后的类图为:
Android源码设计模式探索与实战【面向对象六大基本原则】_第3张图片
外界调用时:

  //V3版本
  ImageLoader.getInstance(this).setImageCache(new DiskCache());
  ImageLoader.getInstance(this).setImageView((String) message.obj, imageView);

从类图看,是不是很清晰呢?

小结
1)不单单实现了新需求功能,可以支持外界去自定义实现自己的缓存框架,而且ImageLoader与ImageCache之间完全是依赖于接口编程的。
2)依赖倒置:依赖于接口编程,从类图中就可以看到了,依赖于接口编程的好处,从这里也可以看到,外界想要替换自己的缓存实现类,完全不用修改我们已有类。
3)里氏代换:所有基类出现的地方,都可以用子类来替代。ImageLoader中,把外界设置的具体的实现类(缓存基类的子类),替换为rivate ICache mImageCache;对象,从而在封装层面,只面向接口编程。

3.1.4 V1.4 接口隔离原则的应用

接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署,让客户端依赖的接口尽可能地小。
我们在上面硬盘缓存、文件读写、数据库读写的时候,经常会写这样的代码。

 finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

大家平常开发中,是否有过这样的想法?文件读写、数据库读写本来就是在代码编写中经常遇到的操作,经常在类中写这样的代码,一方面忘记了,那就会引入灾难性的后果,另外一方面,代码不美观而且类的可读性,发现少了。这段代码可能我们平时都这么写,各种try…catch嵌套,都是些简单的代码,但是一旦多了,那基于上面两方面的原因,问题就多了。
我们查看源码,发现这些流操作了,实际上都是实现了Closeable接口。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package java.io;

public interface Closeable extends AutoCloseable {
    void close() throws IOException;
}

Android源码设计模式探索与实战【面向对象六大基本原则】_第4张图片
所以基于接口隔离原则,我们可以把这部分,封装如下:


package com.itbird.utils;

import java.io.Closeable;
import java.io.IOException;

public class CloseUtils {

    /**
     * 关闭 Closeable
     * @param closeable
     */
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

代码简洁了很多!保证了代码的重用性。close 方法的基本原理就是依赖于 Closeable 抽象而不是具体实现(这其实也是依赖倒置),并且建立在最小化依赖原则的基础,它只需要知道这个对象是可关闭,其他的一概不关心,也就是这里的接口隔离原则。

Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以及依赖倒置(也称为依赖反转)5个原则定义为SOLID原则,指代了面向对象编程的5个基本原则。当这些原则被一起应用时,它们使得一个软件系统更清晰、简单、最大程度地拥抱变化。

3.2 业务、数据、界面分离封装

在刚刚接触android的时候,或者说,现在依然有很大一部分的APP开发者,在开发过程中,总是习惯在一个Activity、Fragment中几乎完成了所有的功能。例如网络请求、数据加载、业务逻辑处理、界面加载、界面动画。
后来渐渐的我们接触到了MVC、MVP各种官方的框架,懂得了模块的分离、解耦(MVC),懂得了通过依赖于抽象去分离各个模块彻底解耦(MVP)。
但是官方的MVP框架的确已经帮我们做了很多,但是依然不够,接下来,我们基于官方给的MVP框架,结合六大基本原则,去封装更加适宜于项目的MVP框架。
整体设计模式Demo代码

3.2.1 V1.0 Google官方的MVP

官方Demo怎么去做的?
Android源码设计模式探索与实战【面向对象六大基本原则】_第5张图片
我们为了方便去分析,我这里简化代码,我们逐步分析,BaseView 和 BasePresenter,BaseView谷歌是这么写的(其实就是view的接口,展示view),以下样例为了理解,我简化处理了部分代码
BaseView.java

package com.itbird.design.principle.mvp.google;

/**
 * Google Demo
 * Created by itbird on 2022/2/25
 */
public interface BaseView<T> {
    //View中,设置presenter对象,使View可以持有Presenter对象引用
    void setPresenter(T presenter);
}

BasePresenter

package com.itbird.design.principle.mvp.google;

/**
 * Google Demo
 * Created by itbird on 2022/2/25
 */
public interface BasePresenter {
}

TaskDetailContract

package com.itbird.design.principle.mvp.google;

/**
 * Google Demo
 * Created by itbird on 2022/2/25
 */
public interface TaskDetailContract {
    interface View extends BaseView<Presenter> {
        //界面UI刷新方法
        void updateTextView(String s);
    }

    interface Presenter extends BasePresenter {
        void loadDataFromModel();
    }
}

TaskGoogleActivity

package com.itbird.design.principle.mvp.google;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.itbird.design.R;


public class TaskGoogleActivity extends AppCompatActivity implements TaskDetailContract.View {

    private static final String TAG = TaskGoogleActivity.class.getSimpleName();
    private TaskDetailContract.Presenter mPresenter;
    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.textview);
        Log.e(TAG, TAG + " onCreate");
        new TaskGooglePresenter(this);
    }

    @Override
    public void setPresenter(TaskDetailContract.Presenter presenter) {
        mPresenter = presenter;
    }

    @Override
    public void updateTextView(String s) {
        mTextView.setText(s);
    }
}

TaskGooglePresenter

package com.itbird.design.principle.mvp.google;

public class TaskGooglePresenter implements TaskDetailContract.Presenter {
    private static final String TAG = TaskGooglePresenter.class.getSimpleName();
    TaskDetailContract.View mView;

    public TaskGooglePresenter(TaskDetailContract.View view) {
        mView = view;
        mView.setPresenter(this);
    }

    @Override
    public void loadDataFromModel() {
        //TODO :loaddata,此处可以用model、或者进行业务操作
        //调用界面方法,进行数据刷新
        mView.updateTextView("loaddata success!!!");
    }
}

小结
我们单从设计来说, Google MVP Demo的确向我们展示了MVP的优点:
1)业务、数据、视图分离、解耦,从六大基本原则上来说,开闭、单一职责、里氏代换、依赖倒置、接口隔离、最少知道,基本都做到了。
2)由于完全分离,所以我们很方便针对于每层去选取对应的单元测试模式,例如针对于数据层可以使用(Junit+Mockito)、视图层可以使用(AndroidJunitRunner+Espresso)、业务层可以使用(Junit+Mockito)
3)通过Contract 契约类,完全将视图、业务相关的接口,封装放在了一个地方,简单明了,针对于开发者来说,不容易忘记和辅助养成习惯
但是,但是对于我们实际开发使用来说,依然有以下几点问题:
1)setPresenter接口完全没有必要存在,因为Presenter对象一定是在View类中new出来的,我既然都有它自己的对象了,我干嘛还要在Presenter内部,去调用mView.setPresenter(this);,再返回View中,再去保存一个引用,代码看着很怪
2)我们知道MVP的精髓在与,P、V肯定要相互交互,他们互相要持有对方的引用,通过上面一点,我们知道Presenter对象一定是在View类中new出来的,所以View肯定有Presenter对象的引用,这个没问题了。但是Presenter要想拥有View的引用,只能通过Presenter构造方法、或者Presenter内部有一个setView方法,让开发者自己去调用setView或者实现带参构造方法,从设计角度来说,这明显是一个坑,因为一个框架的设计,是为了更加方便开发,而不是让开发人员设置这个、设置那个、必须调用某个方法。既然是必须调用的方法,我们应该通过框架去内部消化掉,而不是给开发者制造麻烦
3)Presenter中拥有View的引用,如果activity、fragment销毁,但是presenter依然在执行某些任务,这样会导致activity、fragment无法GC回收,导致内存泄露,甚至与崩溃,所以这也是一个框架必须解决的问题。

3.2.2 V1.1 My MVP V1

基于上面提出的三点,我们去优化Google的MVP框架。
我们首先将第一点和第二点通过抽象来解决一下。
IPresenter

package com.itbird.design.principle.mvp.v1;

/**
 * 自定义MVP框架,BasePresenter
 * Created by itbird on 2022/2/25
 */
public interface IPresenter {
    /**
     * 与view班定
     *
     * @param view
     */
    void onAttach(IView view);

    /**
     * 与view解绑
     */
    void onDetach();

    /**
     * 是否与view已经班定成功
     *
     * @return
     */
    boolean isViewAttached();

    /**
     * 获取view
     * @return
     */
    IView getView();
}

IView

package com.itbird.design.principle.mvp.v1;

/**
 * 自定义MVP框架,BaseView
 * Created by itbird on 2022/2/25
 */
public interface IView {
}

接下来是借助activity生命周期,对presenter的初始化进行封装
BaseActivity

package com.itbird.design.principle.mvp.v1;

import android.app.Activity;
import android.os.Bundle;

import androidx.annotation.Nullable;

/**
 * Created by itbird on 2022/3/29
 */
public abstract class BaseActivity extends Activity implements IView {
    IPresenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = createPresenter();
        if (mPresenter != null) {
            mPresenter.onAttach(this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mPresenter != null) {
            mPresenter.onDetach();
            mPresenter = null;
        }
    }

    abstract IPresenter createPresenter();
}

BasePresenter

package com.itbird.design.principle.mvp.v1;


import java.lang.ref.WeakReference;

/**
 * Created by itbird on 2022/3/29
 */
public class BasePresenter<V extends IView> implements IPresenter {
    WeakReference<V> mIView;

    @Override
    public void onAttach(IView iView) {
        mIView = new WeakReference<>((V) iView);
    }

    @Override
    public void onDetach() {
        mIView = null;
    }

    @Override
    public V getView() {
        if (mIView != null) {
            mIView.get();
        }
        return null;
    }

    @Override
    public boolean isViewAttached() {
        return mIView != null && mIView.get() != null;
    }
}

Android源码设计模式探索与实战【面向对象六大基本原则】_第6张图片

3.2.3 V1.2 My MVP V2

看上图类图,我们依然发现有一些不满足的点:
1)activity中,依然需要初始化mPresenter

 @Override
    IPresenter createPresenter() {
        mTaskPresenter = new TaskMyPresenter();
        return mTaskPresenter;
    }

是否可以做到在activity中,自己像presenter中调用view一样,自己一句话getView就可以搞定
2)观察类图,其实IView、IPresenter没有必要存在,因为毕竟只是baseActivity、basePresenter的行为

所以改造如下:
BasePresenter

package com.itbird.design.principle.mvp.v2;


import java.lang.ref.WeakReference;

/**
 * Created by itbird on 2022/3/29
 */
public abstract class BasePresenter<V> {
    WeakReference<V> mIView;

    public void onAttach(V iView) {
        mIView = new WeakReference<>(iView);
    }

    public void onDetach() {
        mIView = null;
    }

    public V getView() {
        if (mIView != null) {
            mIView.get();
        }
        return null;
    }

    public boolean isViewAttached() {
        return mIView != null && mIView.get() != null;
    }
}

BaseActivity

package com.itbird.design.principle.mvp.v2;

import android.app.Activity;
import android.os.Bundle;

import androidx.annotation.Nullable;


/**
 * Created by itbird on 2022/3/29
 */
public abstract class BaseActivity<V, T extends BasePresenter<V>> extends Activity {
    T mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = initPresenter();
        if (mPresenter != null) {
            mPresenter.onAttach((V) this);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mPresenter != null) {
            mPresenter.onDetach();
            mPresenter = null;
        }
    }

    public T getPresenter() {
        return mPresenter;
    }


    abstract T initPresenter();
}

此时,契约类,不再需要依赖BasePresenter/BaseView相关接口,而且View中也可以自己获取到presenter的引用了。

package com.itbird.design.principle.mvp.v2;


/**
 * my Demo
 * Created by itbird on 2022/2/25
 */
public interface TaskMyContract {
    interface View {
        //界面UI刷新方法
        void updateTextView(String s);
    }

    interface Presenter {
        void loadDataFromModel();
    }
}

package com.itbird.design.principle.mvp.v2;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.itbird.design.R;

public class TaskMyActivity extends BaseActivity<TaskMyContract.View, TaskMyPresenter> implements TaskMyContract.View {

    private static final String TAG = TaskMyActivity.class.getSimpleName();
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.textview);
        Log.e(TAG, TAG + " onCreate");

        mPresenter.loadDataFromModel();
    }

    @Override
    TaskMyPresenter initPresenter() {
        return new TaskMyPresenter();
    }


    @Override
    public void updateTextView(String s) {
        mTextView.setText(s);
    }
}

Android源码设计模式探索与实战【面向对象六大基本原则】_第7张图片
此时的类图,是否更加明确,而且上面各个问题都已经得到了解决。

整体设计模式Demo代码

你可能感兴趣的:(java,MVP,图片加载框架,ImageLoader,六大基本原则)