面向对象的六大原则

面向对象六大原则
  1. 单一职责原则--SRP(Single Responsibility Principles)

    • 每个类都有唯一的职责

      设计一个类,首先要明白这个类要去干什么,即为这个类的职责。

      发动机是一个复杂的系统,但是无论它多复杂,它也只有一个职责:输出动力。

  2. 开闭原则--OCP(Open Close Principles)

    • 对扩展开放,对修改关闭

      即设计的类在满足其职责的基础上,在不修改类中代码的情况下,能够良好的扩展.

      单一职责原则和开闭原则一起,构成了设计一个类的指导思想

  3. 里式替换原则--LSP(Liskov Substitution Principles)

    • 所有的基类出现的地方,都能透明的被其子类替换

      里式替换原则是对扩展开放的基础,A依赖于B,那么A就是可以使用B的子类,来实现功能,从而让A具有了扩展性。

      看下面的例子:

    /**
     * 厨师
     */
     public class Chef {
     private Cai cai;
    ​
     public void cook() {
     if (cai == null) {
     System.out.print("我是厨师,我没菜做,我在和Lance吹牛逼...\n");
     } else {
     System.out.print("我是厨师,我在做" + cai.getNameToCook()+"\n");
     }
     }
    ​
     public void setCai(Cai cai){
     this.cai = cai;
     }
     }
    ​
    ​
     /**
     * 菜的父类
     */
     public class Cai {
     public String getNameToCook() {
     return "菜";
     }
     }
    ​
     /**
     * 辣椒炒肉
     */
     public class LaJiaoChaoRou extends Cai {
     public String getNameToCook() {
     return "辣椒炒肉";
     }
     }
    ​
    ​
     /**
     * 西红柿炒蛋
     */
     public class XiHongShiChaoDan extends Cai {
     public String getNameToCook() {
     return "西红柿炒蛋";
     }
     }
    ​
    ​
     @Test
     public void testChef(){
     Chef chef = new Chef();
     chef.cook();
    ​
     Cai cai = new Cai();
     chef.setCai(cai);
     chef.cook();
    ​
     XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
     chef.setCai(xiHongShiChaoDan);
     chef.cook();
    ​
     LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
     chef.setCai(laJiaoChaoRou);
     chef.cook();
     }
    ​
     //打印结果如下
     我是厨师,我没菜做,我在和Lance吹牛逼...
     我是厨师,我在做菜
     我是厨师,我在做西红柿炒蛋
     我是厨师,我在做辣椒炒肉

可以看出,由于里式替换原则的存在,厨师类Chef虽然依赖于Cai这个类,但是它一样可以使用Cai的子类,做出西红柿炒蛋,辣椒炒肉等等。

  1. 依赖倒置原则--DIP(Dependence Inversion Principles)

    • 高层模块不应该依赖低层模块,他们都应该依赖其抽象

    • 抽象不应该依赖细节,细节应该依赖抽象

      这是一个解耦原则,依据该原则对类进行设计,能够使类与被其使用的其他类之间最大限度的解耦。

      什么是抽象? ​ 接口或者抽象类是抽象 什么是细节? ​ 实现类是细节

      在里式替换的列子中,类Chef依赖于类Cai,Chef是高层模块,Cai是低层模块,高层模块直接依赖了低层模块。这样做有什么坏处呢?即Chef的cook()方法必须使用Cai或者Cai的子类,其他类即使有getNameToCook()这个方法,但是也一无法提供给A使用,例如下面的类

   public class CaiBaoZi{
     public String getNameToCook(){
     return "菜包子";
     }
    }

