设计模式(五)观察者模式

本文首发于微信公众号「刘望舒」

相关文章
设计模式系列

1.观察者模式模式简介

定义

观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

观察者模式结构图

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcrereObserver:具体观察者,是实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

2.观察者模式简单实现

观察者模式这种发布-订阅的形式我们可以拿微信公众号来举例,假设微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号,当这个公众号更新时就会通知这些订阅的微信用户。好了我们来看看用代码如何实现:

抽象观察者(Observer)

里面定义了一个更新的方法:

public interface Observer {
    public void update(String message);
}

具体观察者(ConcrereObserver)

微信用户是观察者,里面实现了更新的方法:

public class WeixinUser implements Observer {
    // 微信用户名
    private String name;
    public WeixinUser(String name) {
        this.name = name;
    }
    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
    }

   
}

抽象被观察者(Subject)

抽象主题,提供了attach、detach、notify三个方法:

public interface Subject {
    /**
     * 增加订阅者
     * @param observer
     */
    public void attach(Observer observer);
    /**
     * 删除订阅者
     * @param observer
     */
    public void detach(Observer observer);
    /**
     * 通知订阅者更新消息
     */
    public void notify(String message);
}

具体被观察者(ConcreteSubject)

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:

public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();

    @Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
    }

    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
        }
    }
}

客户端调用

public class Client {
    public static void main(String[] args) {
        SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
        //创建微信用户
        WeixinUser user1=new WeixinUser("杨影枫");
        WeixinUser user2=new WeixinUser("月眉儿");
        WeixinUser user3=new WeixinUser("紫轩");
        //订阅公众号
        mSubscriptionSubject.attach(user1);
        mSubscriptionSubject.attach(user2);
        mSubscriptionSubject.attach(user3);
        //公众号更新发出消息给订阅的微信用户
        mSubscriptionSubject.notify("刘望舒的专栏更新了");
    }
}

结果

杨影枫-刘望舒的专栏更新了
月眉儿-刘望舒的专栏更新了
紫轩-刘望舒的专栏更新了

3.使用观察者模式的场景和优缺点

使用场景

  • 关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。

优点

解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。

缺点

在应用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。

4.Android中的观察者模式

android源码中也有很多使用了观察者模式,比如OnClickListener、ContentObserver、android.database.Observable等;还有组件通讯库RxJava、RxAndroid、EventBus;在这里将拿我们最常用的Adapter的notifyDataSetChanged()方法来举例:
当我们用ListView的时候,数据发生变化的时候我们都会调用Adapter的notifyDataSetChanged()方法,这个方法定义在BaseAdaper中,我们来看看BaseAdaper的部分源码:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {

    //数据集观察者
    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public boolean hasStableIds() {
        return false;
    }

    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

    /**
     * 当数据集变化时,通知所有观察者
     */
    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

很明显BaseAdapter用的是观察者模式,BaseAdapter是具体被观察者,接下来看看mDataSetObservable.notifyChanged():

public class DataSetObservable extends Observable<DataSetObserver> {
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

我们看到了mObservers,这就是观察者的集合,这些观察者是在ListView通过setAdaper()设置Adaper时产生的:

@Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }
        ...
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
            checkFocus();
            //创建数据观察者
            mDataSetObserver = new AdapterDataSetObserver();
            //注册观察者
            mAdapter.registerDataSetObserver(mDataSetObserver);

            ...
        }

接下来看看观察者AdapterDataSetObserver中处理了什么:

class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroller != null) {
                mFastScroller.onSectionsChanged();
            }
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            if (mFastScroller != null) {
                mFastScroller.onSectionsChanged();
            }
        }
    }

从上面的代码看不出什么,再看看AdapterDataSetObserver的父类AdapterView的AdapterDataSetObserver:

class AdapterDataSetObserver extends DataSetObserver {
        private Parcelable mInstanceState = null;
        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            //重新布局
            requestLayout();
        }

        ...

        public void clearSavedState() {
            mInstanceState = null;
        }
    }

我们看到在onChanged()方法中调用了requestLayout()方法来重新进行布局。好了,看到这里我们都明白了,当ListView的数据发生变化时,我们调用Adapter的notifyDataSetChanged()方法,这个方法又会调用观察者们(AdapterDataSetObserver)的onChanged()方法,onChanged()方法又会调用requestLayout()方法来重新进行布局。

