设计模式与Android(篇一)

试图看懂 --> 试图模仿 --> 试图记住 --> 试图熟练

对于设计模式这是牛人们对代码中的一定场景而进行提炼的结果, 对于一个进阶的开发人员这是一个必不可少的技能. 当代码越写越好, 更易扩展更加灵活. 这对于Coder来说是最酷的事情.

通过设计模式Android源码中的关系, 可以更加清楚的记住各个模式的特点, 和源码中的实现方式. 多练多分析之间的关系这是必不可少的一步!

本篇就是<Android源码设计模式分析>一书的缩减版. 这本书挺不错的. 片中类图讲解出自<大话设计模式>

灵活之路 - 面向对象六大原则

如果下面文字描述,不是很明白那么跳转到后面链接有详细代码说明原则解析

  • 单一职责原则SRP(Single Responsibility Principle)

    • 定义: 就一个类而言, 应该仅有一个引起它的变化的原因. 通俗点就是一个类应该是相关性很高数据封装
    • 举例: 现在有一个图片加载类. 但是这个类内部包含了图片下载的逻辑,图片缓存的逻辑这样就使得这个类的职责过多, 随着功能的不断完善, 这个类的代码和逻辑也变得纵横交错混合在了一起. 对于后续的修改维护扩展都是不利的. 所以让两个类组合起来, 一个类内部只负责图片下载,另一个类内部负责图片缓存. 保持每个类的单一职责
  • 开闭原则OCP(Open Close Principle)

    • 定义: 软件中的对象应该对于扩展是开放的 但是对于修改是封闭的. 通俗点 : 尽量通过扩展的方式来实现变化, 而不是通过修改已有的代码来实现.
    • 举例: 此时我们实现了一个双缓存类和单缓存类. 在图片加载类中进行这两个缓存类的实例. 并对外暴露一个布尔值让用户设置是否使用双缓存来决定内部缓存的逻辑. ok. 目前看可能没有问题. 但是如果有一个更好的缓存算法类, 这时候每次都需要在图片加载类中修改代码. 这就违反了OCP原则, 利用继承,接口的特性可以让此类问题得以解决. 比如: 我们可以定义一个缓存接口, 在加载类中使用的个这个接口中的方法. 而这个接口的具体实现通过暴露一个方法让外部调用的时候传入, 以后如果有新的缓存类只需要调用方法传入接口的子类就可以. 这样就对于原始代码修改进行了关闭, 而对于扩展是开放的.
  • 里氏替换原则LSP(Liskov Substitution Principle)

    • 定义: 所有引用基类的地方必须能透明地使用其子类. 通俗点:是基于继承,多态两大特性. 再简单点抽象
    • 举例Window#show(View view)这个方法接收一个View, 但是我们可以Button,TextView等等. 其实很简单. 我们常用只不过不知道这个名字而已. 所以LSP的原则的核心就是抽象. 抽象又依赖于继承这个特性. 通常开闭原则和里氏替换是不离不弃的例如上面OCP中举得例子. 在外部调用就是利用了继承的特性, 也就是里氏替换
  • 依赖倒置原则DIP(Dependence Inversion Principle)

    • 定义: 指代了一种特定的解耦形式, 使得高层次的模块不依赖于低层次的模块的实现细节的目的, 依赖模块被颠倒了. 通俗点: 在Java中依赖抽象(接口,抽象类), 而不依赖具体实现类. 模块之间的依赖通过抽象发生, 实现类之间不发生直接的依赖关系, 其依赖关系是通过接口或抽象类产生.
    • 举例: 还是在OCP中的例子, 内部加载类依赖于也就是成员变量是缓存接口, 而不是具体的某一个单缓存或者双缓存的实现类.
  • 接口隔离原则ISP(Interface Segregation Principles)

    • 定义: 接口的依赖关系应该建立在最小的接口上. 通俗点:接口隔离原则的目的是系统解开耦合, 从而容易重构, 更改和重新部署.
    • 举例: 在操作一些IO文件,网络的时候我们总是伴随着try...catch...finally. 在最终调用块中调用close()确保资源可以正确的释放. 但这样这样的代码不仅可读性差可以每次都是写一些冗余的模板代码. 其实可以提供一个静态方法, 而根据java中的的特性,之上操作的对象都会实现一个标识接口Closeable,这个接口标识了一个可关闭的对象有一个close(). 所以这个静态方法的形参接收一个Closeable接口,并在方法内调用close()即可. 仔细想想: 这个方法的形参在调用的时候传入的实参是里氏替换原则, 而方法内部调用的是一个接口的close()方法,但传入的可能是某一个实现类,那么这不就是依赖导致原则,并且建立在最小化的依赖基础上, 只要知道这个对象是可关闭的, 别的一概不关心, 这就是接口隔离原则.
  • 迪米特原则LOD(Law of Demeter)

    • 定义: 一个对象应该对其他对象有最少的了解. 通俗点: 一个类应该对自己需要耦合或调用的类知道的最少, 类的内部如果实现与调用者或者依赖者没有关系, 调用者或者依赖者只需要知道他需要的方法即可, 其他一概不管.
    • 举例: 房间类, 中介类, 上班族类. 可以上班族应该只关心中介类, 而不需要关注房间类. 只需要中介类返回房子的地址即可. 而不需要通过调用中介类返回一个房间类 . 这也就是代码中需要注意的. 不要过度耦合, 要降低类之间的关系.

