JAVA设计模式之开篇

栏目: Java · 发布时间: 6年前

内容简介:定义:单一职责的英文全称是Single Responsibility Principle,简称SPR。英文解释是:There should never be more than one reason for a class to change.翻译过来就是,一个类只能有且仅仅有一个原因导致类的变更。

2.1 单一职责

定义:单一职责的英文全称是Single Responsibility Principle,简称SPR。

英文解释是:There should never be more than one reason for a class to change.

翻译过来就是,一个类只能有且仅仅有一个原因导致类的变更。

我们用一个例子说明下:

需求场景:设计一个手机,手机包含功能为打电话,挂电话,播放音乐功能。

public interface Imobile {
    //打电话
    public void call(String number);
    //播放音乐
    public void playMusic(Object o);
    //挂断电话
    public  void hangup();
}
复制代码

上面设计了一个 Imobile 的接口,声明了打电话,挂断,播放音乐的方法,我们初步看,觉得这么设计没什么问题,但是如果我们考虑单一职责的话,这个设计就有问题了,其实单一职责最难划分的就是职责,我们针对这个场景可以给这个电话分为两个职责,打电话和挂电话是属于协议管理的,播放音乐其实属于附属功能管理,所以这里的职责就划分了两个:1.协议管理;2.附属功能管理。那么单一职责的定义就是:一个类只能有且仅仅有一个原因导致类的变更。而上面这个接口中划分了两个职责,而且,协议的变动,附属功能的变动,都会导致接口和类改变,所以,这个接口就是不符合单一职责的。那么如何让其满足单一职责原则呢?我们需要拆分接口,因为协议管理和附属功能管理两个彼此并不互相影响,所以我们可以直接拆分为两个接口,如下:

//协议管理接口
public interface IMobileManager {
    //打电话
    public void call(String number);
    //挂断电话
    public  void hangup();
}
//附属功能接口
public interface Ifunction {
    public void playMusic(Object o);
}
复制代码

这个时候很多人可能不理解,你这么做的好处是什么呢?我感觉不到这么做的好处啊。这里做一个假设,假设这个时候新增了一部高级手机,它可以保持会话,这个时候协议管理接口需要修改了,需要新增一个保持会话的功能,这个时候实现类也要跟着改变,如果采用第一种设计,那么所有的电话都要修改。如果有一个玩具手机,它并不会通话,这个时候也要修改这个实现类,这个设计就糟糕了。如果采用了单一职责,玩具手机并不会实现协议管理的接口,只会实现附属功能接口,所以协议管理的修改并不会导致玩具手机也要修改。

2.1.1 单一职责的好处

  • 类的复杂度降低了,各个职责都有清晰明确的定义
  • 提高了可读性,知道什么接口是干什么的
  • 提高了可维护性,某个接口的修改不会导致无关类受影响。

2.1.2 单一职责的补充

其实单一职责并不只要求接口,方法也是,我们写一个方法要能清晰的定义这个方法的职责,比如修改用户信息最好就要写多个方法来实现,不要就只写一个方法。类似于这样:

public interface IUserSerivice {
    void updateUserInfo(User user);
}
复制代码

这种设计不清晰,我们应该针对每一个修改都有一个方法,类似于这样:

public interface IUserSerivice {
    void updateUserName(String name,String id);
    void updateUserTelPhone(String phone,String id);
    void updateUserHomeAddr(String adrr,String id);
}
复制代码

这样写虽然很啰嗦,但是职责很清晰,后续代码也好维护,直接就能知道更新了什么信息。

2.2 里氏替换

定义:里氏替换原则的英文全称:Liskov Substitution Principle ,简称LSP。

英文解释:Functions that user pointer or references to base classes must be able to use objects of derived classes without knowing it.

翻译:所有引用基类的地方都必须能透明的使用其子类对象。

其实理解这句话很简单,无非就是父类执行的方法,替换成子类也可以正确执行并且达到一样的效果。我们先写一个没有按照里氏替换原则的代码。

public class Father {
    public void doSomeThing(Map map){
        System.out.println("父类执行啦!");
    }
}
public class Son extends Father{
    public void doSomeThing(HashMap map) {
        System.out.println("子类执行了!");
    }
}
public class Client1 {
    public static void main(String[] args) {
        HashMap map=new HashMap();
        Father father=new Father();
        father.doSomeThing(map);
    }
}

public class Client2 {
    public static void main(String[] args) {
        HashMap map=new HashMap();
        Son son=new Son();
        son.doSomeThing(map);
    }
}
复制代码

