设计模式之设计原则

在进行架构工作时,需要遵循面向对象的设计原则。这里主要介绍单一职能原则、开闭原则、里氏替换原则、依赖注入原则、接口分离原则、迪米特原则和优先使用组合而不是继承原则。                  

(1)单一职能原则                                                                                            

单一职能原则(Single Responsibility Principle,SRP)的核心思想就是:系统中每一个对象都应该只有一个单独的职责,而所有对象所关注的就是自身的职责的完成。

单一职能原则的意思就是开发人员所说的“高内聚、低耦合”。也就是说,每个类应该只有一个职责,对外职能提供一种功能,而引起变化的原因应该只有一个。在设计模式中,所有的设计模式都遵循这一原则。

有时候,开发人员设计接口的时候会有一些问题,比如用户的属性和用户的行为被放在一个接口中声明。这就造成了业务对象和业务逻辑被放在了一起,使得接口有两种职责,造成职责不明确,按照SRP的定义就违背了接口的单一职责原则了。看下面的代码:

/**
 * @Description:
 * @Date: 2017/12/25 20:39
 */

public interface IFmb {
    //身高
    void setShengao(double height);
    double getShengao();

    //体重
    void setTizhong(double weight);
    double getTizhong();

    //吃饭
    boolean chiFan(boolean hungry);
    //上网
    boolean shangWang(boolean silly);
}

  上面的例子就存在这个问题,身高、体重属于业务对象,与之相对应的方法主要负责用户的属性。而吃饭、上网是响应的业务逻辑,主要负责用户的行为。但是这样就会给人一种不知道这个接口到底是做什么的感觉,职责不清晰,后期维护的时候也会造成各种各样的问题。

  解决的办法是使用单一职责原则,将这个接口分解成两个职责不同的接口即可。先编写文件IFmbBO.java 实现Fmb的属性:

/**
 * @Description:BO业务对象,负责用户的属性
 * @Date: 2017/12/25 20:46
 */

public interface IFmbBO {
    //身高
    void setShengao(double height);
    double getShengao();

    //体重
    void setTizhong(double weight);
    double getTizhong();
}

然后编写文件IFmbBL.java,负责Fmb的行为:

/**
 * @Description:BL:Business Logic,业务逻辑
 * @Date: 2017/12/25 20:47
 */

public interface IFmbBL {
    //吃饭
    boolean chiFan(boolean hungry);
    //上网
    boolean shangWang(boolean silly);
}

这样就实现了接口的单一职责。那么实现接口的时候,就需要两个不同的类。如下面的代码所示:

/**
 * @Description:
 * @Date: 2017/12/25 20:52
 */

public class FmbBL implements IFmbBL {
    @Override
    public boolean chiFan(boolean hungry) {
        if(hungry){
            System.out.println("去吃火锅...");
            return true;
        }
        return false;
    }

    @Override
    public boolean shangWang(boolean silly) {
        if(silly){
            System.out.println("好无聊,上会儿网...");
            return true;
        }
        return false;
    }
}
/**
 * @Description:
 * @Date: 2017/12/25 20:49
 */

public class FmbBO implements IFmbBO{
    private double height;
    private double weight;

    @Override
    public double getShengao() {
        return height;
    }

    @Override
    public double getTizhong() {
        return weight;
    }

    @Override
    public void setShengao(double height) {
        this.height=height;
    }

    @Override
    public void setTizhong(double weight) {
        this.weight=weight;
    }
}

注意:

(a)一个合理的类,应该仅有一个引起它变化的原因,即单一职责。

(b)在没有变化征兆的情况下使用SRP或其他原则是不明智的。

(c)在需求实际发生变化时就应该使用SRP的原则来重构代码。

(d)使用测试驱动开发会迫使我们在设计出现劣质趋势之前分离不合理的代码。

(e)如何测试不能迫使职责分离,僵硬化和侧若星的腐朽味会变得很浓烈,那就应该用Facade或Proxy模式对代码进行重构。

(2)里氏替换原则                                                                                                  