启航之路 - UML类图说明

对于许多类组成的庞大关系网, 最好的办法是通过图来表示出其关系. 可以直观的看出组合的元素, 元素直接是如何存在的, 元素与哪些元素直接存在着联系等. 表示出来的图就是UML类图.

可以看如下一个稍微完整的一个UML类图


组成元素

  • 类和接口: 通过黄色的矩形框来表示一个类, 例如上面鸟就是一个普通类, 如果类名是斜体那么就是抽象类, 如果和飞翔或者唐老鸭的表示法那么就是接口.
  • 访问权限: 通过+ 公共权限- 私有权限# 保护权限
  • 变量和方法: 分别在第二行, 和第三行表示,抽象方法同样斜体表示, 静态属性的用下划线表示.

关系结构

  • 继承关系: 类与类之间的关系, 通过空心三角+实线表示, 通过箭头的方向指向父类表述关系.
  • 实现关系: 类与接口直接的关系, 通过空心三角+虚线表示, 通过箭头的方向指向接口表述关系.
  • 关联关系: 当一个类知道另一个类的时候,可以使用关联, 比如企鹅和气候两个类中, 企鹅类的变量有气候类的引用, 这个时候就如上图之间的关系. 实线箭头表示, 箭头指向被知道的类
  • 依赖关系: 例如动物是依赖氧气和水的, 就如动物类中的方法形参类型依赖这两个类型. 如上图动物和水之间关系. 使用虚线箭头, 箭头指向被依赖的类
  • 聚合关系: 表示一种弱拥用, A可以包含B, 但B不可以包含A. 如大雁和雁群两个类. 雁群类中会有一个数组,数组的元素是大雁类型. 这之间就是聚合. 使用空心菱形+实线箭头
  • 合成关系: 也可以认为是组合. 是一种强拥有关系. 例如鸟类和翅膀类, 鸟类是整体, 翅膀类是部分. 并且其生命周期相同, 对应着就是在鸟类初始化的时候,翅膀类也会随之初始化. 并且, 上图中的鸟到翅膀还有1..2的字样. 这称为基数. 表明一段会有几个实例, 例如一个鸟会有两个翅膀. 如果一个类有无数个实例那就用n表示. 关联关系,聚合关系也是可以有基数的. 使用实心菱形+实线箭头表示.

编程是门技术, 更加是一门艺术, 不能只满足代码结果运行正确就完事, 时常考虑如果让代码更加简练, 更加容易维护, 更易扩展和复用, 这样才可以真正提高.

## 发现之路 - 23种设计模式  ### 单例模式 Singleton  #### 模式介绍

定义: 确保某个类只有一个实例, 而且自行实例化并向整个系统提供这个实例.

场景: 确保一个类只会有一个对象实例, 避免产生多个对象消耗过多的资源, 或者某种类型的对象只应该有且只有一个. 如创建一个对象需要消耗的资源过多, 访问IO和数据库等资源时就可以考虑单例.

模式范例

单例模式的实现有5种.

饿汉式单例 -->  实现代码懒汉式单例 -->  实现代码静态内部类单例 -->  实现代码枚举单例 -->  实现代码容器实现单例 -->  实现代码 这种方式在 android源码中存在.

知识扩展

枚举实现法最大的优点就是实现简单, 但是在android却比较消耗内存. 有一点与其他单例模式不同的是: 默认枚举实例的创建是线程安全的. 为什么? 因为其他的单例在一种特定的场合下会重新创建对象,那就是反序列化.

反序列化是从磁盘读回数据并创建一个新的对象. 即使构造函数是私有的, 反序列化依然可以通过特殊的途径去创建一个实例, 相当于调用了构造函数. 反序列化提供了一个很特别的钩子函数, 类中具有一个私有的, 被实例化的方法readResolver(), 这个方法可以让开发人员控制对象的反序列化. 例如上面的几个单例模式, 如果想杜绝单例对象在被反序列化时重新生成对象, 那么必须加入如下方法:

