一、创建型模式--单例模式

##一、创建型模式--单例模式

单例模式介绍

    单例模式是应用最广的模式之一,也可能是很多初级工程师唯一会使用的设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例对象存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。如在一个应用中,应该只需要有一个ImageLoader实例,这个ImageLoader中含有线程池、系统缓存、网络请求等,很消耗资源,因此,没有理由让它构造多个实例。这种不能自由构造对象的情况,就是单例模式的使用场景。

单例模式的定义

    确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的使用场景

    确保某个类有且只有一个对象的情景,避免产生多个对象,消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如:创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时候就需要考虑使用单例模式。

单例模式的关键点

  • 构造函数不对外公开,一般为private
  • 通过一个静态方法或者枚举返回单例类对象
  • 确保单例类的对象有且只有一个,尤其是在多线程下。
  • 确保单例在反序列化时不会重现构建对象。

单例模式的简单示例

//普通员工
publi class Staff{
    public void work(){
        //干活
    }
}

//副总裁
publi class VP extends Staff{
    @Override
    public void work(){
        //管理下面的经理
    }
}

//CEO
publi class CEO extends Staff{
    private static final CEO mCeo = new CEO();
    //私有化构造方法
    private CEO(){
    }
    public static CEO getCeo(){
        return mCeo;
    }

    @Override
    public void work(){
        //管理下面的VP
    }
}

//公司类
public class Company{
    public List<Staff> allStaffs = new ArrayList();
    public void addStaff(Staff person){
        allStaffs.add(person);
    }
    public void showAllStaffs(){
        for(Staff staff:allStaffs){
            System.out.println("Obj:"+staff.hashcode());
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Conpany cp = new Conpany();
        //CEO只能通过getCeo()函数获取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        //通过new创建VP对象
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        //通过new创建Staff对象
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);
        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);
        cp.showAllStaffs();
    }
}

每个对象都有一个唯一的hashcode值,打印结果请自行测试。

从上述代码中可以看到,CEO类不能通过new的形式来创建,只能通过CEO.getCEO()函数来获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化,这就保证了CEO对象的唯一性。从输出结果可以得知两次输出的CEO对象是一样的,而VP、Staff等类型的对象都是不同的,这个实现的核心在于将CEO类的构造方法私有化,使得外部程序不能通过构造方法来构造CEO对象,而CEO类通过一个静态方法返回一个静态对象。

单例模式-懒汉模式

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化。而上述的饿汉式(CEO类)是在声明静态对象时就已经初始化。懒汉单例模式的实现代码如下。

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

总结:getInstance()方法中添加了synchronized关键字,也是就是说getInstance()是一个同步方法,这样在多线程下保持了单例对象唯一性。但是存在一个问题,即使是instance已经被初始化(第一次调用就初始化好了),每次调用getInstance()时都会进行同步,消耗不必要的资源,这也是懒汉单例模式存在最大的问题。


最后:懒汉单例模式优点时只有在需要使用的时候才初始化,在一定程度上节省资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次getInstance都进行同步,造成不必要的同步开销,这种模式一般不建议使用。

单例模式-Double CheckLock实现单例

DCL方式实现单例模式的优点是既能够在需要的时候才初始化单例,又能够保证线程安全,且单例对象初始化之后调用getInstance不进行同步锁。代码如下:

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

单例模式-枚举单例

更为简单的单例实现方式:

public enum SingletonEnum{
    INSTANCE;
    public void dosomething(){
        system.out.println("do sth.");
    }
}

枚举在java中和普通类是一样的,不仅有字段,还有自己的方法。最重要的是枚举默认是线程安全的。

⚠️通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来从而有效的获得一个实例。即使构造函数是私有的,反序列化时然后可以通过特殊的途径去创建一个新的实例,相当于调用该类的构造方法。反序列化操作提供一个很特殊的钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个对象可以让开发人员控制对象的反序列化。例如:上述几个示例如果要杜绝反序列化时重新生成新对象,那么必须加入如下方法:

private Object readResolve() throw ObjectStreamException{
    return sInstance;
}

也就是说在readResolve()中将sInstance对象返回,而不是重新生成一个新的对象。对于枚举,并不存在这个问题,因为即使反序列化也不会重新生成新的实例。

