JAVA初探设计模式的六大原则

前言

我想用贴近生活的语句描述一下自己对六种原则的理解。也就是不做专业性的阐述,而是描述一种自己学习后的理解和感受,因为能力一般而且水平有限,也许举的例子不尽妥当,还请谅解原本我是想用JavaScript编写的,但是JavaScript到现在还没有提出接口的概念,而用TypeScript写又感觉普及度还不算特别高,所以还是决定用Java语言编写

首先要提的是:六大原则的灵魂是面向接口,以及如何合理地运用接口

P1.单一职责原则(Single Responsibility Principle)

应该有且仅有一个原因引起类的变更(There should never be more than one reason for a class to change)。为了达到这个目标,我们需要对类和业务逻辑进行拆分。划分到合适的粒度,让这些各自执行单一职责的类,各司其职。让每个类尽量行使单一的功能,实现“高内聚”,这个结果也使得类和类之间不会有过多冗余的联系,从而“低耦合”。比如我们现在有了这样一个类

public class People {
    public void playCnBlogs () {
        System.out.println("刷博客");
    }
    public void doSports () {
        System.out.println("打乒乓球");
    }
    public void work () {
        System.out.println("工作");
    }
}

现在看起来有点混乱,因为这个类里面混合了三个职责:

  • 刷博客园,这是博主的职责
  • 打乒乓球,这是业余运动爱好者的职责
  • 工作,这是“普普通通上班族”的职责

OK,正如你所见,既然我们要遵循单一职责,那么怎么做呢?当然是要拆分了我们要根据接口去拆,拆分成三个接口去约束People类(不是把People类拆了哈)

// 知乎er
public interface Blogger {
    public void playCnBlogs();
}
// 上班族
public interface OfficeWorkers {
    public void work();
}
// 业余运动爱好者
public interface AmateurPlayer {
    public void doSports();
}

然后在People中继承这几个接口

public class People implements Blogger,AmateurPlayer,OfficeWorkers{
    public void playCnBlogs () {
        System.out.println("刷博客园");
    }
    public void doSports () {
        System.out.println("打乒乓球");
    }
    public void work () {
        System.out.println("工作");
    }
}

最后创建实例运行一下

public class Index {
    public static  void main (String args []) {
        People people = new People();
        Blogger blogger = new People();
        blogger.playCnBlogs(); // 输出:刷博客园
        OfficeWorkers workers = new People();
        workers.work(); // 输出: 工作
        AmateurPlayer players = new People();
        players.doSports(); // 输出:打乒乓球
    }
}

备注:这个原则不是死的,而是活的,在实际开发中当然还要和业务相结合,不会纯粹为了理论贯彻单一职责,就像数据库开发时候,不会完全遵循“三大范式”,而是允许一定冗余的

P2.里氏替换原则(liskov substitution principle)

里氏替换原则,一种比较好的理解方式是: 所有引用基类的地方必须能透明地使用其子类的对象。 换句话说,子类必须完全实现父类的功能。凡是父类出现的地方,就算完全替换成子类也不会有什么问题。以上描述来自《设计模式之禅》,刚开始看的时候我有些疑惑,因为一开始觉得:只要继承了父类不都可以调用父类的方法吗?为什么还会有里氏替换所要求的:子类必须完全实现父类的功能呢, 难不成继承的子类还可以主动“消除”父类的方法?还真可以,请看

父类

public abstract class Father {
    // 认真工作
    public abstract void work();
    // 其他方法
}

子类

public class Son extends Father {
    @Override
    public void work() {
     // 我实现了爸爸的work方法,旦我什么也不做!
    }
}

子类虽然表面上实现了父类的方法,但是他实际上并没有实现父类要求的逻辑。里氏替换原则要求我们避免这种“塑料父子情”,如果出现子类不得不脱离父类方法范围的情况, 采取其他方式处理,详情参考《设计模式之禅》

(其实个人觉得《禅》的作者其实讲的“父类”其实着重指的是抽象类)

P3.依赖倒置原则 (dependence inversion principle)