分享到 评论

Android View体系(三)属性动画

本文首发于微信公众号「刘望舒」

前言

上一篇文章讲了View滑动的六种方法,其中一种是使用动画,这篇文章我们来讲一讲动画的其中一种:属性动画。

查看更多

分享到 评论

Android View体系(二)实现View滑动的六种方法

本文首发于微信公众号「刘望舒」

1.View的滑动简介

View的滑动是Android实现自定义控件的基础,同时在开发中我们也难免会遇到View的滑动的处理。其实不管是那种滑动的方式基本思想都是类似的:当触摸事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后的触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。
实现View滑动有很多种方法,这篇文章主要讲解六种滑动的方法,分别是:layout()、offsetLeftAndRight()与offsetTopAndBottom()、LayoutParams、动画、scollTo与scollBy和Scroller;在下一篇文章我们会详细介绍属性动画。

2.实现View滑动的六种方法

layout()

view进行绘制的时候会调用onLayout()方法来设置显示的位置,因此我们同样也可以通过修改View的left、top、right、bottom这四种属性来控制View的坐标。首先我们要自定义一个View,在onTouchEvent()方法中获取触摸点的坐标:

   public boolean onTouchEvent(MotionEvent event) {
        //获取到手指处的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;

...

接下来我们在ACTION_MOVE事件中计算偏移量,再调用layout()方法重新放置这个自定义View的位置就好了:

case MotionEvent.ACTION_MOVE:
    //计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    //调用layout方法来重新放置它的位置
    layout(getLeft()+offsetX, getTop()+offsetY,
            getRight()+offsetX , getBottom()+offsetY);
    break;

当我们每次移动时都会调用layout()方法来对自己重新布局,从而达到移动View的效果。

自定义View的全部代码(CustomView.java):

package com.example.liuwangshu.moonviewslide;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class CustomView extends View {
    private int lastX;
    private int lastY;

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomView(Context context) {
        super(context);
    }

    public boolean onTouchEvent(MotionEvent event) {
        //获取到手指处的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;

            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法来重新放置它的位置
                layout(getLeft()+offsetX, getTop()+offsetY,
                        getRight()+offsetX , getBottom()+offsetY);
                break;
        }

        return true;
    }
}

布局中引用自定义View:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.liuwangshu.moonviewslide.CustomView
        android:id="@+id/customview"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="50dp"
        android:background="@android:color/holo_red_light" />
</LinearLayout>

offsetLeftAndRight()与offsetTopAndBottom()

这两种方法和layout()方法效果方法差不多,使用也差不多,我们将ACTION_MOVE中的代码替换成如下代码:

case MotionEvent.ACTION_MOVE:
    //计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    //对left和right进行偏移
    offsetLeftAndRight(offsetX);
    //对top和bottom进行偏移
    offsetTopAndBottom(offsetY);
    break;

LayoutParams(改变布局参数)

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。同样的我们将ACTION_MOVE中的代码替换成如下代码:

LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
              layoutParams.leftMargin = getLeft() + offsetX;
              layoutParams.topMargin = getTop() + offsetY;
              setLayoutParams(layoutParams);

因为父控件是LinearLayout,所以我们用了LinearLayout.LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

动画

可以采用View动画来移动,在res目录新建anim文件夹并创建translate.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="300" android:duration="1000"/>
</set>

在Java代码中引用:

mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));

当然使用属性动画移动那就更简单了,我们让CustomView在1000毫秒内沿着X轴像右平移300像素:

ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();

scollTo与scollBy

scollTo(x,y)表示移动到一个具体的坐标点,而scollBy(dx,dy)则表示移动的增量为dx、dy。其中scollBy最终也是要调用scollTo的。scollTo、scollBy移动的是View的内容,如果在ViewGroup中使用则是移动他所有的子View。我们将ACTION_MOVE中的代码替换成如下代码:

((View)getParent()).scrollBy(-offsetX,-offsetY);

这里要实现CustomView随着我们手指移动的效果的话,我们就需要将偏移量设置为负值。

Scroller

我们用scollTo/scollBy方法来进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用Scroller来实现有过度效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔完成的。Scroller本身是不能实现View的滑动的,它需要配合View的computeScroll()方法才能弹性滑动的效果。
在这里我们实现CustomView平滑的向右移动。

  • 首先我们要初始化Scroller:
