设计原则-Java

在讲设计原则之前,我先强制灌输大家一波鸡汤,提倡
面向接口编程,代码的设计更重要的是考虑以后的扩展和可维护性
大家带着这样的思维来学习设计模式以及设计原则,慢慢就意会这波毒鸡汤了。
先声明一点就是老衲的blog,也是边学习,边记录,而后以讨论交流的方式叙述,有什么不对的地方大家多多担待。

创建型-抽象工厂(Abstract Factory)

创建型-单例(Singleton)

创建型-建造者(Builder)

创建型-工厂方法(Factory Method)

创建型-原型(Prototype)

行为型-策略(Strategy)

行为型-代理(Proxy)

行为型-观察者(Observer)

行为型-责任链(Chain of Responsibility)

行为型-中介者(Mediator)

结构型-桥接(bridge)

结构型-适配器(Adapter)

设计原则

单一职责原则(Single Responsibility Principle, 简称是SRP)

老衲是一位粗鄙之人,所以描述也是尽可能的白话哈~

定义

白话单一职责:首先,顾名思义,什么是单一职责?就是某玩意,专门负责某个东西,就譬如说,你的手机屏幕,他就只负责显示,不管显示app还是视频还是小黄书,只要是显示的活,他就干,而且他就只干显示的活,这就是单一职责,那么放到术语里面就是咱们设计的接口或者类,尽量遵循此原则,有什么好处赖?

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

这一波好处摘自设计模式之禅,总结下来就是,看见这个接口所声明的方法,你就知道功能都有什么,初学者或者初接手的人都可以很快融入到代码中进行迭代和维护了。

然而道理是这个道理,但是具体在设计代码的时候,还是要考虑到具体的应用下。用书中的话描述就是
单一职责原则提出了一个编写程序的标准, 用“职责”或“变化原因”来衡量接口或 类设计得是否优良, 但是“职责”和“变化原因”都是不可度量的, 因项目而异, 因环境而异

code

OK,结合上面说的小Demo,接下来来一杯Java解解渴

interface IScreenDisplay {
    /**
     * display image on screen
     *
     * @param image
     */
    void displayImage(String image);

    /**
     * display a text on screen
     *
     * @param text
     */
    void displayText(String text);

    /**
     * display a video on screen
     *
     * @param video
     */
    void displayVideo(String video);
}

上来就是我们的小接口,屏幕显示,干什么玩意呢?自行翻译不谢~

然后是我们的实现类

static class Phone implements IScreenDisplay {

    @Override
    public void displayImage(String image) {
        System.out.println("displayImage:" + image);
    }

    @Override
    public void displayText(String text) {
        System.out.println("displayText:" + text);
    }

    @Override
    public void displayVideo(String video) {
        System.out.println("displayVideo:" + video);
    }
}

实现类就是干具体的活了,国际惯例sout输出~

public static void main(String[] args) {
    IScreenDisplay phone = new Phone();
    phone.displayImage("ic_launcher.png");
    phone.displayText("Hello Done!");
    phone.displayVideo("xiao huang pian.avi");
}

这一套降龙十八掌走下来结果:

displayImage:ic_launcher.png
displayText:Hello Done!
displayVideo:xiao huang pian.avi

多嚣张?多简单?当后期review一看,咱们的接口告诉你只负责显示,可以显示文字,图片和视频,至于什么时候显示,显示什么内容,那我不管,我就显示,唯一让我引起变化的是什么?当然是内容咯~

总结一番便是

接口一定要做到单一职责, 类的设计尽量做到只有一个原因引起变化

里氏替换原则(LiskovSubstitution Principle, LSP)

此原则相较于上面的单一职责,要复杂一些,这里引用书中的原话(一定要认真阅读),后面会通过白话做出相关解释哈~

首先要理解的是继承的特点

  • 代码共享, 减少创建类的工作量, 每个子类都拥有父类的方法和属性
  • 提高代码的重用性
  • 子类可以形似父类,但又异于父类, “龙生龙, 凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的
  • 提高产品或项目的开放性

