“设计模式之禅”——六大设计原则详解解读

目录

一、单一职责原则

二、里氏替换原则

三、依赖倒转原则

四、接口隔离原则

五、迪米特法则

六、开闭原则


一、单一职责原则

        单一职责原则的英文名称是:Single Responsibility Principle,简称SRP。它的内容是:应该有且只有一个引起类变化的原因

        例如下面这个类,它的设计就违反了单一职责原则:

        

        经过职责划分后的类图如下:

         

        重新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。

        分清职责后的代码示例:

......
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();
......

        采用SRP的类图如下:

         

 

        单一职责原则的好处:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高,复杂性降低
  • 可维护性提高,可读性提高
  • 变更引起的风险降低

        单一职责适用于接口、类,同时也适用于方法。

二、里氏替换原则

        里式替换原则的英文名称是:Liskov Substitution Principle,简称LSP。它的内容是:所有引用基类的地方必须能透明地使用其子类的对象。通俗地讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常。

        里式替换原则为良好的继承定义了一个规范,它包含了4层含义:

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

        例如下面这个例子,模拟射击游戏的枪支类:

        

        设计一个枪支抽象类,然后具体的枪支继承这个抽象类,并实现对应的shoot方法。

public abstract class AbstractGun {
    public abstract void shoot();
} 
public class HandGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.println("手枪设计...");
    }
}

//Rifle、MachineGun省略

        在士兵类中,使用枪来杀敌,但是这个枪是抽象的,具体是什么类型的枪需要通过setGun方法来确定。

public class Soldier {
    private AbstractGun gun;
    public void setGun(AbstractGun gun) {
        this.gun = gun;
    }
    public void killEnemy() {
        System.out.println("士兵开始杀敌人...");
        gun.shoot();
    }
}
public class Client {
    public static void main(String[] args) {
        Soldier soldier = new Soldier();
        soldier.setGun(new HandGun());
        soldier.killEnemy();
    }
}

        在类中调用其他类时务必使用父类或者接口,如果不能使用父类或者接口,则说明类的设计已经违背了LSP原则。 

        下面定义一个玩具枪类:

        

        如果还是使用原来的继承方式,由于玩具枪是不能射击的,所以此时的shoot方法就“没用”了。这就导致了正常的业务逻辑不能运行了。此时,有两种解决方法:

        1)在Soldier类中增加instanceof的判断,如果是玩具枪就不能用来杀敌人。但是,这种修改每增加一个类就必须修改,所以不提倡。

        2)将ToyGun脱离继承,建立一个独立的父类,并与AbstractGun建立关联委托关系。

        

        如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。 

 (2)子类可以有自己的个性

        还是上面的例子,假设增加AUG狙击枪和狙击手类,此时除了实现shoot方法之外,还可以增加一些自己特有的方法。

        

 

public class AUG extends Rifle {
    public void shoot() {
        System.out.println("AUG射击...");
    }
    public void zoomOut() {
        System.out.println("通过瞄准镜观察敌人...");
    }
}
public class Snipper {
    private AUG aug;
    public void setGun(AUG aug) {
        this.aug = aug;
    }
    public void killEnemy() {
        aug.zoomOut();
        aug.shoot();
    }
}

(3)重写或者实现父类的方法时输入参数可以被放大      

        当我们重载父类中的方法的时候(注意:重载(Overload)父类的方法而不是重写(Override)),必须要满足子类中方法的前置条件(参数)必须与超类中被重载的方法的前置条件相同或者更宽松。

public class Father {
    public Collection doSomething(HashMap map) {
        System.out.println("父类被执行...");
        return map.values();
    }
}
public class Son extends Father {
    //放大输入参数类型
    public Collection doSomething(Map map) {
        System.out.println("子类被执行...");
        return map.values();
    }
}

         最终的结果和使用父类时的打印结果是一样的。

public class Client {
    public static void invoker() {
        //父类存在的地方,子类就应该存在
        //Father f = new Father();
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }    
    public static void mian(String[] args) {
        invoker();
    }
}

//结果:父类被执行...

        除非重写父类中的方法,否则,子类代替父类传递到调用者中,子类的方法永远不会执行。

(4)重写或者实现父类的方法时输出结果可以被缩小

        同理,当我们重载父类中的方法的时候,必须要满足子类中方法的后置条件(返回值)必须与超类中被重载的方法的后置条件相同或者更狭窄。

        总而言之,采用里式替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性即使增加子类,原有的子类还可以继续运行。

三、依赖倒转原则

        依赖倒转原则的英文名称是:Dependence Inversion Principle,简称DIP。它主要有三层含义:

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

        依赖倒转原则在Java语言中的表现就是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。

        对象的依赖关系有三种写法来传递:

(1)构造函数传递依赖对象

(2)Setter方法传递依赖对象

(3)接口声明依赖对象

        如下面这个例子,按照下面这种设计就不符合依赖倒转原则,具体的汽车类应该依赖一个抽象类。

        

         经过修改后的类图如下:

         