public CustomView(Context context, AttributeSet attrs) {
      super(context, attrs);
      mScroller = new Scroller(context);
  }
  • 接下来重写computeScroll()方法,系统会在绘制View的时候在draw()方法中调用该方法,这个方法中我们调用父类的scrollTo()方法并通过Scroller来不断获取当前的滚动值,每滑动一小段距离我们就调用invalidate()方法不断的进行重绘,重绘就会调用computeScroll()方法,这样我们就通过不断的移动一个小的距离并连贯起来就实现了平滑移动的效果:
@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
         //通过不断的重绘不断的调用computeScroll方法
         invalidate();
    }  
}
  • 调用Scroller.startScroll()方法。我们在CustomView中写一个smoothScrollTo()方法,调用Scroller.startScroll()方法,在2000毫秒内沿X轴平移delta像素:
public void smoothScrollTo(int destX,int destY){
      int scrollX=getScrollX();
      int delta=destX-scrollX;
      //1000秒内滑向destX
      mScroller.startScroll(scrollX,0,delta,0,2000);
      invalidate();
  }
  • 最后我们在ViewSlideActivity.java中调用CustomView的smoothScrollTo()方法:
//使用Scroll来进行平滑移动
mCustomView.smoothScrollTo(-400,0);

这里我们是设定CustomView沿着X轴向右平移400像素。

github源码下载

分享到 评论

Android View体系(一)视图坐标系

本文首发于微信公众号「刘望舒」

前言

Android View体系是界面编程的核心,他的重要性不亚于Android四大组件,在这个系列中我会陆续讲到View坐标系、View的滑动、View的事件分发等文章来逐步介绍Android View体系。

查看更多

分享到 评论

Android IPC机制(五)用Socket实现跨进程聊天程序

本文首发于微信公众号「刘望舒」

查看更多

分享到 评论

设计模式(四)简单工厂模式

本文首发于微信公众号「刘望舒」

相关文章
设计模式系列

1.简单工厂模式简介

定义

简单工厂模式属于创建型模式又叫做静态工厂方法模式,是由一个工厂对象决定创建出哪一种产品类的实例。

查看更多

分享到 评论

设计模式(三)建造者模式

本文首发于微信公众号「刘望舒」

相关文章
设计模式系列

1.建造者模式简介

定义

建造者模式(builder),将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

查看更多

分享到 评论

Swift快速入门(四)流程控制

相关文章
Swift快速入门系列

1.分支结构

Swift提供两种常见的分支控制结构:if语句和swich语句。一般来说,当条件简单且可能情况较少时使用if语句;当条件比较复杂情况较多时则可以考虑使用swich语句。

查看更多

分享到 评论

Swift快速入门(三)运算符

相关文章
Swift快速入门系列

1.赋值运算符

赋值运算(a = b),表示用b的值来初始化或更新a的值:

查看更多

分享到 评论

Swift快速入门(二)基本数据类型

相关文章
Swift快速入门系列

1.变量和常量

声明常量和变量

Swfit是强类型的语言,Swift要求所有的变量和常量必须先声明后使用。
声明变量需要使用var,声明常量则需要使用let

查看更多

分享到 评论

Android IPC机制(四)用ContentProvider进行进程间通信

本文首发于微信公众号「刘望舒」

查看更多

分享到 评论

关于App程序员泡沫

QQ20160426-18 (1)_副本.png

前言

做开发快七年了,对于程序员,外行人总有着数不完的讽刺和误解,但是我都懒得去解释,代码搬运工人也好,民工也罢,随他们去说吧。但是网上最近流传的程序员泡沫,尤其是APP程序员泡沫的文章导致很多我们的年轻的同行产生了疑惑,所以我这个隐藏了很久的能言善辩的老程序员出山来聊一聊这个APP程序员泡沫的话题。
笔者是2010年从事安卓开发,此前做J2ee,对于安卓我有很深的感情,此前也是有意学了iOS,但是还是决定在安卓这条路上一直走到黑,在2010年一个好的安卓开发苗子工资可以过万,工作经验也就1年那样子,基本上你会点安卓都可以接近1W。想想最近某些文章中提到现在安卓开发新手动不动就要过万的工资相比,我觉得现在的新手做法并不为过:第一,以现在的北京物价房价对比2010年来说,开发的工资其实并没有涨反倒是跌了。第二,现在的开发比2010年的新手安卓开发要厉害一些,那个时候网上资料很少,书也很少,大多数安卓开发自学起来很痛苦。现在网上资料多,也有很多高水品的技术书,也有很完善的培训机制。
当然现在很多APP开发存在漫天要价的现象,但是作为企业的HR,技术经理甚至老板你可以选择不要他啊。这篇文章只讨论一般的APP开发,脑残的APP开发不在此文范畴。