里氏替换原则(Liskov Substitution Principle,LSP)的核心思想就是:在任何父类出现的地方都可以用它的子类来代替。

里氏替换原则的意思就是:同一个继承体系中的对象应该有共同的行为特征。里氏替换原则则关注的是如何良好地使用继承,不要滥用继承,他是继承服用的基石。在里氏替换原则中,所有引用基类的地方必须能够透明地使用其子类对象。也就是说,只要父类出现的地方,子类就能够出现,而且替换为子类不会产生任何错误或者异常。但是反过来,子类出现的地方,替换为父类就可能出现问题了。

这个原则是为了良好的继承定义了一个模范,简单的讲,有四层含义。

(a)子类必须完全实现父类的方法。

例如用下面的代码定义了一个抽象类:

/**
 * @Description:
 * @Date: 2017/12/25 14:30
 */

public  abstract class ViewPoint {
    public abstract void where();
} 

下面两个类是用于实现上述抽象类:

/**
 * @Description:
 * @Date: 2017/12/25 14:31
 */

public class LiJiang extends ViewPoint{
    @Override
    public void where() {
        System.out.println("欢迎来到丽江");
    }
}
/**
 * @Description:
 * @Date: 2017/12/25 14:33
 */

public class Zhangjiajie extends ViewPoint {
    @Override
    public void where() {
        System.out.println("欢迎来到张家界...");
    }
}

Fmb是一个人物,在里面这只类类型来船体参数,此时要去的旅游景点还是抽象的。

public class Fmb {
    //定义要去的旅游景点
    private ViewPoint viewPoint;
    //要去的旅游景点

    public void setViewPoint(ViewPoint viewPoint) {
        this.viewPoint = viewPoint;
    }

    public void travelTo(){
        System.out.println("fmb要去旅游了...");
        viewPoint.where();
    }
}

下面场景类,用于具体要去的景点:

public class Scene {
    public static void main(String[] args) {
        Fmb fmb=new Fmb();
        //设置要去旅游的景点
        fmb.setViewPoint(new LiJiang());
        fmb.travelTo();
    }
}

执行的结果为:

fmb要去旅游了...
欢迎来到丽江

(b)子类可以有自己的属性。

也就是说在类的子类上,可以定义其他的方法或属性。

(c)覆盖或者实现父类的方法时输入参数可以被放大。

分类能够存在的地方,子类就能存在,并且不会对运行结果又变动。反之则不行。例如在下面的代码中,父类的say()里面的参数是HashMap类型,是Map类型的子类型,因为子类的范围应该比父类大:

public class Father {
    public Collection say(HashMap map){
        System.out.println("父类被执行...");
        return  map.values();
    }
}

在下面的代码中,子类的say()里面的参数变成了Map类型,Map范围比HashMap类型大,符合LSP原则

public class Son extends Father{
    public Collection say(Map map){
        System.out.println("子类被执行...");
        return  map.values();
    }
}

下面是场景类的代码

public class Home {
    public static void main(String[] args) {
        invoke();
    }
    public static void invoke(){
        //父类存在的地方,子类就应该能够存在
        Father father=new Father();
        Son son=new Son();
        HashMap map=new HashMap();
        father.say(map);
        son.say(map);
    }
}

无论是父类还是子类调用say方法,运行结果都一样:

父类被执行......

但是如果将上面的Father里面的参数say改为Map,将子类Son里的参数say改为HashMap,则运行的结果为:

f.say(map)结果:父类被执行。。。

s.say(map)结果:子类被执行。。。

(d)覆盖或者实现父类的方法时输出结果可以被缩写。                                                              

(3)依赖注入原则                                                                                              

依赖注入原则(Dependence Inversion Principle)的核心思想就是:要依赖于抽象,不要依赖与具体的实现。

依赖注入原则的意思就是:在应用程序中,所有的类如果使用或依赖于其他的类,则都应该依赖于这些其他类的抽象类,而不是这些其他类的具体实现类。抽象层次应该不依赖于具体的实现细节,这样才能保证系统的可复用性和可维护性。为了实现这一原则,就要求开发人员在编程时针对接口编程,而不是针对实现编程。