我们执行客户端main方法,发现结果输出为:“父类执行啦!”,我们采用子类替换父类执行 doSomeThing() 方法,发现输出结果是:“子类执行了!”,这和父类执行的结果不一致,不符合里氏替换原则,这里为什么没有执行父类的方法呢?这里因为是子类重载了父类的方法,客户端调用的参数是HashMap,所以匹配到了子类的方法。那么我们如何修改就能满足里氏替换原则呢?其实很简单,两种方式。

  • 第一,直接继承,不要重写父类的非抽象方法。
  • 第二,我们重载方法的参数范围必须大于等于父类的范围。

第一个好理解,那第二个怎么理解呢?我们还是用上面那个例子改动下,代码如下:

public class Father {
    public void doSomeThing(HashMap map){
        System.out.println("父类执行啦!");
    }
}
public class Son extends Father{
    public void doSomeThing(Map map) {
        System.out.println("子类执行了!");
    }
}
复制代码

这里其实就只把子类的参数类型改成了Map,父类的参数类型改成了HashMap, 这样客户端声明的参数类型是HashMap,所以调用 son.doSomeThing(map)只会执行父类的方法。

这里其实可以总结一句:里氏替换原则就是要求, 不要重写父类的非抽象方法,尽量不要重载父类的方法,如果要重载,需要注意方法的前置条件(形参),如果要保持子类的个性化,可以采用新增方法的方式。

2.2.1 里氏替换原则的作用

  • 其实最主要的作用就是降级继承的复杂度,增强代码的可维护性

2.3 依赖倒置

定义:依赖倒置英文全称为:Dependence Inversion Princiole,简称DIP。

英文解释: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。

官方翻译:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

依赖倒置,我们用通俗的解释就是,平常我们生活中的依赖都是依赖具体细节,比如我要用手机就是具体的某个手机,用电脑就是用具体的某台电脑,这个依赖倒置就是和我们生活是反的,故称为倒置,所以依赖倒置就是依赖抽象(接口或者抽象类)。我们同样用一个例子来说明下:

我们实现一个司机开车的例子,我们可以抽象出2个接口,一个是司机接口,一个是汽车接口。

public interface ICar {
    //开汽车方法
    public void run();
}
public interface IDriver {
    //开车
    public void driver(ICar car);
}
//汽车实现类,宝马车
public class BmwCar implements ICar {
    @Override
    public void run() {
        System.out.println("宝马车开动啦");
    }
}
//司机实现类,C1驾照司机
public class COneDriver implements  IDriver {
    @Override
    public void driver(ICar car) {
        System.out.println("我是C1驾照司机");
        car.run();
    }
}
// 客户端场景类
public class Client {
    public static void main(String[] args) {
        ICar bmw=new BmwCar();
        IDriver cOneDriver=new COneDriver();
        cOneDriver.driver(bmw);
    }
}

复制代码

这里实现了C1驾照司机开宝马车的场景,这就是依赖倒置原则的写法,那如果我不采用依赖倒置会发生什么情况呢?不依赖倒置也就是说要依赖细节,以上场景就会出现C1驾照车司机只能开宝马车的情况,这显然是有问题的。

2.3.1 依赖倒置的规则

根据上面的例子以及我们的分析,我们可以总结出依赖倒置的几个规则:

  • 每个类尽量都有接口或者抽象类,或者抽象类和接口两者都具备。
  • 变量的表面类型尽量是接口或者是抽象类。
  • 任何类都不应该从具体的实现类中派生
  • 尽量不要重写基类的方法

2.4 迪米特法则

定义:迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP)

迪米特法则通俗的解释就是,一个类要对其所耦合的类了解的尽量少,不管耦合的类内部多么复杂,都只管其暴露的public方法。迪米特法则另外一种说法是,只和朋友类交流。朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。我们先看一个违法迪米特法则的例子。

场景:我们吃饭要经过客户点菜,服务员下单,厨师做菜这三个流程,我们来用代码设计这个场景。

//厨师接口
public interface ICooker {
    //根据订单做菜
    public void cooke(List<Order> orders);
}
//服务员接口
public interface IWaiter {
    //下单
    public void doOrder(List<String> dishNames);
}
// 订单实体类
public class Order {
    private List<String> dishNames;

    public Order(List<String> dishNames) {
        this.dishNames = dishNames;
    }

    public List<String> getDishNames() {
        return dishNames;
    }

    public void setDishNames(List<String> dishNames) {
        this.dishNames = dishNames;
    }
}

// 服务员实现类
public class ChineseWaiter implements IWaiter {
    private ICooker cooker;

    public ChineseWaiter(ICooker cooker) {
        this.cooker = cooker;
    }

    @Override
    public void doOrder(List<String> dishNames) {
        List<Order> cookOrders=new ArrayList<>();
        cookOrders.add(new Order(dishNames));
        cooker.cooke(cookOrders);
    }
}

