设计模式之大话细品SOLID设计原则(一)

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

定义:

      单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情。举个例子,比如吃西餐用的刀叉,刀是用来分割食物,叉是用来拾取食物,职责很清晰。而在中餐中的筷子呢?筷子的主要作用是用来拾取食物,但是偶尔也会客串分割食物的角色,所以筷子的职责不是单一的。

      在软件工程中,这个原则看似最容易理解,但是确是最难实现的一个原则。因为主观性会比较强。在设计接口中,如果不满足原则,可以拆分接口,这个很勉强可以接受。但是如果一个实体类不满原则,那该怎么办?拆分成几个类?在实际开发中要是这么干了,估计脑壳会被敲爆。原则是提供了一种规范,尺寸要把握合适。

举例:

    声明一个吃饭工具的接口EatingTool ,接口含有两个抽象方法:cuttFood和pickFood。声明了一个筷子类Chopstick,筷子了实现了接口EatingTool ,并实现了cuttFood和pickFood方法。测试类用筷子吃摊鸡蛋的时候,先用筷子把摊鸡蛋切成小块,然后再夹起来送到嘴里,这个设计和过程都是合乎情理的。

/**
 * 吃饭工具接口
 */
public interface EatingTool {

    //切割食物
    void cuttFood(String food);
    //拾取食物
    void pickFood(String food);
}

/**
 * 筷子类
 */
public class Chopstick  implements EatingTool{
    @Override
    public void cuttFood(String food) {
        System.out.println("切割了 :" + food);
    }
    @Override
    public void pickFood(String food) {
        System.out.println("夹起了 :" + food);
    }
}

public class TestEat {
    public static void main(String[] args) {
        Chopstick chopstick = new Chopstick();
        chopstick.cuttFood("摊鸡蛋");
        chopstick.pickFood("摊鸡蛋");
    }
}

打印结果:
切割了 :摊鸡蛋
夹起了 :摊鸡蛋

       但是有一天,筷子不见了,只剩下刀叉,但是摊鸡蛋还是要吃的,用刀叉也一样,但事实错了。声明一个刀类,也实现了工具接口EatingTool,但是这里就有问题了。餐刀只能用来切割食物,不能拾取食物啊,用餐刀吃饭,气氛有点怪怪的。EatingTool有两个功能,不满足单一职责,怎么办呢? 拆分?EatingTool保留没有具体细节,将切割食物和拾取食物拆分出两个接口。于是又吃到了摊鸡蛋,代码如下:

/**
 * 吃饭工具接口
 */
public interface EatingTool {

}

/**
 * 切割食物的工具
 */
public interface CuttFoodTool extends EatingTool{
    //切割食物
    void cuttFood(String food);
}

/**
 * 拾取食物
 */
public interface PickFoodTool extends EatingTool{
    //拾取食物
    void pickFood(String food);
}


/**
 * 餐刀
 */
public class Knife implements CuttFoodTool{

    @Override
    public void cuttFood(String food) {
        System.out.println("切割 "+food );
    }
}

/**
 * 餐叉
 */
public class Fork  implements PickFoodTool{
    @Override
    public void pickFood(String food) {
        System.out.println("拾取 "+ food);
    }
}

public class TestEat {
    public static void main(String[] args) {
        Knife knife = new Knife();
        knife.cuttFood("摊鸡蛋");
        Fork fork = new Fork();
        fork.pickFood("摊鸡蛋");
    }
}

执行结果:
切割 摊鸡蛋
拾取 摊鸡蛋

不过此时的筷子类也需要适当修改,分别实现CuttFoodTool,PickFoodTool两个接口

/**
 * 筷子类
 */
public class Chopstick  implements CuttFoodTool,PickFoodTool{
    @Override
    public void cuttFood(String food) {
        System.out.println("切割了 :" + food);
    }
    @Override
    public void pickFood(String food) {
        System.out.println("夹起了 :" + food);
    }
}

这样就解决了问题,如果将来再来个汤勺,那么一样可以扩展一个盛汤的接口,而不影响筷子和刀叉类 

优势: 

简单归纳下单一原则的优点:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 代码复杂性降低,代码的可读性提高;
  • 可维护性提高,可读性提高,更容易维护;
  • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