private Object readResolve() throws ObjectStreamException(){    return sInstent;        // 返回单例中的实例对象}

这样在反序列化的时候就不是默认的重新生成一个新对象. 而对于枚举,并不存在这个问题. 因为即使反序列化它也不会重新生成新的实例.

#### Android源码对应模式

我们经常会在Activity中通过getSystemService(String name)这个函数来获取系统的服务, 比如说WMS,AMS,LayoutInflater等等. 这些服务都会在某一时刻以容器单例的形式保存在应用中.

Adapter#getView()中使用布局加载器LayoutInflate.from(context).inflate(layoutId,null)为例

会调用ContextImpl#getSystemService(String)方法获取服务, 而方法内部只是从一个SYSTEM_SERVICE_MAP名字的集合中获取了一个ServiceFetcher对象, 并从其中获取具体的服务返回.

那么我们可以缕一下应用的启动, 并定位到何时保存的这些服务到这个集合的.

  1. 首先应用的入口为ActivityThread#main(),在这个函数里面会创建ActivityThread对象, 并启动消息循环(UI)线程, 调用attach(boolean)函数
  2. attach(boolean)中通过Binder机制与ActivityManagerService通信, 最终回调本类的handlelaunchActivity()函数.
  3. 然后执行PerformLaunchActivity()函数, 开始创建Application,Context,Activity, 并把上下文关联到Activity中, 最终调用Activity#onCreate()

ok刚才大概流程是这样的, 通过之前的分析我们知道, 各个系统服务是保存在ContextImpl类中的, 这个类是在上面的第3步中被初始化的. 看如下代码, 就是服务被注册的代码, 时机也就是第一个Context被创建的时候.

class ContextImpl extends Context {    // 存储所有系统服务的集合    private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =new HashMap<String, ServiceFetcher>();        // 一个注册服务的并添加到结合的方法    private static void registerService(String serviceName, ServiceFetcher fetcher) {       if (!(fetcher instanceof StaticServiceFetcher)) {           fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;       }       SYSTEM_SERVICE_MAP.put(serviceName, fetcher);    }        // 静态语句块, 只在类第一次被加载的时候调用, 保证了服务只被添加一次.    static {        // 注册了LayoutInflate服务        registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {                public Object createService(ContextImpl ctx) {                    return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());                }});                        registerService(INPUT_SERVICE, new StaticServiceFetcher() {                public Object createStaticService() {                    return InputManager.getInstance();                }});        /**         *  后面省略一大坨的注册的服务代码        **/    }    }
### 建造者模式 Builder  #### 模式介绍

一个复杂的对象有很多组成成分, 如汽车, 车轮, 方向盘, 发动机,等等. 为了在构建过程中对外部隐藏实现细节, 就可以使用Builder模式将部件和组装过程分离, 使得构建过程和部件都可以自由扩展, 两者之间的耦合也将到了最低.

定义: 将一个复杂对象的构建与它的表示分离, 使得同样的构建过程可以创建不同的表示.

场景:

  1. 当初始化一个队形特别复杂, 参数特别多, 且有很多参数都具有默认值时.
  2. 相同的方法, 不同的执行顺序, 产生不同的事件结果时
  3. 多个部件或零件, 都可以装配到一个对象中, 但是产生的运行结果又不相同.
模式范例

范例代码

范例的UML类图

上例中通过具体MacbookBuilder类构建Macbook对象, 而Director封装了构建复杂产品对象的过程, 对外隐藏了构建的细节. BuilderDirector一起将一个复杂对象的构建与它的表示分离, 是的同样的构建过程可以创建不同的对象.

可能你会觉得唉? 怎么和我见过的Builder模式不一样呢? ,这是因为Director这个角色经常会被忽略. 而直接使用一个Builder来进行对象的封装, 并且这个Builder通常为链式调用, 它的每个setter方法都会返回this自身, 比如我们常用的AlertDialog. 下节介绍.

#### Android源码模式实现

在Android中最经典的Builder实现就是AlertDialog. 看一下开发中的使用:

// 一个粗略的创建dialog// 创建构建者builder角色AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setIcon(android.R.drawable.sym_def_app_icon)      .setTitle("标题")      .setMessage("message")      // 设置点击等..      .setPositiveButton("确定", null);// 构建AlertDialog alertDialog = builder.create();// 显示alertDialog.show();

从类名就可以看出这是一个Builder模式, 通过Builder对象来组装Dialog的各个部分. 将Dialog的构造和表示进行了分离.

接下来看一下AlertDialog的源码:

public class AlertDialog extends Dialog implements DialogInterface {    // AlertController 这个对象会保存Builder对象中的各个参数    private AlertController mAlert;        // 实际上操作的是上面这个变量中的属性    @Override    public void setTitle(CharSequence title) {        super.setTitle(title);        mAlert.setTitle(title);    }        public void setMessage(CharSequence message) {        mAlert.setMessage(message);    }    // 省略一坨代码如各种setter等    // Builder以内部类的形式存在    public static class Builder {        // 1.存储AlertDialog的各个参数 如title,icon等        private final AlertController.AlertParams P;                // 构造函数        public Builder(Context context) {            this(context, resolveDialogTheme(context, 0));        }        // 2. 设置参数, 我们构建的Builder设置的参数就是这些方法        public Builder setTitle(int titleId) {            P.mTitle = P.mContext.getText(titleId);            return this;        }                public Builder setTitle(CharSequence title) {            P.mTitle = title;            return this;        }                // ....                // 3.构建AlertDialog, 传递参数        public AlertDialog create() {            // 4.因为已经通过builder设置了参数, 接下来就可以创建真正需要的AlertDialog对象            final AlertDialog dialog = new AlertDialog(P.mContext, mTheme, false);                        // 5.将Builder类中的成员变量P应用到AlertDialog类中            P.apply(dialog.mAlert);            dialog.setCancelable(P.mCancelable);            if (P.mCancelable) {                dialog.setCanceledOnTouchOutside(true);            }            dialog.setOnCancelListener(P.mOnCancelListener);            dialog.setOnDismissListener(P.mOnDismissListener);            if (P.mOnKeyListener != null) {                dialog.setOnKeyListener(P.mOnKeyListener);            }            return dialog;        }          }   }

对, 最后还调用了AlertDialog#show()函数, 这个函数主要做了如下几件事情:

  1. 通过dispatchOnCreate()函数来调用AlertDialog#onCreate()函数
  2. 然后调用AlertDialog#onStart()函数
  3. 最后将DialogDecorView添加到WindowManager中.

那么在看一下onCreate()函数的源码及后续调用.

// AlertDialog类protected void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);   mAlert.installContent();}// AlertController类public void installContent() {   // 设置窗口, 没有title类型   mWindow.requestFeature(Window.FEATURE_NO_TITLE);   int contentView = selectContentView();   // 设置窗口的内容视图   mWindow.setContentView(contentView);   // 初始化AlertDialog其他子视图的内容   setupView();   setupDecor();}

这部分比较重要, 通过Window#setContentView()和Activity是一样的过程, 设置了内容布局, 通过AlertController的构造函数可以发现加载布局资源就是com.android.internal.R.layout.alert_dialog这个文件, 之前的Builder中的各种setter方法就是把设置的内容传入到这个布局当中.


可以看到Android源码中的AlertDialog并没有遵循GOF设计模式中经典的实现方式, 而是进行了变种, 但却使其使用更加的方便. 这里AlertDialog.Builder这个类同时扮演了范例中的builder,具体实现builder,Director的角色. 简化了Builder设计模式, 因为模块比较稳定不会存在变化, 根据具体场景简化模式, 正是体现了灵活运用设计模式的实例.

#### 实战场景

就如Picasso,Glide等链式的调用, 你可以通过链式设置很多配置属性, 也可以仅调用两三此传入必要参数即可. 是的调用实现更加灵活.

### 原型模式 Prototype  #### 模式介绍

创建性模式, 从一个样板对象中复制出一个内部属性一致的对象, 其实就是克隆. 而被复制的对象就叫做原型, 多用于创建复杂的或者构造耗时的实例

  • 定义: 用原型实例指定创建对象的种类, 并通过拷贝这些原型创建新的对象.
  • 场景:
    1. 类初始化需要消耗非常多的资源, 这个资源包括数据,硬件资源等, 可通过原型拷贝避免这些消耗
    2. 通过new产生一个对象需要非常繁琐的数据准备或访问权限, 同样可以使用原型模式
    3. 一个对象需要提供给其他对象访问, 并且会能会对其修改属性, 可以用原型拷贝多个对象提供使用

其实这个模式很简单, 就是利用Object#clone()方法可以复制一份提供使用(clone是一个native方法). 但是需要注意, 通过实现Cloneable接口的原型模式在调用clone函数构造并不一定就比通过new方式的快, 只有当通过new构造对象较为耗时或者说成本较高时, 通过clone方法才能获得效率提升.

UML类图

#### 模式范例

这里模式实现很简单, 实现也比较少, 这里就贴出代码

public class WordDocument implements Cloneable{    // 文本    public String mText;    // 图片名列表    public ArrayList<String> mImages = new ArrayList<String>();    public WordDocument(){        System.out.println("-----------WordDocument构造函数-----------");    }    @Override    protected WordDocument clone() {        try {            // 通过本地方法特殊途径, 构建一个对象            WordDocument doc = (WordDocument) super.clone();            doc.mText = this.mText;            // 因为Image是引用类型, 这样直接赋值属于浅拷贝, 再次对集合进行clone. 实现wordDocument的深拷贝            doc.mImages = (ArrayList<String>) this.mImages.clone();            return doc;        }catch (Exception ex){}        return null;    }    /**     * 打印文档内容     */    public void showDocument(){        System.out.println("------------开始输出内容---------------------");        System.out.println("Text: "+mText);        System.out.println("List: "+mImages.toString());        System.out.println("------------输出结束------------------------");    }}

与标准的原型模式相比WordDocument就是一个具体实现的原型对象. 而实现的Cloneable接口为抽象的原型对象.

其实Cloneable这个接口内部没有任何方法, 所以其本质就是标识接口,只是表明这个类的对象是可拷贝的, 而clone()这个方法是Objec类中的, 如果没有标识这个接口, 那么调用会抛出异常.


深拷贝浅拷贝

例如上面的代码中进行修改一下

@Overrideprotected WordDocument clone() {   try {       // 通过本地方法特殊途径, 构建一个对象       WordDocument doc = (WordDocument) super.clone();       doc.mText = this.mText;       // 这里进行修改 那么此时属于浅拷贝       doc.mImages = this.mImages;       return doc;}

你可能应该发现了什么, 其实本质不过就是通过super.clone()构建了一个本类对象的初始状态, 然后把被拷贝的对象的各个属性值进行赋值操作而已.

的确, 就是如此. 就如上面两处不同的代码,

  • 浅拷贝: 也称影子拷贝, 拷贝出来的对象并不是完全一份独立的对象, 新的对象某些属性如引用传递可能会引用原始对象的对应属性值, 也就是说, 对浅拷贝的属性可能会影响到原始数据的属性.
  • 深拷贝: 拷贝出一份原始对象, 并对原始对象的属性值, 进行复制添加到新拷贝的对象的各个属性上. 这样拷贝出来的对象与原始对象不存在任何关联, 只作为一个数据的副本存在.

上面因为mImages的类型是ArrayList如果直接进行赋值那么属于引用传递, 共享的一份数据源, 而如果在对ArrayList进行一次clone, 那么相当于又构建了一个集合并进行数据的复制.

mText虽然是对象, 但是因为是String类型, 属于安全类型, 由于final类,实例不可更改的特性. 如果对副本进行字符串的修改, 只不过是把原引用删除,重新指向了新的字符串.

#### Android源码对应实现

上面我们说了通过对集合再次调用clone()即可完成深拷贝. 那么看一下ArrayList源码

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{    transient Object[] elementData;     private int size;        public Object clone() {        try {            ArrayList<?> v = (ArrayList<?>) super.clone();            v.elementData = Arrays.copyOf(elementData, size);            v.modCount = 0;            return v;        } catch (CloneNotSupportedException e) {            throw new InternalError(e);        }    }

ArrayList的内部克隆实现很简单, 我们都知道ArrayList内部是通过数组的数据结构来实现的. 通过Arrays工具类对原始集合的数据进行赋值并添加到一个新的数组并返回, 而返回的数组挂到了克隆出来对象上的elementData变量上.

而集合的大小size没有被进行赋值? 因为其类型是整型, 属于值传递, 在clone之后原始值通过值传递到了新对象中, 即使修改也不会对原始对象有任何的影响.


那么Android源码中的实现是什么?

Intent, 我们看如下代码

Intent intent = new Intent("某一个activity的action");intent.putExtra("result", "Successful");// 调用克隆方法Intent clone = (Intent) intent.clone();startActivity(clone);

这样同样没问题, 一样的效果. 那么看一下Intent#clone()内部是如何实现的.

@Overridepublic Object clone() {   return new Intent(this);}/*** Copy constructor.*/public Intent(Intent o) {   this.mAction = o.mAction;   this.mData = o.mData;   this.mType = o.mType;   this.mPackage = o.mPackage;   this.mComponent = o.mComponent;   this.mFlags = o.mFlags;   this.mContentUserHint = o.mContentUserHint;   if (o.mCategories != null) {       this.mCategories = new ArraySet<String>(o.mCategories);   }   if (o.mExtras != null) {       this.mExtras = new Bundle(o.mExtras);   }   if (o.mSourceBounds != null) {       this.mSourceBounds = new Rect(o.mSourceBounds);   }   if (o.mSelector != null) {       this.mSelector = new Intent(o.mSelector);   }   if (o.mClipData != null) {       this.mClipData = new ClipData(o.mClipData);   }}

很简单不需要解释了, 手动new的并进行数据复制. 相当于封装了一下复制的细节而已.

但是为什么没有调用super.clone()来实现拷贝呢? 之前说过使用clone还是new关键字是需要根据构造对象的成本来决定的, 如果对象的构造成本比较复杂或者麻烦, 那么clone则是一种更优的选择, 否则就可以使用new的形式. 这和c++拷贝构造函数是一样的.

#### 实战场景

当登录模块登录成功之后, 会把一些个人信息,token等信息在保存类中的某个数据结构上, 并通过一个方法对外暴露出去, 提供其他模块使用. 但是如果你返回的是一个数据结构也就是一个对象, 这个对象包含了很多个人信息, 但是正常来说, 对于外部应该只提供查看数据的能力, 不应该提供修改的能力.

所以这个使用, 就可以对登录模块对外暴露的方法进行修改, 利用原型模式对外返回的是一个内部数据的深拷贝, 这样就把可能出现的隐患彻底的隔绝了.

说明

原型模式是通过内存中二进制流的方式拷贝, 要比直接通过new一个对象性能更好, 特别是循环体内产生大量对象是. 但是注意, 因为是二进制流的拷贝, 所以构造函数是不会执行的. 这点要明确记牢.

### 工厂方法模式 Factory  #### 模式介绍

创建型设计模式, 其实这个模式可能在开发中出现很多回了, 只是并不了解什么是工厂模式的概念.

  • 定义: 定义一个用于创建的对象的接口, 让子类决定实例化哪个类
  • 场景: 在任何需要生成复杂对象的地方, 都可以使用工厂方法模式. 复杂对象适合使用工厂模式, 用new就可以完成创建的对象无需使用工厂模式.

工厂方法模式完全符合设计模式原则, 降低了对象之间的耦合度, 而且, 工厂方法模式依赖于抽象的架构, 将实例化的任务交由了子类实现.

#### 模式范例

实现代码

这是范例的UML类图.

其实这里, 可以去掉抽象的工厂类, 只需要一个工厂即可. 这样会更加简洁直观.

#### Android源码对应实现

ListSet不陌生, 都继承Collection接口, 而Collection接口继承Iterable接口, 而这个接口很简单就一个iterator()方法, 如下

public interface Collection<E> extends Iterable<E> {    // ....}public interface Iterable<T> {    Iterator<T> iterator();        // 可能JDK1.8之后添加两个默认方法, 这里我们不需要关心}

关于ListSet迭代器的方法遍历元素应该都用过. 那么看一下源码实现.

public class ArrayList<E> extends AbstractList<E> implements Cloneable, Serializable, RandomAccess {@Override public Iterator<E> iterator() {        return new ArrayListIterator();    }    private class ArrayListIterator implements Iterator<E> {        private int remaining = size;                private int removalIndex = -1;        private int expectedModCount = modCount;        public boolean hasNext() {            return remaining != 0;        }        @SuppressWarnings("unchecked") public E next() {            ArrayList<E> ourList = ArrayList.this;            // 返回集合大小元素, 还有几个未遍历            int rem = remaining;            if (ourList.modCount != expectedModCount) {                throw new ConcurrentModificationException();            }            if (rem == 0) {                throw new NoSuchElementException();            }            remaining = rem - 1;            return (E) ourList.array[removalIndex = ourList.size - rem];        }        public void remove() {            Object[] a = array;            int removalIdx = removalIndex;            if (modCount != expectedModCount) {                throw new ConcurrentModificationException();            }            if (removalIdx < 0) {                throw new IllegalStateException();            }            System.arraycopy(a, removalIdx + 1, a, removalIdx, remaining);            a[--size] = null;  // Prevent memory leak            removalIndex = -1;            expectedModCount = ++modCount;        }    }}
// hashSet 复写逻辑public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable,Serializable {    public Iterator<E> iterator() {        return backingMap.keySet().iterator();    }}
// HashMap 复写逻辑public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {    Iterator<K> newKeyIterator() { return new KeyIterator();   }        private final class KeyIterator extends HashIterator            implements Iterator<K> {        public K next() { return nextEntry().key; }    }}
  • HashSetiterator方法会返回成员变量backingMap中对应HashSet对象元素的迭代器对象, 最终返回的是KeySet中的一个迭代器对象
  • ArrayListHashMap中的iterator()就相当一个工厂方法, 专为new对象而生!

Android中, 看一下如下代码

public class MainActivity extends Activity {    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(new FrameLayout(this));       }}

通过onCreate()这个方法, 我们可以构建出任何样式的根布局, 如LinearLayout,TextView等等. 我们在不同的Activity#onCreate()方法将设置的布局通过setContentView()函数传递给frameworks并显示出来. 这不就是一个工厂模式的结构. 方法内部可以创建不同的对象, 产生不同的实例.

#### 实战场景

例如对数据的持久化, 可以通过的途径有SP,File,SQLite等. 但是对数据的操作无非就是增删改查, 那么我们可以抽象一个抽象类并定义CURD抽象方法. SPFile,SQLite分别继承抽象类, 并在抽象方法实现自己的处理逻辑. 然后就可以创建一个工厂类, 工厂类有一个方法, 形参为产品实现类的字节码, 返回一个泛型上限限定是产品的抽象类对象, 方法内部通过字节码反射具体的产品类实例.

这样在使用的使用, 我们只需有通过工厂方法传入的不同产品Class就可以构建不同的实例, 而数据的CRUD通过依赖倒置抽象特性, 高层不需要依赖底层的类.

### 抽象工厂模式 Abstract Factory  #### 模式介绍

创建型设计模式, 之前工厂模式会生产某一个产品, 但是如果说, 不同的操作系统图形的场景下的两个产品按钮和文本框. 对于每一个操作系统, 其本身就构成了一个单独的产品. 两种产品两种变化, 这种情况就较之前的普通工厂升级了复杂度, 如: Android中的ButtonTextViewiOS中的ButtonTextView或者WindowPhone场景...

定义: 为创建一组相关或者是相互依赖的的对象提供一个接口, 而不需要指定他们的具体类

场景: 一个对象族有相同的约束时可以使用抽象工厂, 如androidiOS都有打电话软件和短信软件, 两者都属于软件的范畴, 但是他们的操作平台不同, 实现逻辑也不会相同. 这个时候就可以使用抽象工厂方法模式

模式范例

实现代码

范例UML图

看一下运行结果:

如果这是时候, 如果想创建一种使用普通轮胎新款发动机的车型. 只需要继承抽象工厂, 并使用原有的普通轮胎类, 并继承IEngfine实现一款新的发动机类. 即可完成扩展. 这就是通过接口扩展.

上面的范例, 对于每一个造车的工厂, 内部使用的零件不管哪个车场都是具有抽象的轮胎和发送机类. 这样可以达到一种自由组合的状态.

但是弊端也显示出来了, 不仅需要扩展新的工厂类还要扩展新的组件类.

#### Android源码对应实现

抽象工厂在Android实现较少, 上一节说onCreate()方法就相当于一个工厂方法. 那么对于另外一个组件Service#onBind()同样也可以看做一个工厂方法.

如果从frameworks层的角度来看ActivityService可以看做一个具体的工厂, 这样来看相当于一个抽象方法模式的雏形也没错.

另一个更像的例子是Android底层对MediaPlayer使用. 这里书上噼里啪啦一堆C语言. 我就不抄了....

### 策略模式 Strategy  #### 模式介绍

开发中可能有这样的情况: 实现某一个功能可以有多中算法或者策略, 我们根据不同的功能来选择不同的算法. 针对这种情况, 1.可以在一个类中封装多个方法, 每个方法实现不同算法. 2.通过if..else if..else..条件判断来决定使用哪种算法. 但是这两种都是硬编码实现. 并且随着算法的增多类也就变得臃肿, 维护的成本随之变高. 如果需要增加一种新的算法, 必然需要对算法类进行修改. 这就违反了OCP原则和单一职责的原则.

定义: 策略模式定义了一系列的算法, 并将每一个算法封装起来, 而且使它们还可以相互替换. 策略模式让算法独立于使用它的客户而独立变化.

场景:

  • 针对同一类型问题的多种处理方式, 仅仅是具体行为有差别时
  • 需要安全地封装多种同一类型的操作时
  • 出现同一抽象类有多个子类, 而又不需要使用if-else或者switch等来选择具体子类.
模式范例

最方便的记忆法就是记住, 策略模式可以去掉if-else或者switch. 语句, 即使后续会扩展通过接口来进行扩展, 不会对源代码进行修改. 满足了OCP开闭原则.

看一下范例代码: --> 对于交通费用的计算, 计算的算法可能会有公交, 地铁等...

代码地址

范例类图:

在看一下代码的使用以及结果-->

public static void main (String arg[]){   // 创建操作策略的环境类   TranficCalculator calculator = new TranficCalculator();   // 设置公交车的策略, 并准备计算   calculator.setStrategy(new BusStrategy());   System.out.println("公交车-->计算9公里价格: "+calculator.calculatePrice(9));   // 设置地铁的策略, 并准备计算   calculator.setStrategy(new SubwayStrategy());   System.out.println("地铁-->计算9公里价格: "+calculator.calculatePrice(9));}// 结果-->公交车-->计算9公里价格: 1地铁-->计算9公里价格: 4

你应该可以发现, 这种方式在隐藏实现的同时, 可扩展性变得很强, 如果此时需要增加一个出租车的计算策略, 那么只需要添加一个实现了计算策略接口即可. 对原始代码的修改进行了关闭, 并对扩展开放.

#### Android源码对应实现

动画里面的插值器Interpolator利用了策略模式, 利用Interpolator策略的抽象, LinearInterpolator,CycleInterpolator等插值器为具体的实现策略, 通过注入不同的插值器实现不同的动态效果.

看一下大概的类图

  • 动画中的TimeInterpolator时间插值器, 它的作用是根据时间流逝的百分比计算出当前属性值改变的百分比, 内置的插值器有如下几种
    • 线性插值器(LinearInterpolator)用于匀速动画
    • 加速减速插值器(AccelerateDecelerateInterpolator):起始时动画加速, 结尾时动画减速
    • 减速插值器(DecelerateInterpolator): 用于随着时间的推移动画越来越慢.
  • 动画中的TypeEvalutor类型估值器: 根据当前属性改变的百分比来计算改变后的属性值. 内置的类型估值器有如下几种
    • 整型估值器(IntEvalutor)
    • 浮点型估值器(FloatEvalutor)
    • Color估值器(ArgbEvalutor)

接下来就开始回忆一下从一个动画开始后, 代码究竟做了什么?

对于源码的起始点入口就是调用View的startAnimation()

public void startAnimation(Animation animation) {    // 1.初始化动画的开始时间   animation.setStartTime(Animation.START_ON_FIRST_FRAME);   // 2.对View设置动画   setAnimation(animation);   // 3.刷新父类缓存   invalidateParentCaches();   // 4.刷新View本身及子View   invalidate(true);}

这里首先设置了动画的起始时间, 然后将该动画设置到View中, 最后再向ViewGroup请求刷新视图, 随后ViewGroup会调用dispatchDraw()方法对这个View所在的区域进行重绘. 其实对于某一个View的重绘最终是调用其ViewGroupdrawChild(...)方法. 跟入一下

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {   // 简单的转发   return child.draw(canvas, this, drawingTime);}boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {    // ....    // 查看是否需要清除动画信息    final int flags = parent.mGroupFlags;    // 省略无关代码        // 获取设置的动画信息    final Animation a = getAnimation();            if (a != null) {            // 绘制动画           more = drawAnimation(parent, drawingTime, a, scalingRequired);           //...       } }

父类会调用子类的draw方法, 其中会先判断是否设置了清除动画的标记, 然后再获取该View动画信息, 如果设置了动画, 就会调用View#drawAnimation()方法.

private boolean drawAnimation(ViewGroup parent, long drawingTime,       Animation a, boolean scalingRequired) {   Transformation invalidationTransform;   final int flags = parent.mGroupFlags;   final boolean initialized = a.isInitialized();   // 1. 判断动画是否已经初始化过   if (!initialized) {       a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());       a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);       if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);       // 如果设置了动画的监听, 则触发对应的回调       onAnimationStart();   }   // 获取Transformation对象, 存储动画的信息   final Transformation t = parent.getChildTransformation();   // 2. 调用Animation#getTransformation, 通过计算获取动画的相关值   boolean more = a.getTransformation(drawingTime, t, 1f);       if (more) {        // 3. 根据具体实现, 判断当前动画类型是否需要进行调整位置大小, 然后刷新不同的区域       if (!a.willChangeBounds()) {           // ...       } else {           // 获取重绘区域           a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,                   invalidationTransform);           parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;            // 更新计算有效区域           final int left = mLeft + (int) region.left;           final int top = mTop + (int) region.top;                      // 进行区域更新           parent.invalidate(left, top, left + (int) (region.width() + .5f),                   top + (int) (region.height() + .5f));       }   }   return more;}

drawAnimation中主要操作是动画的初始化, 动画操作, 界面刷新. 动画的回调监听onStart()会在动画进行初始化的时候调用, 动画的具体实现是通过Animation#getTransformation()方法.这个方法主要获取了缩放系数和调用Animation.getTransformation(long, Transformation)来计算和应用动画效果.

public boolean getTransformation(long currentTime, Transformation outTransformation) {   //...   float normalizedTime;   // 1.计算当前时间的流逝百分比   if (duration != 0) {       normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /               (float) duration;   } else {       // time is a step-change with a zero duration       normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;   }   // 动画是否完成标记   final boolean expired = normalizedTime >= 1.0f;   mMore = !expired;   if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {        // 2.通过插值器获取动画执行百分比  , 这里获取的方法就是通过策略模式       final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);       // 3.应用动画效果       applyTransformation(interpolatedTime, outTransformation);   }   // 4. 如果动画执行完毕, 那么触发动画完成的回调或者执行重复动画等操作   // ...   if (!mMore && mOneMoreTime) {       mOneMoreTime = false;       return true;   }   return mMore;}

这段代码, 先计算已经流逝的的时间百分比, 然后再通过具体的插值器重新计算这个百分比, 也就是上面的第二步. 而具体是哪一个插值器是通过之前说的策略模式来实现的.

第3步调用了applyTransformation, 这个方法在基类Animation中是空实现, 可以在子类查看实现如ScaleAnimation,AlphaAnimation等查看. 当这个方法内部主要通过矩阵来实现动画. 当这个方法执行完毕之后, View的属性也就发生了变化, 不断地重复这个过程, 动画就随之产生.

#### 实战场景

当我们自己组装了一个队列请求, 对于这个队列的处理方式默认可能是先处理进入队列的, 但是如果想实现一个可以先处理后入队的, 和随机读取队列元素. 那么为了以后的扩展不影响源代码, 那么可以通过策略模式在代码中通过对策略抽象面向接口,抽象编程. 是具体的实现有后续的传入的子类来决定.

未完待续...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值