单例模式-使用容器实现单例模式

直接贴代码

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

在程序开始时,将许多单例类型注入到一个统一的管理类中,在使用的时候更具key获取对象对应类型的操作。这种方式可以使得我们管理多种类型的单例,并且在使用的时候通过统一的接口进行操作,降低用户的使用成本,也对用户隐藏了具体细节,降低耦合度。

###扩展

Android源码中单例模式的探索

在Android开发中,我们经常使用context.getSystemService(Context.*)来获取系统级别的服务。如 WindowsMangerService、ActivityManagerService、LayoutInflater等。接下来,我们以LayoutInflater为例来说明

未完待遇….

Create by Diamond_Lin
2016年8月21日。

设计模式前传--面向对象六大原则

##面向对象

1、什么叫面向对象
这个对象不是java中的实例,java语法中,我们把一个类的实例也叫做对象。这种对象严格的说应该是面向对象编程实现(OOP)中的对象,面向对象编程,也不是面向类的实例编程。对象的定义是人们要进行研究的任何事物,从最简单的整数到复杂的飞机等均可看作对象,它不仅能表示具体事务,还能表示抽象的规则、计划或事件。也有的定义为“万物皆对象”。

2、为什么要面向对象

面向对象是为了解决系统的可维护性,可扩展性,可重用性,我们再进一步思考,面向对象为什么能解决系统的可维护性,可扩展性,可重用性?
(面向对象的理解不够深刻,待扩展)

##设计模式前传——面向对象六大基本原则

*单一职能原则

*里氏替换原则

*依赖倒置原则

*接口隔离原则

*迪米特原则

*合成聚合复用原则

####单一职能原则

*定义:不要存在多于一个导致类变更的原因,即一个类只负责一项职责。

*难点:职责的划分

*划分:在不同的情景和生产环境下,我们对职责的细化不同的(职责单一的相对性)单一职责原则提出的是一个评价接口是否优良的标准,但是职责和变化的原因是不可度量的,因项目而异,因环境而异(不可度量性)

*优势

  • 类的复杂性降低:每个类或接口都只实现单一的职责,定义明确清晰
  • 可读性提高:定义明确清晰,自然带来较高的代码可读性
  • 可维护性提高:代码可读性强,更容易理解,自然方便维护,而且职责单一所以类之间耦合度较低,所以更容易修改。
  • 拓展性更好:有新的职责需要拓展,只需要继承对应的接口实现新的实现即可。

*解决方案:遵循单一指责原则,分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

*举例:引用网上一个经典的案例,动物呼吸~

class Animal{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
    }
} 

运行结果:

牛呼吸空气

羊呼吸空气

猪呼吸空气

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下

class Terrestrial{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }
}
class Aquatic{
    public void breathe(String animal){
        System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
        Terrestrial terrestrial = new Terrestrial();
        terrestrial.breathe("牛");
        terrestrial.breathe("羊");
        terrestrial.breathe("猪");

        Aquatic aquatic = new Aquatic();
        aquatic.breathe("鱼");
    }
}

运行结果:

牛呼吸空气

羊呼吸空气

猪呼吸空气

鱼呼吸水

我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:

class Animal{
    public void breathe(String animal){
        if("鱼".equals(animal)){
            System.out.println(animal+"呼吸水");
        }else{
            System.out.println(animal+"呼吸空气");
        }
    }
}

public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe("鱼");
    }
} 

可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:

class Animal{
    public void breathe(String animal){
        System.out.println(animal+"呼吸空气");
    }

    public void breathe2(String animal){
        System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
        Animal animal = new Animal();
        animal.breathe("牛");
        animal.breathe("羊");
        animal.breathe("猪");
        animal.breathe2("鱼");
    }
} 

可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则。

####里氏替换原则

*定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,程序 P 中所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

*定义2:所有引用基类的地方必须能透明地使用其子类的对象。

*问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

*解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

*继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

  • 继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。

image

我们需要完成一个两数相减的功能,由类A来负责。

class A{
    public int func1(int a, int b){
        return a-b;
    }
}

public class Client{
    public static void main(String[] args){
        A a = new A();
        System.out.println("100-50="+a.func1(100, 50));
        System.out.println("100-80="+a.func1(100, 80));
    }
} 

运行结果:

100-50=50