实践:

       在实际的开发场景中,单一职责可能在不同场景有着不同的表现,比如用户信息,项目初期由于业务简单,可能用户的所有信息都可以放在一个userInfo类中,不过随着业务的不过扩展,比如扩展了电商功能,那么地址信息就需要拆分成独立类。在不同的业务场景或者发展接口,对于单一职责解读是不一样的。单如果无法确认类的最终业务走向和设计规则,不妨先设计一个粗粒度类,随着业务的方法,在扩张到了一定程度之后对类再进行拆分成几个粒度更小的类。

       单一职责原则主要体现了内聚性,接口的功能越单一,影响的范围越小,同时代码维护的成本与越低。不过从上面修改的例子中,新增了很多类和接口,所以代码的维护成本只是从复杂度的简单来说明。单一职责也不并不粒度划分的越小就越好,划分的粒度越小,那么接口和类越多,反而会造成代码不易维护,所以也要避免过度设计。

     对于如何判断一个类是否符合单一职责。如果单从概念上去匹配的话还是比较吃力。也可以从以下几个技巧方面去突破以下:比如一个类的方法、属性较多时,上百个得情况也会出现;一个类过度依赖其他类,不符合高内聚低耦合的设计模型;新建一个类时想不到一个恰当的类名来表述含义;一个类拥有大多数方法只是针对几个属性的操作;私有方法很多等等情况。

里氏替换原则LSP(Liskov SubstitutionPrinciple)

定义:

       只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

举例:

      小明 在小时候的都从自行车开始学起,找大以后有了实例就开汽车,自行车和汽车都是可以行驶的交通工具。从代码中,可以看出,小明选择的只是一种交通工具DirverTool ,至于是汽车还是自行车并不关心,只要是交通工具的一种,也就是父类出现的地方子类都可以出现。


/**
 * 驾驶工具
 */
public interface DirverTool {

    void dirvering();
}
/**
 * 汽车类
 */
public class Car implements DirverTool{

    @Override
    public void dirvering() {
        System.out.println("开汽车");
    }
}
/**
 * 自行车
 */
public class Bicycle  implements  DirverTool{
    @Override
    public void dirvering() {
        System.out.println("骑自行车");
    }
}

/**
 * 小明
 */
public class People {
    //小明开车
    public void dirver(DirverTool dt){
        dt.dirvering();
    }
}

/**
 * 里氏替换原则测试
 */
public class TestDriver {
    public static void main(String[] args) {
        People xiaoming = new People();
        //小时候
        xiaoming.dirver(new Bicycle());
        //长大后
        xiaoming.dirver(new Car());
    }
}
执行结果:
骑自行车
开汽车

      但是子类出现的地方,父类就不一定能出现,小明后来转行,买了台拖拉机开始搬砖,拖拉机也可以形式,但是有自己独特的个性搬砖,这个时候work的时候只能使用拖拉机,而不是所有驾驶工作都有搬砖的属性,所有父类不能出现的。

/**
 * 拖拉机
 */
public class Tractor implements DirverTool {
    @Override
    public void dirvering() {
        System.out.println("开拖拉机");
    }

    //搬砖
    public void CarryBricks(){
        System.out.println("开始搬砖");
    }
}

/**
 * 小明
 */
public class People {
    //小明开车
    public void dirver(DirverTool dt){
        dt.dirvering();
    }
    //工作
    public void work(Tractor t){
        t.carryBricks();
    }
}


/**
 * 里氏替换原则测试
 */
public class TestDriver {
    public static void main(String[] args) {
        People xiaoming = new People();
        xiaoming.dirver(new Tractor());
        xiaoming.work(new Tractor());
    }
}
测试结果:
开拖拉机
开始搬砖

       里氏替换原则主要是为了提高代码的健壮性,把父类当做参数,而实际运用的是子类,这样及时新增一个子类,也无需影响原因代码。但是如果子类具有自己的个性时,在参数传输中,子类的个性会丢失,这是要注意的地方,尽量子类不要个性太强。

实践:

里氏替换原则规定了父子继承关系范围(包括接口的实现)

  • 子类必须完全实现父类的方法
  • 子类可以有自己的个性
  • 覆盖或实现父类的方法时输入参数可以被放大

        多态和里氏替换很相似,多态是面向对象编程语言的一大特性,是一种指导思想;里氏替换原则是一种设计原则,是用来指导继承关系中子类的实现逻辑,子类在设计过程要保证在替换父类时要保证原有逻辑的正确性。

依赖倒置原则Dependence Inversion Principle(DIP)

定义:

依赖倒置原则指的是两个模块之间依赖抽象的类,比如接口或者抽象类,更利用模块间的解耦,也是倒置的真正含义。在Java编程思想中,是将具体变为抽象,倒置就是反过来运用,将依赖变为更抽象的类。更具的说依赖倒置富含三层含义:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

举例:

通俗的讲每个类应该是由抽象类和实体类实现,模块和模块之间通过抽象类依赖。比如小王是一个游戏爱号者Player ,尤其酷爱手游MobileGame ,每天都会在手游里打怪升级TestMG ,用代码实现如下:

/**
 * 手游
 */