//厨师实现类
public class ChineseCooker implements ICooker {
    @Override
    public void cooke(List<Order> orders) {
        for (int i = 0; i < orders.size(); i++) {
           Order order=orders.get(i);
           List<String> dishNames=order.getDishNames();
            for (int j = 0; j < dishNames.size(); j++) {
                System.out.println("我是中餐厨师,我做:"+dishNames.get(j));
            }
        }

    }
}
//场景类
public class Client {
    public static void main(String[] args) {
        IWaiter waiter=new ChineseWaiter(new ChineseCooker());
        List<String> dishNames=new ArrayList<>();
        dishNames.add("红烧鱼块");
        dishNames.add("宫保鸡丁");
        waiter.doOrder(dishNames);
    }
}
复制代码

我们自己思考下,其实上述代码中,违法迪米特法则地方就是服务员的实现类,我们发现,服务员实现类ChineseWaiter在实现类中,和非朋友类产生了依赖,这个依赖就是Order类,我们再回顾下朋友类的定义: 出现在成员变量、方法的输入输出参数中的类称为成员朋友类 ,Order类并不满足这个定义,所以它违反了迪米特法则。那么我们如何修改满足迪米特法则呢?我们只要修改服务员实现类和场景类即可,修改后的代码如下:

public interface IWaiter {
    //下单
    public void doOrder(List<Order> orders);
}


public class ChineseWaiter implements IWaiter {
    private ICooker cooker;

    public ChineseWaiter(ICooker cooker) {
        this.cooker = cooker;
    }


    @Override
    public void doOrder(List<Order> orders) {
        cooker.cooke(orders);
    }
}
public class Client {
    public static void main(String[] args) {
        IWaiter waiter=new ChineseWaiter(new ChineseCooker());
        List<String> dishNames=new ArrayList<>();
        dishNames.add("红烧鱼块");
        dishNames.add("宫保鸡丁");
        List<Order> orders =new ArrayList<>();
        orders.add(new Order(dishNames));
        waiter.doOrder(orders);
    }
}
复制代码

这里把订单的封装丢给了场景类中,服务员只依赖他的朋友类厨师类就可以了。那么这个迪米特法则有什么作用呢?其实迪米特法则最主要的作用就是降低耦合,从而使得类的复用率得以提高。但是采用迪米特法则后就会导致产生了过多的中间类和跳转类,导致系统的复杂性提高,所以我们在使用该法则的时候要权衡利弊,还是那句话,没有最完美的设计,只有最合适的设计。

2.5 接口隔离

英文解释:Clients should not be forced to depend upon interfaces that they don't use.The dependency of one class to another one should depend on the smallest possible interface.

官方翻译:客户端不应该依赖它不需要的接口。类间的依赖关系应该建立在最小的接口上。

接口隔离原则,其实可以理解为接口设计的粒度要尽量小,接口中的方法要尽量少。这里其实和单一职责很相识,但是有区别,单一职责是职责的划分要求,每个接口只要表述对应的职责就可以了。但是接口隔离一般是对应于某个模块调用,可能只用到某个接口的部分方法,可以更细分。举例说明:

还是以单一职责的例子,设计手机。之前的代码是分为了一个功能接口,一个协议管理接口。代码见单一职责部分。我们看看如果是用接口隔离还可以怎么设计。我们其实还可以对功能接口可以划分更细的粒度,比如最新的iPhone手机有faceId功能,三星手机有虹膜功能。那这个时候,我还是用一个功能接口,就会导致接口非常冗余,一个接口有faceid,虹膜,但是实际上有些手机并没有这些功能,那么我们就可以对功能接口进行拆分。拆分成这样:

public interface ISamFunction {
    //虹膜功能
    public void iris();
}

public interface IAppleFnction {
    //faceId 功能
    public void faceId();
}
复制代码

然后如果有手机既有虹膜又有faceId功能,直接实现两个接口就可以了。这样就满足了接口隔离原则。

2.6 开闭原则

英文解释:Software entities like classes,modules and functions should be open for extension but closed for modifications

官方翻译:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

开闭原则,其实是一个总的原则,前面五种原则其实都是开闭原则的具体实现,它并没有一个具体的设计思路,只是要求我们对设计的类,方法等对扩展开放,对修改关闭。掌握了前面五种设计原则,其实也就掌握了开闭原则了,这里就不举例说明了。


以上所述就是小编给大家介绍的《JAVA设计模式之开篇》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTML5

HTML5

Matthew David / Focal Press / 2010-07-29 / USD 39.95

Implement the powerful new multimedia and interactive capabilities offered by HTML5, including style control tools, illustration tools, video, audio, and rich media solutions. Understand how HTML5 is ......一起来看看 《HTML5》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试