查看更多

分享到 评论

Swift快速入门(一)第一个Swift程序

相关文章
Swift快速入门系列

1. 本系列说明

本系列只是一个Swift快速入门的教程,并没有详尽的介绍Swift,Swift也并不是一个简单的编程语言,所以要想详尽的系统的学习Swift,本系列并不适合你,此系列只是让开发者可以快速的用Swift来进行开发。另外学习本系列并不需要obj-c的知识,但是如果你想开发iOS,obj-c是必须要学的,因为Swift并不能很快的替代obj-c。另外本系列基于OS X EI Captitan 10.11.2,Xcode7.2。

2. Swift介绍

Swift,苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于Mac OS和iOS平台,用于搭建基于苹果平台的应用程序。Swift吸收了众多现代编程语言的优点,尽力的提供简洁的编程语言和强大的功能。

3. 第一个Swift程序

Swift源文件的第一行可执行的代码就是Swift的程序的入口:

print("Hello World")

Swift程序的执行语句可以无须任何符号作为结束,Swift把每行代码作为了一个语句。但是如果在一行写了多个Swift语句则应该用”;”来作为语句的结束符。

print("Hello");print("World")

笔者还是建议用”;”来作为语句的结束符,这样可读性更强。

3. Playground工具介绍

Playground是一个简单的测试环境,主要是用于快速测试Swift语法功能和验证API功能,并不是用来进行实际开发应用。如果开发者对Swift语法功能不太确定,则可以用Playground来测试代码,其次Playground也可以用来验证某个函数,类的功能。

首先我们启动Xcode,选中Playground
这里写图片描述

接着在Name文本框输入Playground的名字

这里写图片描述

Playground保存成功后就会看到下面的编辑界面,左边是编辑界面,当开发者在左边编写代码,定义变量和常量之后,即可在右边是实时的看到变量或常量的值。我们将上面将的代码写进去就会看到,右边显示”Hello World”说明我们的代码没有问题。下面是运行按钮,点击运行按钮,控制台输出Hello World,第一个Swift程序就完成了。
这里写图片描述

4. 用swiftc编译Swift程序

swiftc的命令基本格式是

swiftc -o <生成文件> <源程序>

我们用文本编辑工具(我用的是Notepad+)在里面写上print(“Hello World”),保存在桌面取名为hello.swift
打开终端程序,进入桌面目录,输入如下命令:

swiftc -o hello.out hello.swift

我们会发现桌面生成了hello.out文件,接下来我们执行命令

./hello.out

这个命令会执行当前目录下的hello.out程序,执行该程序会看到输出Hello World

上述的编译,运行完整过程:
这里写图片描述

本篇先到这里,接下来会讲Swift的基本语法。

分享到 评论

设计模式(二)单例模式的七种写法

本文首发于微信公众号「刘望舒」

相关文章
设计模式系列

面试的时候,问到许多年轻的Android开发他所会的设计模式是什么,基本上都会提到单例模式,但是对单例模式也是一知半解,在Android开发中我们经常会运用单例模式,所以我们还是要更了解单例模式才对。

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式结构图:

单例模式有多种写法各有利弊,现在我们来看看各种模式写法。

1. 饿汉模式

public class Singleton {  
     private static Singleton instance = new Singleton();  
     private Singleton (){
     }
     public static Singleton getInstance() {  
     return instance;  
     }  
 }

这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。 这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。

2. 懒汉模式(线程不安全)

public class Singleton {  
      private static Singleton instance;  
      private Singleton (){
      }   
      public static Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
      }  
 }

懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。

3. 懒汉模式(线程安全)

public class Singleton {  
      private static Singleton instance;  
      private Singleton (){
      }
      public static synchronized Singleton getInstance() {  
      if (instance == null) {  
          instance = new Singleton();  
      }  
      return instance;  
      }  
 }