100-80=20

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:

*两数相减。

*两数相加,然后再加100。

由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:

class B extends A{
    public int func1(int a, int b){
        return a+b;
    }

    public int func2(int a, int b){
        return func1(a,b)+100;
    }
}

public class Client{
    public static void main(String[] args){
        B b = new B();
        System.out.println("100-50="+b.func1(100, 50));
        System.out.println("100-80="+b.func1(100, 80));
        System.out.println("100+20+100="+b.func2(100, 20));
    }
} 

类B完成后,运行结果:

100-50=150

100-80=180

100+20+100=220

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

  • 子类可以扩展父类的功能,但不能改变父类原有的功能。

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
    • 子类中可以增加自己特有的方法。

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?

后果就是:你写的代码出问题的几率将会大大增加。

####依赖倒置原则

原始定义:High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

翻译过来就三层定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象

  • 抽象:即抽象类或接口,两者是不能够实例化的。

  • 细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。

现在我们来通过实例还原开篇问题的场景,以便更好的来理解。下面代码描述了一个简单的场景,Jim作为人有吃的方法,苹果有取得自己名字的方法,然后实现Jim去吃苹果。

//具体Jim人类
public class Jim {
    public void eat(Apple apple){
        System.out.println("Jim eat " + apple.getName());
    }
}
//具体苹果类
public class Apple {
    public String getName(){
    return "apple";
    }
}
public class Client {
    public static void main(String[] args) {
        Jim jim = new Jim();
        Apple apple = new Apple();
        jim.eat(apple);
    }
}

运行结果:

Jim eat apple

  上面代码看起来比较简单,但其实是一个非常脆弱的设计。现在Jim可以吃苹果了,但是不能只吃苹果而不吃别的水果啊,这样下去肯定会造成营养失衡。现在想让Jim吃香蕉了(好像香蕉里含钾元素比较多,吃点比较有益),突然发现Jim是吃不了香蕉的,那怎么办呢?看来只有修改代码了啊,由于上面代码中Jim类依赖于Apple类,所以导致不得不去改动Jim类里面的代码。那如果下次Jim又要吃别的水果了呢?继续修改代码?这种处理方式显然是不可取的,频繁修改会带来很大的系统风险,改着改着可能就发现Jim不会吃水果了。

  上面的代码之所以会出现上述难堪的问题,就是因为Jim类依赖于Apple类,两者是紧耦合的关系,其导致的结果就是系统的可维护性大大降低。要增加香蕉类却要去修改Jim类代码,这是不可忍受的,你改你的代码为什么要动我的啊,显然Jim不乐意了。我们常说要设计一个健壮稳定的系统,而这里只是增加了一个香蕉类,就要去修改Jim类,健壮和稳定还从何谈起。

  而根据依赖倒置原则,我们可以对上述代码做些修改,提取抽象的部分。首先我们提取出两个接口:People和Fruit,都提供各自必需的抽象方法,这样以后无论是增加Jim人类,还是增加Apple、Banana等各种水果,都只需要增加自己的实现类就可以了。由于遵循依赖倒置原则,只依赖于抽象,而不依赖于细节,所以增加类无需修改其他类。

//人接口
public interface People {
    public void eat(Fruit fruit);//人都有吃的方法,不然都饿死了
}
//水果接口
public interface Fruit {
    public String getName();//水果都是有名字的
}
//具体Jim人类
public class Jim implements People{
    public void eat(Fruit fruit){
        System.out.println("Jim eat " + fruit.getName());
    }
}
//具体苹果类
public class Apple implements Fruit{
    public String getName(){
        return "apple";
    }
}
//具体香蕉类
public class Banana implements Fruit{
    public String getName(){
        return "banana";
    }
}
public class Client {
    public static void main(String[] args) {
        People jim = new Jim();
        Fruit apple = new Apple();
        Fruit Banana = new Banana();//这里符合了里氏替换原则
        jim.eat(apple);
        jim.eat(Banana);
    }
}

运行结果:

Jim eat apple