很多文章阐述依赖倒置原则都会阐述为三个方面

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

换句话说, 高层次的类不应该依赖于,或耦合于低层次的类,相反,这两者都应该通过相关的接口去实现。要面向接口编程,而不是面向实现编程,所以编程的时候并不是按照符合我们逻辑思考的“依赖关系”去编程掉的,这种不符,就是依赖倒置举个例子,类好比是道德,接口好比是法律。道德呢,有上层的也有下层的,春秋时代,孔圣人提出了上层道德理论:“仁”的思想,并进一步细化为低层道德理论:“三纲五常”(高层模块和底层模块),想要以此规约众生,实现天下大同。可是奈何民众的道德终究还是靠不住(没有接口约束的类,可能被混乱修改),何况道德标准是会随物质经济的变化而变化的,孔子时代和我们今天的已经大有不同了。(类可能会发生变化)所以才需要法律来进一步框定和要求道德。(我们用接口来约束和维护“类”,就好比用法律来维护和规约道德一样。)假如未来道德伦理的标杆发生了变化,肯定是先修缮法律,然后再次反向规制和落实道德(面向接口编程,而不是面向实现编程)。我们看下下面没有遵循依赖倒置原则的代码是怎样的,我们设计了两个类:Coder类和Linux类,并且让它们之间产生交互:Coder对象的develop方法接收Linux对象并且输出系统名

// 底层模块1:开发者
public class Coder {
    public void develop (Linux linux) {
        System.out.printf("开发者正在%s系统上进行开发%n",linux.getSystemName());
    }
}
// 底层模块2:Linux操作系统
public class Linux {
    public String name;
    public Linux(String name){
        this.name = name;
    }
    public String getSystemName () {
        return this.name;
    }
}
// 高层模块
public class Index {
    public static  void main (String args []) {
        Coder coder = new Coder();
        Linux ubuntu = new Linux("ubuntu系统"); // ubuntu是一种linux操作系统
        coder.develop(ubuntu);
    }
}

输出

开发者正在ubuntu系统系统上进行开发 

但是我们能发现其中的问题:

操作系统不仅仅有Linux家族,还有Windows家族,如果我们现在需要让开发者在windows系统上写代码怎么办呢? 我们可能要新建一个Windows类,但是问题来了,Code.develop方法的入参数类型是Linux,这样以来改造就变得很麻烦。让我们利用依赖倒置原则改造一下,我们定义OperatingSystem接口,将windows/Linux抽象成操作系统,这样,OperatingSystem类型的入参就可以接收Windows或者Linux类型的参数了

// 程序员接口
public interface Programmer {
    public void develop (OperatingSystem OS);
}
// 操作系统接口
public interface OperatingSystem {
    public String getSystemName ();
}
// 低层模块:Linux操作系统
public class Linux implements  OperatingSystem{
    public String name;
    public Linux (String name) {
        this.name = name;
    }
    @Override
    public String getSystemName() {
        return this.name;
    }
}
// 低层模块:Window操作系统
public class Window implements OperatingSystem {
    String name;
    public Window (String name) {
        this.name = name;
    }
    @Override
    public String getSystemName() {
        return this.name;
    }
}
// 低层模块:开发者
public class Coder implements Programmer{
    @Override
    public void develop(OperatingSystem OS) {
        System.out.printf("开发者正在%s系统上进行开发%n",OS.getSystemName());
    }
}
// 高层模块:测试用
public class Index {
    public static  void main (String args []) {
        Programmer coder = new Coder();
        OperatingSystem ubuntu = new Linux("ubuntu系统"); // ubuntu是一种linux操作系统
        OperatingSystem windows10 = new Window("windows10系统"); // windows10
        coder.develop(ubuntu);
        coder.develop(windows10);
    }
}

虽然接口的加入让代码多了一些,但是现在扩展性变得良好多了,即使有新的操作系统加入进来,Coder.develop也能处理

P4. 接口隔离原则(interface segregation principle)

接口隔离原则的要求是:类间的依赖关系应该建立在最小的接口上。这个原则又具体分为两点