四、接口隔离原则

        接口隔离原则定义如下:

(1)客户端不应该依赖它不需要的接口;

(2)类间的依赖关系应该建立在最小的接口上。

        概括为一句话为:建立单一接口,不要建立臃肿庞大的接口。同时,接口尽量细化,同时接口中的方法尽量少。

        例如下面这个例子,定义了一个IPetteyGirl接口,声明所有的PetteyGirl的标准是goodLooking、niceFigure和greatTemperament,然而实际的情况并不是这样,显然这个接口设计的过于庞大了。

        

        修改后的类图如下:

         

        接口隔离原则单一职责原则区别:

  • 单一职责原则要求的是类和接口的职责要一致
  • 接口隔离原则要求接口中的方法尽量少

        接口隔离原则对接口进行规范约束,包含了以下4层含义:

(1)接口要尽量小

        但是“小”是有限度的,首先必须满足单一职责原则

(2)接口要高内聚

        高内聚就是提高接口、类、模块的处理能力,减少对外的交互。

(3)定制服务

        定制服务就是单独为一个个体提供优良的服务,因此在接口设计时要求:只提供访问者需要的方法。

(4)接口的设计是有限度的

        接口的设计粒度越小,系统越灵活。但是,灵活的同时也带来了结构的复杂性,开发难度增加,可维护性降低。

五、迪米特法则

        迪米特法则的英文名称是:Law of Demeter,简称LoD,也称最少知识原则。它的规则是:一个对象应该对其他的对象有最少的了解。通俗的讲,一个类应该对自己需要耦合或调用的类知道得最少。

        如下面这个例子:老师希望体育委员统计女生的数量,在这个场景下,显然老师不需要与女生产生依赖关系,只需要与体育委员依赖即可。而在原来的类的设计中,老师和体育委员与女生同时存在依赖关系,这显然不符合迪米特法则。

        

public class Teacher {
    public void commond(GroupLeader groupLeader) {
        List<Girl> listGirls = new ArrayList();
        //初始化女生
        for(int i = 0; i < 20; i++) {
            listGirls.add(new Girl());
        }
        groupLeader.countGirls(listGirls);
    }
}
public class GroupLeader {
    public void countGirls(List<Girl> listGirls) {
        System.out.println("女生数量是:" + listGirls.size());
    }
}

        根据迪米特法则进行如下修改:

        

public class Teacher {
    public void commond(GroupLeader) {
        groupLeader.countGirls(listGirls);
    }
}
public class GroupLeader {
    private List<Girl> listGirls;
    public GroupLeader(List<Girl> listGirls) {
        this.listGirls = listGirls;
    }
    public void countGirls() {
        System.out.println("女生数量是:" + listGirls.size());
    }
}

        迪米特法则对类的低耦合提出了明确的要求,包含以下4中含义:

(1)“只和朋友交流”

        如果两个对象耦合,那么它们就是“朋友”关系。一个类只和朋友交流,不与陌生类交流。不要出现getA().getB()这种情况,类与类之间的关系是建立在类间的,而不是方法间。因此,一个方法尽量不引入一个类中不存在的对象(JDK API提供的方法除外)。

(2)“朋友间也是有距离的”

        尽量不要对外公布太多public方法和非静态的public变量,多使用private、package-private、protected等访问权限。

(3)“是自己的就是自己的”

        如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。

(4)谨慎使用Serializable

六、开闭原则

        开闭原则是六大设计原则中最基础的设计原则。它的定义是:一个实体(如类、模块或者函数)的设计应该对扩展开放,对修改关闭。

        开闭原则的核心思想面向抽象编程。通过接口或抽象类来约束一组可能变化的行为,从而实现对扩展开放,任何时候我们都不会直接去修改原来类中的内容,从而实现了对修改关闭。

        如下面这个例子,如果我们希望再想实现图书“打折”的效果,我们不应该直接去增加一个setPrice的方法或者再原有的getPrice方法中进行修改,正确的做法是:创建一个打折类,并且这个类继承自NovelBook类,通过重写打折类中的getPrice方法来实现。

        

         

public class OffNovelBook extends NovelBook{

   public OffNovelBook(String name,int price,String author){
      super(name,price,author);
   }

   //覆写价格方法,当价格大于40,就打8析,其他价格就打9析
   public int getPrice(){
     if(this.price > 40){
        return this.price * 0.8;
     }else{
        return this.price * 0.9;
     }     
   } 
}

        开闭原则包含三层含义:

(1)通过接口或抽象类来约束扩展,对扩展进行边界限定,不允许出现在接口或者抽象类中不存在的public方法;

(2)参数类型、引用类型尽量使用接口或者抽象类,而不是实现类;

(3)抽象类尽量保持稳定,一旦确定即不允许修改。

        开闭原则的重要性:

  • 面向对象的要求
  • 提高代码的复用性
  • 提高代码的可维护性
  • 便于进行单元测试

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值