上面是优点,下面是缺点

  • 继承是侵入性的。 只要继承, 就必须拥有父类的所有属性和方法
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束
  • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构

定义

  • 看不懂,绕口定义:

If for each object o1 of type S there is an object o2 oftype T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 issubstituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1, 都有类型为T的对象o2, 使得以T定义的所有程序P在所有的对象o1都代换成o2时, 程序P的行为没有发生变
化, 那么类型S是类型T的子类型。 )

  • 通俗易懂,亲民定义:

Functions that use pointers or references to base classes must be able to useobjects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。 )

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

里氏替换原则为良好的继承定义了一个规范,一句简单的定义包含了4层含义

  • 子类必须完全实现父类的方法

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

  • 子类可以有自己的个性

向下转型(downcast)是不安全的, 从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现

  • 覆盖或实现父类的方法时输入参数可以被放大

里氏替换原则也要求制定一个契约, 就是父类或接口,这种设计方法也叫做Design by Contract(契约设计) ,与里氏替换原则有着异曲同工之妙。 契约制定了, 也就同时制定了前置条件和后置条件, 前置条件就是你要让我执行,就必须满足我的条件; 后置条件就是我执行完了需要反馈, 标准是什么。

  • 覆写或实现父类的方法时输出结果可以被缩小

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑

白话方式总结一下上面的内容:
里氏替换原则更像是一种java继承的规范用法,"extends"嘛,大家都用过的东西,可以这么用那么用随便用,想用就用,但是写完以后发现没有卵用,用是用了,但是有什么奥妙还是不太清楚,为什么要用它?
咱们经常谈到的一个词:抽取,譬如说activity,这个活动要setContentView,那个活动也要setContentView,这个活动要上下文,那个活动也要上下文,诶~这时候我们就开始搞一个BaseActivity的东西,然后让所有子类去重写获取布局ID,同时父类直接拿到自己的上下文对象,子类直接使用即可。

那么是不是我们的活动程序代码逻辑无论ams(自行百度)怎么使,你创建的这个活动都好使,符合咱们android体系的健壮性,另外你想实现的黑科技是不是都可以在自己的活动里面去实现,当然这里的设计并不是完全符合里氏替换原则,姑且断章取义,您就这么断章取义的理解即可,另外如果老衲说的有什么不对的地方,欢迎大家指正批评~

还记得上面提到过的Java中继承带来的优缺点吗?咱们的里氏替换原则就是一个“扬长避短”的做法,具体怎么搞?code一下见分晓

code

还是手机的例子,这次咱们不说屏幕,说品牌,先来个抽象手机

abstract class AbsPhone {
    /**
     * 使用
     */
    public abstract void use();
}

卧槽,就那么简单,抽象一个使用的方法~
OK,接下来是子类们

/**
 * 诺基亚
 */
class NokiaPhone extends AbsPhone {

    @Override
    public void use() {
        System.out.println("砸核桃 实用户");
    }
}

/**
 * 锤子
 */
class TPhone extends AbsPhone {

    @Override
    public void use() {
        System.out.println("锤子 情怀户");
    }
}

/**
 * 苹果
 */
class ApplePhone extends AbsPhone {

    private void ringing() {
        System.out.println("先让苹果特有铃声响一阵...呜呜呜");
    }

    @Override
    public void use() {
        ringing();
        System.out.println("苹果 zhuang bi 专业户");
    }
}

很简单,就是诺基亚,锤子和苹果三个街机,这里并没有任何对这些品牌的观点,这是假借名义,传输知识用
再然后必须有一个使用者嘛

class Person {

    AbsPhone phone;
    String name;

    public Person(String name) {
        this.name = name;
    }

    public void setPhone(AbsPhone phone) {
        this.phone = phone;
    }

    void communicate() {
        System.out.println(name + "掏出手机了...");
        phone.use();
    }
}

然后我们来用这些兄dei跑一把

public static void main(String[] args) {
    Person coke = new Person("库克");
    coke.setPhone(new ApplePhone());

    Person laoluo = new Person("老罗");
    laoluo.setPhone(new TPhone());

    Person bill = new Person("比尔盖茨");
    bill.setPhone(new NokiaPhone());

    coke.communicate();
    laoluo.communicate();
    bill.communicate();
}