这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。

4. 双重检查模式 (DCL)

public class Singleton {  
      private volatile static Singleton singleton;  
      private Singleton (){
      }   
      public static Singleton getInstance() {  
      if (instance== null) {  
          synchronized (Singleton.class) {  
          if (instance== null) {  
              instance= new Singleton();  
          }  
         }  
     }  
     return singleton;  
     }  
 }

这种写法在getSingleton方法中对singleton进行了两次判空,第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。在这里用到了volatile关键字,不了解volatile关键字的可以查看Java多线程(三)volatile域这篇文章,在这篇文章我也提到了双重检查模式是正确使用volatile关键字的场景之一。
在这里使用volatile会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。 DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。

5. 静态内部类单例模式

public class Singleton { 
    private Singleton(){
    }
      public static Singleton getInstance(){  
        return SingletonHolder.sInstance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    }  
}

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

6. 枚举单例

public enum Singleton {  
     INSTANCE;  
     public void doSomeThing() {  
     }  
 }

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:

private Object readResolve() throws ObjectStreamException{
return singleton;
}

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

7. 使用容器实现单例模式

public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

总结

到这里七中写法都介绍完了,至于选择用哪种形式的单例模式,取决于你的项目本身,是否是有复杂的并发环境,还是需要控制单例对象的资源消耗。

分享到 评论

Android Design Support Library(三)用CoordinatorLayout实现Toolbar隐藏和折叠

本文首发于微信公众号「刘望舒」

查看更多

分享到 评论

设计模式(一)设计六大原则

本文首发于微信公众号「刘望舒」

相关文章
设计模式系列

1. 单一职责原则(SRP)

定义:就一个类而言,应该仅有一个引起它变化的原因
从这句定义我们很难理解它的含义,通俗讲就是我们不要让一个类承担过多的职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到破坏。
比如我经常看到一些Android开发在Activity中写Bean文件,网络数据处理,如果有列表的话Adapter 也写在Activity中,问他们为什么除了好找也没啥理由了,把他们拆分到其他类岂不是更好找,如果Activity过于臃肿行数过多,显然不是好事,如果我们要修改Bean文件,网络处理和Adapter都需要上这个Activity来修改,就会导致引起这个Activity变化的原因太多,我们在版本维护时也会比较头疼。也就严重违背了定义“就一个类而言,应该仅有一个引起它变化的原因”。
当然如果想争论的话,这个模式是可以引起很多争论的,但请记住一点,你写代码不只是为了你也是为了其他人。

2. 开放封闭原则(ASD)

定义:类、模块、函数等等等应该是可以拓展的,但是不可修改。
开放封闭有两个含义,一个是对于拓展是开放的,另一个是对于修改是封闭的。对于开发来说需求肯定是要变化的,但是新需求一来,我们就要把类重新改一遍这显然是令人头疼的,所以我们设计程序时面对需求的改变要尽可能的保证相对的稳定,尽量用新代码实现拓展来修改需求,而不是通过修改原有的代码来实现。
假设我们要实现一个列表,一开始只有查询的功能,如果产品又要增加添加功能,过几天又要增加删除功能,大多数人的做法是写个方法然后通过传入不同的值来控制方法来实现不同的功能,但是如果又要新增功能我们还得修改我们的方法。用开发封闭原则解决就是增加一个抽象的功能类,让增加和删除和查询的作为这个抽象功能类的子类,这样如果我们再添加功能,你会发现我们不需要修改原有的类,只需要添加一个功能类的子类实现功能类的方法就可以了。

3.里氏替换原则(LSP)

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
在使用里氏代换原则时需要注意如下几个问题:

  • 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。
  • 我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
  • Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。

4.依赖倒置原则(DIP)

定义:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字new产生的对象。高层模块就是调用端,低层模块就是具体实现类。
依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。

5.迪米特原则(LOD)

定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
也称为最少知识原则。如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特法则运用到系统设计中时,要注意下面的几点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,一个类型应当设计成不变类;在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

6.接口隔离原则(ISP)

定义:一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

这六个原则,可以使我们在应用的迭代维护中更加方便、轻松的应对,让我们的软件更加灵活。在后续的文章中我会给大家介绍其他的设计模式。

分享到 评论