1.接口要足够细化,当然了,这会让接口的数量变多,但是每个接口会具有更加明确的功能

2.在1的前提下,类应该依赖于“最小”的接口上

举个例子,中秋节其实只过了一个多月,现在假设你有一大盒“五仁月饼”想带回家喂猪,但是无奈的是包包太小放不下,而且一盒沉重的月饼对瘦弱的你是个沉重的负担。这个时候,我们可以把月饼盒子拆开,选出一部分自己需要(wei zhu)的月饼,放进包包里就好啦,既轻便又灵活。还是上代码吧,比如我们有这样一个Blogger的接口,里面涵盖了一些可能的行为。大多数博客用户会保持友善,同时根据自己的专业知识认真写文章。但也有少数的人会把生活中的负面能量带到网络中

public interface Blogger {
    // 认真撰文
    public void seriouslyWrite();
    // 友好评论
    public void friendlyComment();
    // 无脑抬杠
    public void argue();
    // 键盘攻击
    public void keyboardAttack ();
}

我们发现,这个接口可以进一步拆分成两个接口,分别命名为PositiveBlogger,NegativeBlogger。这样,我们就把接口细化到了一个合理的范围

public interface PositiveBlogger {
    // 认真撰文
    public void seriouslyWrite();
    // 友好评论
    public void friendlyComment();
}

public interface NegativeBlogger {
    // 无脑抬杠
    public void argue();
    // 键盘攻击
    public void keyboardAttack ();
}

>> 备注:妥善处理 单一职责原则 和 接口隔离原则的关系事实上,有两点要说明一下

1.单一职责原则和接口隔离原则虽然看起来有点像,好像都是拆分,但是其实侧重点是不一样的,“职责”的粒度其实是比“隔离接口”的粒度要大的

2.基于1中阐述的原因,其实 单一职责原则 和 接口隔离原则是可能会产生冲突的,因为接口隔离原则要求粒度尽可能要细,但是单一职责原则却不同,它要求拆分既不能过粗,但也不能过细,如果把原本单一职责的接口分成了“两个0.5职责的接口”,那么这就是单一职责所不能允许的了。

3.当两者冲突时,优先遵循 单一职责原则

P5.迪米特原则 (law of demeter)

迪米特原则又叫最少知道原则,在实现功能的前提下,一个对象接触的其他对象应该尽可能少,也即类和类之间的耦合度要低。举个例子,我们经常说要“减少无效社交”,不要总是一昧的以交朋友的数量衡量自己的交际能力,否则会让自己很累的,也会难以打理好复杂的人际关系。对于并不很外向的人,多数时候和自己有交集的朋友交往就可以了。我们看下代码:有如下场景,现在你和你的朋友想要玩一个活动,也许是斗地主等游戏,这个时候需要再喊一个人,于是你让你的朋友帮你再叫一个人,有代码如下

// 我的直接朋友
public class MyFriend {
    // 找他的朋友
    public void findHisFriend (FriendOfMyFriend fof) {
      System.out.println("这是朋友的朋友:"+ fof.name);
    }
}

// 朋友的朋友,但不是我的朋友
public class FriendOfMyFriend {
    public String name;
    public FriendOfMyFriend(String name) {
      this.name = name;
    }
}

// 我
public class Me {
    public void findFriend (MyFriend myFriend) {
      System.out.println("我找我朋友");
      // 注意这段代码
      FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");
      myFriend.findHisFriend(fmf);
    };
}

这时我们发现一个问题,你和你朋友的朋友并不认识,但是他却出现在了你的“找朋友”的动作当中(在findFriend方法内),这个时候,我们认为这违反了迪米特原则(最少知道原则),迪米特原则我们对于对象关系的处理,要减少“无效社交”,具体原则是

  • 一个类只和朋友类交流,朋友类指的是出现在成员变量、方法的输入输出参数中的类
  • 一个类不和陌生类交流,即没有出现在成员变量、方法的输入输出参数中的类

所谓的“不交流”,就是不要在代码里看到他们我们改造一下上面的代码