sout

库克掏出手机了...
先让苹果特有铃声响一阵...呜呜呜
苹果 zhuang bi 专业户
老罗掏出手机了...
锤子 情怀户
比尔盖茨掏出手机了...
砸核桃 实用户

爽哉,爽在哪里了?咱们的person对象只知道自己有个手机,这个手机能执行communicate操作,什么手机我不管,我只管用它。术语就是,逻辑代码不管实现,只需要持有着抽象类,然后执行抽象类提供的方法即可,就算以后扩展了其他的子类,也不影响我之前的业务逻辑,子类完全继承了父类,同时在不改变方法本身的逻辑下增添了自己的特色,同时也符合父类出现的地方,就可以替换成子类

请大家仔细咀嚼下面这两段话:
采用里氏替换原则的目的就是增强程序的健壮性, 版本升级时也可以保持非常好的兼容性。 即使增加子类, 原有的子类还可以继续运行。 在实际项目中, 每个子类对应不同的业务含义, 使用父类作为参数, 传递不同的子类完成不同的业务逻辑

对于基类中定义的所有子程序,用在它的任何一个派生类中时的含义都应该是相同的。这样继承才不会增加复杂度,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。如果我们必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了。

本着负责人的态度,老衲还是把树立的这段话copy过来,望大家酌情使用参考:

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准

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

依赖倒置是什么鬼?纯说感觉也说不明白,白话点来说就是各种注入,依赖的接口注入,就是咱们的接口不依赖实现,而具体的实现类去组装这些接口,简单粗暴的解释就是,
面向接口编程(OOD)
祭出官方释义就是:
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

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

定义

抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字new产生一个对象

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

那么采用依赖倒置的优势在哪里?就是

减少类间的耦合性, 提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”

那么要如何遵循依赖倒置原则去设计代码呢?首先,我们的始终遵循

  • 抽象不依赖细节
  • 在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降到最低
  • 如果两个类直接存在依赖关系,那么连接他们之间的桥梁就是接口,不依赖具体的低层模块
  • 抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到“言必信, 行必果”
  • 常用依赖传递,只要做到抽象依赖,即使是多层的依赖传递也无所畏惧

code

依赖倒置原则的本质就是通过抽象(接口或抽象类) 使各个类或模块的实现彼此独立,
不互相影响, 实现模块间的松耦合

  • 每个类尽量都有接口或抽象类, 或者抽象类和接口两者都具备
    这是依赖倒置的基本要求, 接口和抽象类都是属于抽象的, 有了抽象才可能依赖倒置。
  • 变量的表面类型尽量是接口或者是抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要覆写基类的方法(类间依赖的是抽象, 覆写了抽象方法, 对依赖的稳定性会产生一定的影响)
  • 结合里氏替换原则使用(接口负责定义public属性和方法, 并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑, 同时在适当的时候对父类进行细化)
    说了那么多,还是需要深刻的在代码中多多运用“面向接口编程”

OK,接下来是我们的代码背景,还是不要上面的手机例子了, 老衲也是写吐了哈哈- -,这次是英雄联盟,恭喜RNG!
主角是咱们的Uzi和香锅打野
首先声明英雄和召唤师的接口

interface IHero {

    void attack();

}

interface IPlay {

    void play();
    
}

紧接着是咱们的两个英雄低层接口

static class Xiazi implements IHero {

    @Override
    public void attack() {
        System.out.println("瞎子,我用双手,成就你的梦想");
    }
}

static class VN implements IHero {

    @Override
    public void attack() {
        System.out.println("VN,黑夜也会怕我");
    }
}

然后是咱们的上层player接口

static class ADPlayer implements IPlay {

    IHero hero;

    public ADPlayer(IHero hero) {
        this.hero = hero;
    }

    @Override
    public void play() {
        hero.attack();
    }
}

static class AssistPlayer implements IPlay {

    IHero hero;

    public void setHero(IHero hero) {
        this.hero = hero;
    }

    @Override
    public void play() {
        hero.attack();
    }
}