依赖注入原则有如下三点说明:

(a)高层模块不应该依赖底层模块,两者都应该依赖于抽象(抽象类或接口)。

(b)抽象(抽象类或接口)不应该依赖于细节(具体实现类)。

(c)细节(具体实现类)应该依赖于抽象。

依赖注入原则用如下三种方式来实现。

(a)通过构造函数来传递依赖对象

  例如在构造函数中的需要传递的参数是抽象类或接口的方式实现。

(b)通过setter方法传递依赖对象

  即我们在设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象。

(c)接口声明实现依赖对象

例如在下面的例子中,Fmb是人物

public class Fmb {
    public void cook(Noodles noodles){
        noodles.eat();
    }
}

下面是吃面条的代码:

public class Noodles {
    //吃面条
    public void eat(){
        System.out.println("fmb吃面条了...");
    }
}

下面是在家里吃面条的场景类代码:

public class Home {
    public static void main(String[] args) {
        Fmb fmb=new Fmb();
        Noodles noodles=new Noodles();
        fmb.cook(noodles);
    }
}

但是这里有一个问题,cook方法没有扩展性,只能做面条,比较单调。所以希望Fmb中的Cook方法,会做其他的吃的,这里可以使用依赖注入原则。

 

(4)接口分离原则                                                                                          

接口分离的原则(Interface Segregation Principle)的核心思想就是:不应该强迫客户程序依赖他们不需要使用的方法。

接口分离的意思就是:一个接口不需要提供太多的行为,一个接口应该只提供一种对外的功能,不应该把所有的操作都封装到一个接口中。

接口分为如下两种:

(1)对象接口:java中声明的一个类,通过new关键字产生一个实例,它是对一个类型的事物的描述,这也是一种接口。

(2)类接口:这种接口就是通过关键字interface定义的接口。

接口分离原则和单一职能原则有些相似,不过不同在于:单一职能原则要求的是类和接口职责单一,注重的是职责,是业务逻辑上的划分。而接口分离原则要求的是接口的方法尽量少,针对一个模块尽量有用。

在使用接口分离原则的时候,需要一些规范。

(1)接口尽量小:接口尽量小,这主要是为了保证一个接口只服务于一个子模块或者业务逻辑。

(2)接口高内聚:是对内高度依赖,对外尽可能隔离。即一个接口内部声明的方法相互之间都与某一个子模块相关,且是这个子模块必需的。

(3)接口设计是有限度的:如果完全遵循接口分离原则的话,会出现一个问题,即接口的设计力度会越来越小,这样就在成了接口数量的剧增,系统复杂度会上升,所以在使用这个原则的时候,还要在特定的项目中,根据经验或者尝试去判断,单没有一个固定的标准。

(5)迪米特原则                                                                                                  

迪米特原则(Law of Demeter,LOD)的核心思想是:一个对象应该尽可能对其他对象尽可能少地了解。意思就是降低各个对象之间的耦合,提高系统的课维护性。在模块之间,应该只能通过接口来通信,而不理会模块内部工作原理,他可以使各个模块之间的耦合程度降到最低,促进软件的复用。

在应用迪米特法则时的注意事项如下:

(a)在类的划分上,应该创建有弱耦合的类。

(b)在类的结构设计上,每一个类都应当尽量降低成员的访问权限。

(c)在类的设计上,只要有可能,一个类应当设计成不变类。

(d)在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

(e)尽量降低类的访问权限

(f)谨慎使用序列化功能

(g)不应该暴露类成员,而应该提供相应的访问器(属性)。

(6)开闭原则                                                                                                        

开闭原则(Opne for Extension,Closed for Modification, OCP)。开闭原则的核心思想是:一个对象对扩展开放,对修改关闭。意思就是对类的改动是通过增加代码进行的,而不是改动现有的代码。

原文地址:https://www.cnblogs.com/rainydayfmb/p/8073422.html