CaiBaoZi这个类虽然有getNameOfFood()方法,但是它不是Cai的子类,无法被Chef使用 也就是说,低层模块Cai限制了高层模块Chef的能力,这就是耦合对一个类带来的伤害。 下面我们对上面的例子做一下修改:

   /**
     * 提供菜名
     */
     public interface IName{
     String getNameToCook();
     }
    ​
     /**
     * 厨师
     */
     public class Chef {
     private IName cookedObject;
    ​
     public void cook() {
     if (cookedObject == null) {
     System.out.print("我是厨师,我没菜做,我在和Lance吹牛逼...\n");
     } else {
     System.out.print("我是厨师,我在做" + cookedObject.getNameToCook()+"\n");
     }
     }
    ​
     public void setCai(IName cookedObjec){
     this.cookedObject = cookedObjec;
     }
     }
    ​
    ​
     /**
     * 菜的父类
     */
     public class Cai implements IName {
     public String getNameToCook() {
     return "菜";
     }
     }
    ​
     /**
     * 辣椒炒肉
     */
     public class LaJiaoChaoRou extends Cai {
     public String getNameToCook() {
     return "辣椒炒肉";
     }
     }
    ​
    ​
     /**
     * 西红柿炒蛋
     */
     public class XiHongShiChaoDan extends Cai {
     public String getNameToCook() {
     return "西红柿炒蛋";
     }
     }
    ​
     /**
     * 菜包子
     */
     public class CaiBaoZi implements IName{
     @Override
     public String getNameToCook() {
     return "菜包子";
     }
     }
    ​
     @Test
     public void testChef(){
           Chef chef = new Chef();
           chef.cook();
    ​
           Cai cai = new Cai();
           chef.setCai(cai);
           chef.cook();
    ​
           XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
           chef.setCai(xiHongShiChaoDan);
           chef.cook();
    ​
           LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
           chef.setCai(laJiaoChaoRou);
           chef.cook();
    ​
           CaiBaoZi caiBaoZi = new CaiBaoZi();
           chef.setCai(caiBaoZi);
           chef.cook();
     }
    ​
     //打印结果如下:
     我是厨师,我没菜做,我在和Lance吹牛逼...
     我是厨师,我在做菜
     我是厨师,我在做西红柿炒蛋
     我是厨师,我在做辣椒炒肉
     我是厨师,我在做菜包子</pre>

通过上面的更改,Chef可以使用CaiBaoZi了,显然CaiBaoZi不是Cai的子类,Chef的扩展性得到更大的发展,对应到依赖倒置原则的定义: Chef是高层模块,是细节 Cai是低层模块,是细节 CaiBaoZi也是低层模块,是细节 IName就是抽象 Chef、Cai、CaiBaoZi都依赖于IName 有了IName这个抽象,A就再也不会受到具体的对象的限制了,只要是实现了IName接口的类,都能够被Chef使用,Chef具有了无限可能,可以说是可以使用任意的类来完成其cook()功能。

那有同学就说了,我有一个第三方SDK提供的类HongJiu:

   public class HongJiu{
       public String getNameOfDrink(){
           return "红酒";
       }
   }

这个类封装在SDK中,我没办法去更改,怎么让被Chef使用呢?我们这么干:

    public class HongJiuForChef implements IName{
           HongJiu hongJiu = new HongJiu();
    ​
           @Override
           public String getNameToCook() {
                 return hongJiu.getNameOfDrink();
           }
     }
    ​
     @Test
     public void testChef() {
           Chef chef = new Chef();
           chef.cook();
    ​
           Cai cai = new Cai();
           chef.setCai(cai);
           chef.cook();
    ​
           XiHongShiChaoDan xiHongShiChaoDan = new XiHongShiChaoDan();
           chef.setCai(xiHongShiChaoDan);
           chef.cook();
    ​
           LaJiaoChaoRou laJiaoChaoRou = new LaJiaoChaoRou();
           chef.setCai(laJiaoChaoRou);
           chef.cook();
    ​
           CaiBaoZi caiBaoZi = new CaiBaoZi();
           chef.setCai(caiBaoZi);
           chef.cook();
    ​
           HongJiuForChef hongJiuForChef = new HongJiuForChef();
           chef.setCai(hongJiuForChef);
           chef.cook();
     }
    ​
     //打印结果
     我是厨师,我没菜做,我在和Lance吹牛逼...
     我是厨师,我在做菜
     我是厨师,我在做西红柿炒蛋
     我是厨师,我在做辣椒炒肉
     我是厨师,我在做菜包子
     我是厨师,我在做红酒</pre>

牛逼了,我们的厨师能够做红酒了,这就是高层模块和低层模块的解耦。

那么问题来了,我怎么知道我要使用一个什么样的抽象呢? 答案是:根据高层模块完成功能H,需要低层模块提供什么样的功能L,要使用的抽象,就是这个功能L的抽象。 拿上面的例子来说,Chef要顺利的完成cook()方法需要什么?需要外界提供一个名字,那么我们对这个需求进行抽象,就抽象出来了一个接口IName,只要实现了这个接口的模块,就都可以被Chef作为低层模块使用了

  1. 接口隔离原则--ISP(Interface Segregation Principles)

    • 类不应该依赖它不需要的接口。

    • 类间的依赖关系,应该建立在最小接口上。

      了解了以上四个原则之后,我们已经能够确定,接口即抽象对一个良好的设计的重要性。而接口隔离原则就是设计一个良好的抽象的指导原则。