ok,接下来爽一把
这里注意,咱们的uzi使用构造依赖注入方式,天生的AD,世界第一ADC
咱们的RNG围绕下路战术,所以香锅就一个使命,保护下路,使用setter依赖注入

public static void main(String[] args) {
    ADPlayer uzi = new ADPlayer(new VN());
    AssistPlayer mlxg = new AssistPlayer();
    mlxg.setHero(new Xiazi());
    uzi.play();
    mlxg.play();
}


输出
VN,黑夜也会怕我
瞎子,我用双手,成就你的梦想

再次恭喜RNG集中赛冠军,不知为何,老衲看小花生就是一脸不爽

接口隔离原则(Interface Splite Principle)

接口 隔离
其实从字面上就能很好的理解,先不看书,简单从字面上理解一下这个原则

接口:interface(Java 中interface关键字修饰,只能在其中声明方法/接口和静态变量)
类:class,对外提供的public方法,从外向内看,这其实也是一种接口

隔离:隔离结合单一职责来看,隔离的基础首先尽可能保证接口的定义符合单一职责原则,依据业务划分出来的接口功能进行进一步进行拆分细分,类不要去依赖那些他用不到的接口,不然没有意义啊,依赖那么多搞什么,说白了就是对接口根据依赖关系进行一波“抽取”的骚操作

定义

ok,山寨白话解释完毕,下面来对下文档,接口描述正确,我们就看一下隔离的解释

  • 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.(类间的依赖关系应该建立在最小的接口上。)

好吧,在下认为这个解释还不如白话来的直接明了,类间的依赖关系是什么?其实对于接口最直接的定义便是类之间进行通信使用的,那么既然他们之间进行通信,那么两个类之间就存在了耦合关系,耦合达到最低要怎么做?就是尽量使这个耦合接口简单明了,那么搬出书上的解释:
建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少

code

代码背景还是咱们LOL,之前是hero,那今天就换成NPC吧
首先来两个接口,分别是魔法攻击和物理攻击,也就是咱们的接口隔离

public interface IMagicAttack {
        void magicAttack();
    }

public interface IPhysicalAttack {
    void physicalAttack();
}

接下来是两个咱们的法拉利和远程兵,分别实现魔法和物理攻击接口

public static class YuanChenBing implements IMagicAttack {

        @Override
        public void magicAttack() {
            System.out.println("远程兵,用魔法攻击轰你家大灯");
        }
    }

public static class FaLaLi implements IPhysicalAttack {

    @Override
    public void physicalAttack() {
        System.out.println("法拉利炮车,用大炮物理攻击轰你家大灯");
    }
}

最后登场的是大龙,大龙的话就比较嚣张了,必须两个攻击的接口都实现

public static class DaLong implements IMagicAttack, IPhysicalAttack {

    @Override
    public void magicAttack() {
        System.out.println("大龙向你吐了一口魔法浓痰");
    }

    @Override
    public void physicalAttack() {
        System.out.println("大龙用尾巴怼你");
    }
}

接下来run一把瞅瞅

public static void main(String[] args) {

    DaLong daLong = new DaLong();
    daLong.magicAttack();
    daLong.physicalAttack();

    YuanChenBing yuanChenBing = new YuanChenBing();
    yuanChenBing.magicAttack();

    FaLaLi faLaLi = new FaLaLi();
    faLaLi.physicalAttack();

}


大龙向你吐了一口魔法浓痰
大龙用尾巴怼你
远程兵,用魔法攻击轰你家大灯
法拉利炮车,用大炮物理攻击轰你家大灯