Jim eat banana
  • People类是复杂的业务逻辑,属于高层模块,而Fruit是原子模块,属于低层模块。People依赖于抽象的Fruit接口,这就做到了:高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)。
  • People和Fruit接口与各自的实现类没有关系,增加实现类不会影响接口,这就做到了:抽象(抽象类或接口)不应该依赖于细节(具体实现类)。
  • Jim、Apple、Banana实现类都要去实现各自的接口所定义的抽象方法,所以是依赖于接口的。这就做到了:细节(具体实现类)应该依赖抽象。

什么是倒置

  到了这里,我们对依赖倒置原则的“依赖”就很好理解了,但是什么是“倒置”呢。是这样子的,刚开始按照正常人的一般思维方式,我想吃香蕉就是吃香蕉,想吃苹果就吃苹果,编程也是这样,都是按照面向实现的思维方式来设计。而现在要倒置思维,提取公共的抽象,面向接口(抽象类)编程。不再依赖于具体实现了,而是依赖于接口或抽象类,这就是依赖的思维方式“倒置”了。

依赖的三种实现方式

  • 接口方法中声明依赖对象。就是我们上面代码所展示的那样。
  • 构造方法传递依赖对象。在构造函数中的需要传递的参数是抽象类或接口的方式实现。代码如下:
//具体Jim人类
public class Jim implements People{
    private Fruit fruit;
    public Jim(Fruit fruit){//构造方法传递依赖对象
        this.fruit = fruit;
    }
    public void eat(){
        System.out.println("Jim eat"+fruit.getName());
    }
}
  • Setter方法传递依赖对象。在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象。代码如下:
//具体Jim人类
public class Jim implements People{
    private Fruit fruit;
    public void setFruit(Fruit fruit){//seetter方式传递依赖
        this.fruit = fruit;
    }
    public void eat(){
        System.out.println("Jim eat"+fruit.getName());
    }
}

优点:

  • 从上面的代码修改过程中,我们可以看到由于类之间松耦合的设计,面向接口编程依赖抽象而不依赖细节,所以在修改某个类的代码时,不会牵涉到其他类的修改,显著降低系统风险,提高系统健壮性。
  • 还有一个优点是,在我们实际项目开发中,都是多人团队协作,每人负责某一模块。比如一个人负责开发People模块,一人负责开发Fruit模块,如果未采用依赖倒置原则,没有提取抽象,那么开发People模块的人必须等Fruit模块开发完成后自己才能开发,否则编译都无法通过,这就是单线程的开发。为了能够两人并行开发,设计时遵循依赖倒置原则,提取抽象,就可以大大提高开发进度。

说到底,依赖倒置原则的核心就是面向接口编程的思想,尽量对每个实现类都提取抽象和公共接口形成接口或抽象类,依赖于抽象而不要依赖于具体实现。依赖倒置原则的本质其实就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。但是这个原则也是6个设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开放,对修改关闭)也无法实现。

####接口隔离原则

定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

image

如图:

*类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。*

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下图所示:

image

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

接口隔离原则跟之前的单一职责原则

  • 单一职责原则原注重的是职责;接口隔离原则注重对接口依赖的隔离。
  • 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

采用接口隔离原则对接口进行约束时,要注意以下几点:

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

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

####迪米特法则

定义:一个对象应该对其他对象保持最少的了解。

问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

解决方案:尽量降低类与类之间的耦合。

自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。

举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。

//总公司员工
class Employee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

//分公司员工
class SubEmployee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
}

class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司"+i);
            list.add(emp);
        }
        return list;
    }    
    public void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1 = sub.getAllEmployee();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }
        //以上打印分公司员工的代码应该由SubCompanyManager来提供打印方法。
        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}
public class Client{
    public static void main(String[] args){
        CompanyManager e = new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
    }
} 

分析:现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。

修改方案:CompanyManager提供打印方法,在CompanyManager的printAllEmployee调用SubCompanyManager.printEmployee()即可。

####合成聚合复用原则