// 我朋友
public class MyFriend {
    public void findHisFriend () {
        FriendOfMyFriend fmf = new FriendOfMyFriend("陌生人");
        System.out.println("这是朋友的朋友:"+ fmf.name);
    }
}
// 朋友的朋友,但不是我的朋友
public class FriendOfMyFriend {
    public String name;
    public FriendOfMyFriend(String name) {
        this.name = name;
    }
}

// 我
public class Me {
    public void findFriend (MyFriend myFriend) {
        System.out.println("我找我朋友");
        myFriend.findHisFriend();
    };
}

P6. 开闭原则(open closed principle)

开闭原则的意思是,软件架构要:对修改封闭,对扩展开放举个例子比如我们现在在玩某一款喜欢的游戏,A键攻击,F键闪现。这个时候我们想,如果游戏能额外给我定制一款“K”键,残血时解锁从而一击OK对手完成5杀,那岂不美哉,这就好比是“对扩展开放”。但是呢,如果游戏突然搞个活动,把闪现/攻击/技能释放的键盘通通换个位置,给你一个“双十一的惊喜”,这恐怕就给人带来惨痛的回忆了。所以我们希望已有的结构不要动,也不能动,要“对修改封闭”(本人不玩游戏,这些是自己查到的,如果错误还请指正)

总结

1.原则不是死板的而是灵活的

2.一些原则其实是存在一定的冲突的,重要的是权衡,是掌握好度

3.六大原则是23种设计模式的灵魂,六大原则指导了设计模式,设计模式体现了六大原则