看下面的例子:

     public interface B {
           void hear();
           void talk();
           void read();
           void write();
     }
    ​
     public class A implements B{
    ​
           @Override
           public void hear() {
                 System.out.print("A hear");
           }
    ​
           @Override
           public void talk() {
                 System.out.print("A talk");
           }
    ​
           @Override
           public void read() {
                 //A 不需要这个方法
           }
    ​
           @Override
           public void write() {
                 //A 不需要这个方法
           }
     }
    ​
     public class C implements B{
    ​
           @Override
           public void hear() {
                 System.out.print("C hear");
           }
    ​
           @Override
           public void talk() {
                 //C 不需要这个方法
           }
    ​
           @Override
           public void read() {
                 System.out.print("C read");
           }
    ​
           @Override
           public void write() {
                 System.out.print("C write");
           }
     }

可以看出,A被迫实现了它不需要的方法read、write,而C被迫实现它不需要的方法talk。方法就是实现的功能,本来A是没有read和write的功能的,但是由于实现了接口B,它就多出了这两个功能,这是和设计的初衷所不符的,也容易造成使用者的误解,对于后期的必要的修改也会造成干扰,例如我要对A增加一项功能run,如果将该功能抽象到B接口中,那么C也会被迫跟着修改,这显然不是一个良好的设计。

下面是修改后的设计:

     public interface B {
           void hear();
     }
    ​
     public interface B2A extends B{
           void talk();
           void run();
     }

     public interface B2C extends B{
           void read();
           void write();
     }

     public class A implements B2A{
    ​
           @Override
           public void hear() {
                 System.out.print("A hear");
           }
    ​
           @Override
           public void talk() {
                 System.out.print("A talk");
           }
    ​
           @Override
           public void run() {
                 System.out.print("A run");
           }
     }
    ​
     public class C implements B2C{
    ​
           @Override
           public void hear() {
                  System.out.print("C hear");
           }
    ​
           @Override
           public void read() {
                  System.out.print("C read");
           }  
    ​
           @Override
           public void write() {
                  System.out.print("C write");
           }

     }

修改之后,将接口进行了拆分隔离,再需要修改的时候,公共方法就在接口B中修改,其他的根据需要分别在接口B2A和接口B2C中修改,再也不会对其他的类造成不必要的影响;虽然接口的数量增加了,但是无论是对于以后代码的维护拓展,还是对于使用者的理解,都是非常友好的。

当然,对于最小接口的设计要注意把控好度,拆分的粒度不够细腻,就会维护困难,拆分的太细又会大量的增加接口数量,也会造成困扰。

  1. 迪米特原则(最少知识原则)--LOD(Law of Demeter)

    • 一个对象,应该对其他的对象有最少的了解。

    • 维基百科上定义如下: 得墨忒耳定律Law of Demeter,缩写LoD)亦称为“最少知识原则(Principle of Least Knowledge)”,是一种软件开发的设计指导原则,特别是面向对象的程序设计。得墨忒耳定律是松耦合的一种具体案例。该原则是美国东北大学在1987年末在发明的,可以简单地以下面任一种方式总结:

      1. 每个单元对于其他的单元只能拥有有限的知识:只是与当前单元紧密联系的单元;

      2. 每个单元只能和它的朋友交谈:不能和陌生单元交谈;

      3. 只和自己直接的朋友交谈。

      这个原理的名称来源于希腊神话中的农业女神,孤独的得墨忒耳

      很多面向对象程序设计语言用"."表示对象的域的解析算符,因此得墨忒耳定律可以简单地陈述为“只使用一个.算符”。因此,a.b.Method()违反了此定律,而a.Method()不违反此定律。一个简单例子是,人可以命令一条狗行走(walk),但是不应该直接指挥狗的腿行走,应该由狗去指挥控制它的腿如何行走。

    • 百度百科上有如下描述:

      狭义的迪米特法则

      如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

      朋友圈的确定

      “朋友”条件:

      1)当前对象本身(this)

      2)以参量形式传入到当前对象方法中的对象

      3)当前对象的实例变量直接引用的对象

      4)当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友

      5)当前对象所创建的对象

      任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。

      狭义的迪米特法则的缺点:

      在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。

      遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

      门面模式调停者模式实际上就是迪米特法则的应用。

      广义的迪米特法则在类的设计上的体现:

      优先考虑将一个类设置成不变类。

      尽量降低一个类的访问权限。

      谨慎使用Serializable。

      尽量降低成员的访问权限。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342