定义


    经常又被人们称为合成复用原则。合成聚合复用原则是指在一个新的对象中使用原来已经存在的一些对象,使这些原来已经存在的对象称为新对象的一部分,新的对象通过向这些原来已经具有的对象委派相应的动作或者命令达到复用已有功能的目的。


    合成复用原则更简洁的表述是:要尽量使用合成和聚合,尽量不要使用继承。


    聚合是关联关系的一种,用来表示一种整体和部分的拥有关系。整体持 有对部分的引用,可以调用部分的能够被访问的方法和属性等,当然这种访问往往是对接口 和抽象类的访问。作为部分可以可以同时被多个新的对象引用,同时为多个新的对象提供服务。


    合成也是关联关系的一种,但合成是一种比聚合强得多的一种关联关 系。在合成关系里面,部分和整体的生命周期是一样的。作为整体的新对象完全拥有对作为 部分的支配权,包括负责和支配部分的创建和销毁等,即要负责作为部分的内存的分配和内 存释放等。从这里也可以看出来,一个合成关系中的成员对象是不能喝另外的一个合成关系 共享的。


    为何“要尽量使用合成和聚合,尽量不要使用继承”呢?这是因为:第一,继承复用破 坏包装,它把超类的实现细节直接暴露给了子类,这违背了信息隐藏的原则;第二:如果超 类发生了改变,那么子类也要发生相应的改变,这就直接导致了类与类之间的高耦合,不利 于类的扩展、复用、维护等,也带来了系统僵硬和脆弱的设计。而是用合成和聚合的时候新 对象和已有对象的交互往往是通过接口或者抽象类进行的,就可以很好的避免上面的不足, 而且这也可以让每一个新的类专注于实现自己的任务,符合单一职责原则。

Java代码实现

//大臣的接口
public interface Minister { 
    //大臣能够执行的动作
    public void duty(); 
}


//士兵的接口
public interface Soldier {
    //士兵能执行的动作
    public void duty();
}

//韩信对大臣接口的实现
public class Hanxin implements Minister { 
    Soldier[] soldiers;//对士兵的聚合关系
    public Hanxin() {}
    public Hanxin(Soldier[] soldiers) { 
        super();
        this.soldiers =soldiers; 
    }
    //获取士兵集合
    public Soldier[] getSoldiers() { return soldiers;}
    //设置士兵集合
    public void setSoldiers(Soldier[] soldiers) { 
        this.soldiers = soldiers;
    }
    //韩信的职能
    public void duty() { 
        System.out.println("我是刘邦的大臣,永远忠实于刘邦");
    }
}

//士兵A
public class SoldierA implements Soldier { 
//士兵A的职责
    public void duty() { 
        System.out.println("我是韩信的士兵A");
    } 
}

//士兵B
public class SoldierA implements Soldier { 
//士兵A的职责
    public void duty() { 
        System.out.println("我是韩信的士兵B");
    } 
}

//刘邦类
public class LiuBang { 
    Minister[] minister;//拥有大臣
    public LiuBang() {}
    public LiuBang(Minister[] minister) { 
        super();
        minister =minister; 
    }
    //获取大臣集合
    public Minister[] getMinister() { return minister;}
    //设置大臣集合
    public void setMinister(Minister[] minister) { 
        minister = minister;
    }
    //刘邦的职能
    public void duty() { 
        System.out.println("我是皇帝,普天之下,莫非王土;四海之内,莫非王臣");
    }
}

/*
* 测试类的客户端 
*/
public class CARPClient {
    public static void main(String[] args) {
        //声明并实例化士兵A
        Soldier soldierA = new SoldierA(); 
        //声明并实例化士兵B
        Soldier soldierB = new SoldierB(); 
        //构造士兵数组
        Soldier[] soldiers = {soldierA,soldierB};
        //声明并实例化韩信,同时传入士兵数组 
        Minister hanxin = new Hanxin(soldiers);
        //构造大臣数组
        Minister[] minister = {hanxin};
        //声明并实例化刘邦,同时传入大臣数组
        Liubang liubang = new Liubang(minister);
        liubang.duty();
        //君臣
        for(Minister minister : liubang.getMinister()){
            aminister.duty(); 
            for(Soldier soldier:minister.getSoldiers()){
                soldier.duty();
            }            
        }
    }
}

程序运行结果如下:

我是皇帝,普天之下,莫非王土;四海之内,莫非王臣 
我是刘邦的大臣,永远忠实于刘邦
我是韩信的士兵A
我是韩信的士兵B

####开闭原则

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。

在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面六大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面六项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面六项原则遵守程度的“平均得分”,前面六项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面五项原则遵守的不好,则说明开闭原则遵守的不好。


最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。

image

***图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。

image
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。

Create by Diamond_Lin
2016年8月20日。