以上就是JAVA初探设计模式的六大原则的详细内容,更多关于JAVA设计模式六大原则的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java设计模式之代理模式详解

    一.代理模式 代理模式就是有一个张三,别人都没有办法找到他,只有他的秘书可以找到他.那其他人想和张三交互,只能通过他的秘书来进行转达交互.这个秘书就是代理者,他代理张三. 再看看另一个例子:卖房子 卖房子的步骤: 1.找买家 2.谈价钱 3.签合同 4.和房产局签订一些乱七八糟转让协议 一般卖家只在签合同的时候可能出面一下,其他的1,2,4都由中介去做.那你问这样有什么用呢? 首先,一个中介可以代理多个卖房子的卖家,其次,我们可以在不修改卖家的代码的情况下,给他实现房子加价.打广告等等夹带私货的

  • 详解java设计模式之六大原则

    一.单一职责原则 1.单一职责定义 单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因. 单一职责原则告诉我们:一个类不能太"累"!在软件系统中,一个类承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责 变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封

  • Java设计模式之命令模式详解

    命令模式 定义:将请求封装成对象,这可以让你使用不同的请求.队列.或者日志来参数化其他对象. 何时使用命令模式?当需要将发出请求的对象和执行请求的对象解耦的时候,使用命令模式. 在被解耦的两者之间是通过命令对象进行沟通的.命令对象封装了接收者和一个或一组动作. 调用者通过调用命令对象的execute()方法发出请求,这会使接收者的动作被调用. 调用者可以接收命令当作参数,甚至在运行时动态地进行. 优点: 1.降低了系统耦合度. 2.新的命令可以很容易添加到系统中去. 缺点:使用命令模式可能会导致

  • Java通俗易懂系列设计模式之装饰模式

    介绍 装饰模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式.就功能而言,装饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能. 意图:动态地给一个对象添加一些额外的职责和增加功能. 主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀. 何时使用:在不想增加很多子类的情况下扩展类. 如何解决:将具体功能职责划分,同时继承装饰者模式. 关键代码: 1.Component 类充当抽象角色,不

  • Java设计模式之抽象工厂模式详解

    一.什么是抽象工厂模式 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类,这称之为抽象工厂模式(Abstract Factory).我们并不关心零件的具体实现,而是只关心接口(API).我们仅使用该接口(API)将零件组装称为产品. 二.示例程序   1.抽象的零件:Item类 package com.as.module.abstractfactory; /** * 抽象的零件 * @author Andy * @date 2021/4/29 23:16 */ public

  • Java通俗易懂系列设计模式之策略模式

    介绍 策略设计模式是行为设计模式之一.当我们为特定任务使用多个算法时,使用策略模式,客户端决定在运行时使用的实际实现. 策略模式的最佳示例之一是Collections.sort()采用Comparator参数的方法.基于Comparator接口的不同实现,对象将以不同的方式进行排序. 实例 对于我们的示例,我们将尝试实施一个简单的购物车,我们有两种付款策略 - 使用信用卡或使用PayPal. 首先,我们将为我们的策略模式示例创建接口,在我们的例子中,支付金额作为参数传递. 支付方式:Paymen

  • Java通俗易懂系列设计模式之观察者模式

    介绍 观察者模式是行为设计模式之一.当您对对象的状态感兴趣并希望在有任何更改时收到通知时,观察者设计模式非常有用.在观察者模式中,监视另一个对象状态的对象称为Observer,正在被监视的对象称为Subject. 根据GoF,观察者设计模式的意图是; 定义对象之间的一对多依赖关系,以便当一个对象更改状态时,将自动通知和更新其所有依赖项. Subject包含一个观察者列表,用于通知其状态的任何变化,因此它应该提供观察者可以注册和注销自己的方法.Subject还包含一种方法,用于通知所有观察者任何更

  • Java设计模式之单例模式简介

    一.饿汉式(静态常量) public class Face { private static final Face instance = new Face(); static Face getInstance() { return instance; } private Face() { } } 优点:这种写法比较简单,就是在类装载的时候就完成实例化.避免了线程同步问题. 缺点:在类装载的时候就完成实例化,没有懒加载的效果.如果从始至终从未使用过这个实例,则会造成内存的浪费. 二.饿汉式(静态代

  • Java设计模式之原型模式详解

    一.前言 原型模式是一种比较简单的模式,也非常容易理解,实现一个接口,重写一个方法即完成了原型模式.在实际应用中,原型模式很少单独出现.经常与其他模式混用,他的原型类Prototype也常用抽象类来替代. 该模式的思想就是将一个对象作为原型,对其进行复制.克隆,产生一个和原对象类似的新对象.在Java中,复制对象是通过clone()实现的,先创建一个原型类,通过实现Cloneable 接口 public class Prototype implements Cloneable { public

  • 详解java构建者模式Builder

    定义 Builder模式是一步步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细的控制对象的构建过程.该模式是将构建复杂对象的过程和它的部件解耦,使得构建过程和部件的表示隔离. 作为复杂对象可能有很多组成部分,比如汽车有车轮.方向盘.发动机.变速箱还有各种小零件等,如何将这些部件组装成一台汽车,这个装配的过程漫长且复杂,对于这种情况,为了对外部隐藏实现细节,就可以使用Builder模式将部件和组装过程分离,使得构建过程和部件分离可自由扩展,两者之间的耦合也降到最低

  • Java设计模式之构建者模式知识总结

    一.构建者模式 1.1 定义 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示 1.2 主要作用 在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象. 用户只需要给出指定复杂对象的类型和内容: 建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来) 1.3 解决的问题 方便用户创建复杂的对象(不需要知道实现过程) 代码复用性 & 封装性(将对象构建过程和细节进行封装 & 复用) 例子:造汽车 & 买汽车. 工厂(建造者模式):负责制

  • Java设计模式之桥梁(Bridge)模式

    桥梁模式的结构 桥梁模式是对象的结构模式. 如上图所示,系统含有两个等级结构: 由抽象化角色和修正抽象化角色组成的抽象化等级结构. 由实现化角色和两个具体实现化角色所组成的实现化等级结构. 桥梁模式所涉及的角色有: 抽象化(Abstraction)角色:抽象化给出的定义,并保存一个对实现化对象的引用. 修正抽象(Refined Abstraction)化角色:扩展抽象化角色,改变和修正父类对抽象化的定义. 实现化(Implementor)角色:这个角色给出实现化角色的接口,但不给出具体的实现.必

随机推荐