public class MobileGame {

    public MobileGame(){
        System.out.println("手游");
    }
}

/**
 * 游戏爱好者
 */
public class Player {

    public void play(MobileGame mobileGame){
        System.out.println("打怪升级中......");
    }
}

/**
 * 依赖倒置原则测试类
 */
public class TestMG {

    public static void main(String[] args) {

        Player xiaowang = new Player();
        //小王玩手游
        xiaowang.play(new MobileGame());
    }
}

执行结果:
手游
打怪升级中......


这样一来就满足了小王的需求。不过有一天,小王的手机出问题了,手游玩不了,于是小王想到端游, 但是现在场景并不允许小王来玩端游。之前的场景设计的不合理,游戏和小王强耦合了,倒置小王只能玩手游。解决方案就是把游戏抽取成一个抽象的游戏类Game,端游TerminalTour和手游MobileGame同时实现Game接口,游戏爱好者Player中不只接受Game,不接收具体的游戏细节。修改代码如下:

/**
 * 游戏类接口
 */
public interface Game {
}

/**
 * 手游
 */
    public class MobileGame implements Game{

    public MobileGame(){
        System.out.println("手游");
    }
}

/**
 * 端游
 */
public class TerminalTour implements Game{

    public TerminalTour(){
        System.out.println("端游");
    }
}

/**
 * 游戏爱好者
 */
public class Player {

    public void play(Game game){
        System.out.println("打怪升级中......");
    }
}

/**
 * 依赖倒置原则测试类
 */
public class TestMG {

    public static void main(String[] args) {

        Player xiaowang = new Player();
        //小王玩手游
        xiaowang.play(new MobileGame());
        //小王玩端游
        xiaowang.play(new TerminalTour());
    }
}

执行结果:

手游
打怪升级中......
端游
打怪升级中......
    

这样又满足了小王的需求,哪怕是停电了,小王只能去游戏厅打电动,只需要增加一个电玩的实现类,就可以满足,扩展性增强了。随着对游戏的深入,小玩认识了一帮朋友,有玩手游的小张,有玩端游的小李。小王有个想法,把这些朋友区分出来,这样玩手游的时候就可以找小张,玩端游的时候就可以找小李。问题又来了Palyer是没法根据游戏类型了,按照游戏的方式,玩游戏的人是不是也可以抽象,说干就干。

/**
 * 游戏爱好者
 */
public interface Iplayer {
    //玩游戏
    void play(Game game);
}

/**
 * 手游爱好者
 */
public class GmPlayer  implements Iplayer{
    @Override
    public void play(Game game) {
        System.out.println("玩手游,打怪升级......");
    }
}

/**
 * 端游爱好者
 */
public class TtPlayer implements  Iplayer{
    @Override
    public void play(Game game) {
        System.out.println("玩端游,打怪升级......");
    }
}

/**
 * 依赖倒置原则测试类
 */
public class TestMG {

    public static void main(String[] args) {

        Iplayer xiaoli = new TtPlayer();
        xiaoli.play(new TerminalTour());
        Iplayer xiaozhang = new GmPlayer();
        xiaoli.play(new MobileGame());
    }
}

测试结果:

端游
玩端游,打怪升级......
手游
玩端游,打怪升级......

优势:

如此一顿操作,游戏爱好者和游戏本身的通过抽象类依赖,降低了耦合度。这也是依赖倒置原则的含义。依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。没错,起到联系作用的就是是接口或者抽象类而不应该是实体类。已经见识到了紧耦合的代码会有多可怕,依赖倒置实现了松耦合,除了通过接口方法传递方法也称为接口注入,依赖的方法可以总结为以下三种:

  • 构造函数传递依赖对象
  • Setter方法传递依赖对象.
  • 在接口的方法中声明依赖对象也叫接口注入

实践:

依赖倒置原则的核心其实是“面向接口编程”,那在项目中如果运用到依赖倒置原则:

  • 首先每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备

依赖倒置的基本要求,有了抽象,才能倒置

  • 变量的表面类型尽量是接口或者是抽象类

变量使用抽象类的好处就是父类出现的地方,子类总可以出现。但是像工具类这条原则并不适用,所有应该是有具体逻辑类中适用。

  •  任何类都不应该从具体类派生

项目中,一个类的继承关系有四五层,这个是不能接受的,所有能抽取尽量抽取抽象类,减少继承关系,继承的层级越深,那么修改影响的范围可能就会越广。

  • 尽量不要覆写基类的方法

重写的好处是增加本身的特色,但是坏处就是依赖的稳定性受到一定程度的影响。

  • 结合里氏替换原则使用

接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。

参考:《设计模式之禅》 秦小波著

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值