代码撸完了,接下来搬出书上的总结,说的灰常准确,请大家注意

  • 接口要尽量小,根据接口隔离原则拆分接口时,首先必须满足单一职责原则
  • 接口要高内聚,什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互
  • 定制服务,一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口(并不一定就是Java中定义的Interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各个访问者(即客户端)定制服务,什么是定制服务?定制服务就是单独为一个个体提供优良的服务
  • 接口设计是有限度的,接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度
  • 一个接口只服务于一个子模块或业务逻辑
  • 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法
  • 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理
  • 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑

迪米特法则(Law of Demeter, LoD)

迪米特法则主要表达的是当类与类之间产生耦合的情况下,类对外公布的方法将遵循怎样的规则,其实说白了就是当前类持有的耦合类,那么当前类只关心自己要调用的方法,具体内部有怎样的实现则不关心,这些不关心的方法或者变量都与我无关,这样写有什么好处呢?相当于以后当实现逻辑发生了变化,但是结果不变,我们只需要更改耦合类的内部实现即可,外部无需改动

定义

一个对象应该对其他对象有最少的了解。通俗地讲,
一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

类之间的低耦合要求:

  • 类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象
  • 迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限
  • 如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中
  • 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合

code

惯例做一个代码背景介绍
相信大家都听说过five five open这位兄台,那么咱们就以这位玩家作为咱们此次Demo的主角来编写代码
注意,不对此人自任何评判,纯粹是讲解需要,谢谢
首先咱们来一波接口,声明GB该实现的方法,包括什么自动攻击啊,自动躲避技能啊什么的

public interface IShellMethod {

    void autoAttack();

    void stopAutoAttack();

    void autoDucking();

    void stopAutoDucking();

    void autoUseSkill();

    void stopAutoUseSkill();

    void autoChangeChangeEquipage();

    void stopAutoChangeChangeEquipage();
}

接下来就是咱们的RMB玩家需要持有的接口就比较简单,遵循迪特米法则,就一个启动和停止

public interface IUseShell {

    void enableShell();

    void disableShell();
}

然后是咱们的脚本实现类,此处同时实现RMB玩家的接口,简单包装一下,注意这里增加了一个内部方法为计算躲避最佳路线,符合咱们迪特米法则,外部不关心内部的实现逻辑

private static class ShellImpl implements IShellMethod, IUseShell {

    @Override
    public void autoAttack() {
        System.out.println("auto attack hero or NPC");
    }

    @Override
    public void stopAutoAttack() {
        System.out.println("stop auto attack");
    }

    @Override
    public void autoDucking() {
        calculateDuckingPath();
        System.out.println("auto dodge attacks");
    }

    private void calculateDuckingPath() {
        System.out.println("calculate best ducking path!");
    }

    @Override
    public void stopAutoDucking() {
        System.out.println("stop auto dodge attacks");
    }

    @Override
    public void autoUseSkill() {
        System.out.println("auto use hero's skill");
    }

    @Override
    public void stopAutoUseSkill() {
        System.out.println("stop auto use hero's skill");
    }

    @Override
    public void autoChangeChangeEquipage() {
        System.out.println("auto buy best equipage");
    }

    @Override
    public void stopAutoChangeChangeEquipage() {
        System.out.println("stop auto buy best equipage");
    }

    @Override
    public void enableShell() {
        this.autoAttack();
        this.autoDucking();
        this.autoUseSkill();
        this.autoChangeChangeEquipage();
    }

    @Override
    public void disableShell() {
        this.stopAutoAttack();
        this.stopAutoDucking();
        this.stopAutoUseSkill();
        this.stopAutoChangeChangeEquipage();
    }
}

最后则是我们用户的包装类,持有RMB接口对象即可

private static class ShellUser {

    private IUseShell useShell;

    public ShellUser() {
        useShell = new ShellImpl();
    }

    public void startGB() {
        System.out.println("开始上分");
        useShell.enableShell();
    }

    public void stopGB() {
        System.out.println("卧槽,对面要举报我");
        useShell.disableShell();
    }
}

run一把爽一下

ShellUser lubenwei = new ShellUser();
lubenwei.startGB();
lubenwei.stopGB();

...
开始上分
auto attack hero or NPC
calculate best ducking path!
auto dodge attacks
auto use hero's skill
auto buy best equipage
卧槽,对面要举报我
stop auto attack
stop auto dodge attacks
stop auto use hero's skill
stop auto buy best equipage

好了,通过这个小Demo大家也可以大致轻松愉快的了解到迪特米法则,迪特米法则不同于上面说的几个法则,更注重类间规范,是以后耦合类间的书写规范,很多设计模式也是遵循的这些法则组合完成的设计。

开闭原则(Open-Closed Principle, OCP)

不管你是Java开发还是Android开发,只要你曾经或者正在学习的路上,那么或多或少会在网上看到这样的一句话:"对修改关闭,对扩展开放",OK, what is mean ?

定义

一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。软件实体包括以下几个部分:

  • 项目或软件产品中按照一定的逻辑规则划分的模块
  • 抽象和类
  • 方法

一个软件产品只要在生命期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”。

对于突如其来的变化,我们不是以修改原有代码来适配新的变化,而是通过增写扩展的方式来应对这个新变化。

书上为这些变化做了一个归类,如下:

  • 逻辑变化
    只变化一个逻辑,而不涉及其他模块,比如原有的一个算法是ab+c,现在需要修改为ab*c,可以通过修改原有类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理

  • 子模块变化
    一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的

  • 可见视图变化

注意:

在业务规则改变的情况下高层模块必须有部分改变以适应新业务,改变要尽量地少,防止变化风险的扩散。开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段

项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改,运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。

书中对开闭原则做了一个非常好的总结,这里就搬过来了:
开闭原则是最基础的一个原则,前五章节介绍的原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。换一个角度来理解,依照Java语言的称谓,开闭原则是抽象类,其他五大原则是具体的实现类

那么开闭原则又会带来哪些好处呢?

  • 首先是“测试”,在扩展的基础上,测试只需要测试新增加的接口就可以,无需对之前已经稳定可靠的代码进行重复测试
  • 通过缩小业务逻辑粒度从而达到代码复用的作用,从原子逻辑组合成业务逻辑,那么原子的拼接组合自然而然能够产生新的业务逻辑,复用的是久经测试的稳定代码,效率得到很大提升
  • 软件更多的工作其实是在维护中,我们写代码的目的也是为了今后更好的迭代和维护来对代码进行架构,那么在迭代的过程中,开发人员可以尽可能的少参与之前代码的观看和理解就能在原有的基础上进行功能的扩展,那么这样的代码才是良性的代码,相信同学们对阅读之前的代码也是或多或少的有过经历,能深刻体会其中“奥妙”。
  • 代码的设计并不能仅仅局限于当前的需求,而是要考虑到将来的扩展和可能的变化,预留出扩展的余地

OK,说了这么多关于开闭原则的好处,那么接下来应该提到的是开闭原则的使用。

老规矩,这次的code主角是王者荣耀游戏商城

public interface IGameHero {
    int getHeroPrice();

    int getDressUpPrice();

    String getName();
}

怒上3个接口规定商城售卖英雄的行为,也是定义实体行为,分别是获取英雄价格,获取英雄皮肤价格,获取英雄名字

然后是英雄接口实现类

public static class Hero implements IGameHero {

    private int mPrice;

    private int mDressPrice;

    private String mName;

    public Hero(int mPrice, int mDressPrice, String mName) {
        this.mPrice = mPrice;
        this.mDressPrice = mDressPrice;
        this.mName = mName;
    }

    @Override
    public int getHeroPrice() {
        return mPrice;
    }

    @Override
    public int getDressUpPrice() {
        return mDressPrice;
    }

    @Override
    public String getName() {
        return mName;
    }

    @Override
    public String toString() {
        return "英雄:" + getName() + "\t英雄价格:" + getHeroPrice() + "\t皮肤价格:" + getDressUpPrice();
    }
}

接下来上商店逻辑类,这里就简单写下,通俗易懂

public static class GameStore {
    private List<IGameHero> heroes;

    public GameStore() {
        this.heroes = new ArrayList<>();
        heroes.add(new Hero(13888, 888, "白起"));
        heroes.add(new Hero(10888, 388, "庄周"));
        heroes.add(new Hero(13888, 288, "程咬金"));
        heroes.add(new Hero(18888, 688, "貂蝉"));
    }

    public List<IGameHero> getHeroes() {
        return heroes;
    }
}

意思通俗易懂,大家自行参阅这些中文式代码哈~
然后就是我们的main咯~

List<IGameHero> heroes = gameStore.getHeroes();
    System.out.println("-----------进入商店----------");
    final String storeMessage = "售:";
    for (IGameHero hero : heroes) {
        System.out.println(storeMessage + hero.toString());
}


//输出
//-----------进入商店----------
//售:英雄:白起   英雄价格:13888  皮肤价格:888
//售:英雄:庄周   英雄价格:10888  皮肤价格:388
//售:英雄:程咬金  英雄价格:13888  皮肤价格:288
//售:英雄:貂蝉   英雄价格:18888  皮肤价格:688

这就是完美的构建了我们的某荣耀的简单商城了
好了,接下来TX要出活动了,刺激消费,挣一波,咋整
很简单,针对咱们的扩展开放原则,新建一个英雄实现接口类

public static class OffHero extends Hero {

    private float mDiscount = 1.0F;

    public OffHero(float discount, int price, int dressPrice, String name) {
        super(price, dressPrice, name);
        mDiscount = discount;
    }

    @Override
    public int getHeroPrice() {
        int ret = (int) (super.getHeroPrice() * mDiscount);
        return ret;
    }

    @Override
    public int getDressUpPrice() {
        int ret = (int) (super.getDressUpPrice() * mDiscount);
        return ret;
    }

    @Override
    public String toString() {
        return "折扣英雄:" + super.getName() +
                "\t英雄价格:" + super.getHeroPrice() + ",折扣价格:" + (int) (super.getHeroPrice() * mDiscount)
                + "\t皮肤价格:" + super.getDressUpPrice() + ",折扣价格:" + (int) (super.getDressUpPrice() * mDiscount);
    }
}

其实也就是集成原有的英雄类,增加一个折扣属性,重写获取英雄价格和皮肤价格
然后再略微动一下商城类,增加商城类的方法,这一步也是不可避免的,上层增加实体的获取和实现这些代码是必须要写的。


public void startSale() {
    System.out.println("商店开始活动,88折");
    this.heroes.clear();
    heroes.add(new OffHero(0.88F, 13888, 888, "白起"));
    heroes.add(new OffHero(0.88F, 10888, 388, "庄周"));
    heroes.add(new OffHero(0.88F, 13888, 288, "程咬金"));
    heroes.add(new OffHero(0.88F, 18888, 688, "貂蝉"));
}

public void resetPrice() {
    System.out.println("商店折扣活动截止");
    this.heroes.clear();
    heroes.add(new Hero(13888, 888, "白起"));
    heroes.add(new Hero(10888, 388, "庄周"));
    heroes.add(new Hero(13888, 288, "程咬金"));
    heroes.add(new Hero(18888, 688, "貂蝉"));
}

增加两个方法,折扣为88折,你买不了吃亏,买不了上当~
然后在main里面进行调用

gameStore.startSale();
heroes = gameStore.getHeroes();
for (IGameHero hero : heroes) {
    System.out.println(storeMessage + hero.toString());
}
gameStore.resetPrice();
heroes = gameStore.getHeroes();
for (IGameHero hero : heroes) {
    System.out.println(storeMessage + hero.toString());
}

//log
//商店开始活动,88折
//售:折扣英雄:白起 英雄价格:13888,折扣价格:12221   皮肤价格:888,折扣价格:781
//售:折扣英雄:庄周 英雄价格:10888,折扣价格:9581    皮肤价格:388,折扣价格:341
//售:折扣英雄:程咬金    英雄价格:13888,折扣价格:12221   皮肤价格:288,折扣价格:253
//售:折扣英雄:貂蝉 英雄价格:18888,折扣价格:16621   皮肤价格:688,折扣价格:605
//商店折扣活动截止
//售:英雄:白起   英雄价格:13888  皮肤价格:888
//售:英雄:庄周   英雄价格:10888  皮肤价格:388
//售:英雄:程咬金  英雄价格:13888  皮肤价格:288
//售:英雄:貂蝉   英雄价格:18888  皮肤价格:688

显而易见,很轻松的就达到了折扣的目的,这就是所谓的抽象原则的拥抱开放,关闭修改

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