/   今日科技快讯   /

瑞幸咖啡将支付1.8亿美元,就美国监管机构对它做出的会计欺诈指控达成和解。瑞幸咖啡董事长表示:“与美国证券交易委员会达成的这项和解反映了我们的合作和为补救而作出的努力,使得公司能够继续执行其商业战略。公司董事会和管理层致力于建立一个强有力的内部财务控制系统,并在合规和公司治理方面坚持做到最佳的实践。”

/   作者简介   /

明天就是周六啦,祝大家都能有一个愉快的周末!

本篇文章来自DoneWillianm的投稿,整理并分享了Java设计原则,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

DoneWillianm的博客地址:

https://www.jianshu.com/u/84e0c174a739

/   前言   /

在讲设计原则之前,我先强制灌输大家一波鸡汤,提倡

面向接口编程,代码的设计更重要的是考虑以后的扩展和可维护性

大家带着这样的思维来学习设计模式以及设计原则,慢慢就意会这波毒鸡汤了。

先声明一点就是老衲的blog,也是边学习,边记录,而后以讨论交流的方式叙述,有什么不对的地方大家多多担待。

/   设计原则   /

单一职责原则(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 {@Overridepublic void displayImage(String image) {System.out.println("displayImage:" + image);}@Overridepublic void displayText(String text) {System.out.println("displayText:" + text);}@Overridepublic 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 {@Overridepublic void use() {System.out.println("砸核桃 实用户");}
}/*** 锤子*/
class TPhone extends AbsPhone {@Overridepublic void use() {System.out.println("锤子 情怀户");}
}/*** 苹果*/
class ApplePhone extends AbsPhone {private void ringing() {System.out.println("先让苹果特有铃声响一阵...呜呜呜");}@Overridepublic 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产生一个对象

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

  • 接口或抽象类不依赖于实现类

  • 实现类依赖接口或抽象类

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

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

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

  1. 抽象不依赖细节

  2. 在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降到最低

  3. 如果两个类直接存在依赖关系,那么连接他们之间的桥梁就是接口,不依赖具体的低层模块

  4. 抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到“言必信, 行必果”

  5. 常用依赖传递,只要做到抽象依赖,即使是多层的依赖传递也无所畏惧

  • code

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

  • 每个类尽量都有接口或抽象类, 或者抽象类和接口两者都具备
    这是依赖倒置的基本要求, 接口和抽象类都是属于抽象的, 有了抽象才可能依赖倒置。

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

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

  • 尽量不要覆写基类的方法(类间依赖的是抽象, 覆写了抽象方法, 对依赖的稳定性会产生一定的影响)

  • 结合里氏替换原则使用(接口负责定义public属性和方法, 并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑, 同时在适当的时候对父类进行细化)
    说了那么多,还是需要深刻的在代码中多多运用“面向接口编程”

OK,接下来是我们的代码背景,还是不要上面的手机例子了, 老衲也是写吐了哈哈- -,这次是英雄联盟,恭喜RNG!

主角是咱们的Uzi和香锅打野

首先声明英雄和召唤师的接口

interface IHero {void attack();}interface IPlay {void play();}

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

static class Xiazi implements IHero {@Overridepublic void attack() {System.out.println("瞎子,我用双手,成就你的梦想");}
}static class VN implements IHero {@Overridepublic void attack() {System.out.println("VN,黑夜也会怕我");}
}

然后是咱们的上层player接口

static class ADPlayer implements IPlay {IHero hero;public ADPlayer(IHero hero) {this.hero = hero;}@Overridepublic void play() {hero.attack();}
}static class AssistPlayer implements IPlay {IHero hero;public void setHero(IHero hero) {this.hero = hero;}@Overridepublic 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 {@Overridepublic void magicAttack() {System.out.println("远程兵,用魔法攻击轰你家大灯");}}public static class FaLaLi implements IPhysicalAttack {@Overridepublic void physicalAttack() {System.out.println("法拉利炮车,用大炮物理攻击轰你家大灯");}
}

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

public static class DaLong implements IMagicAttack, IPhysicalAttack {@Overridepublic void magicAttack() {System.out.println("大龙向你吐了一口魔法浓痰");}@Overridepublic 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 {@Overridepublic void autoAttack() {System.out.println("auto attack hero or NPC");}@Overridepublic void stopAutoAttack() {System.out.println("stop auto attack");}@Overridepublic void autoDucking() {calculateDuckingPath();System.out.println("auto dodge attacks");}private void calculateDuckingPath() {System.out.println("calculate best ducking path!");}@Overridepublic void stopAutoDucking() {System.out.println("stop auto dodge attacks");}@Overridepublic void autoUseSkill() {System.out.println("auto use hero's skill");}@Overridepublic void stopAutoUseSkill() {System.out.println("stop auto use hero's skill");}@Overridepublic void autoChangeChangeEquipage() {System.out.println("auto buy best equipage");}@Overridepublic void stopAutoChangeChangeEquipage() {System.out.println("stop auto buy best equipage");}@Overridepublic void enableShell() {this.autoAttack();this.autoDucking();this.autoUseSkill();this.autoChangeChangeEquipage();}@Overridepublic 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;}@Overridepublic int getHeroPrice() {return mPrice;}@Overridepublic int getDressUpPrice() {return mDressPrice;}@Overridepublic String getName() {return mName;}@Overridepublic 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;}@Overridepublic int getHeroPrice() {int ret = (int) (super.getHeroPrice() * mDiscount);return ret;}@Overridepublic int getDressUpPrice() {int ret = (int) (super.getDressUpPrice() * mDiscount);return ret;}@Overridepublic 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
查看全文

http://www.taodudu.cc/news/show-5064796.html

相关文章:

  • python组合和继承_python 继承与组合
  • Linux解压rar文件(unrar安装和使用,分卷解压)
  • ubuntu18.04编译4.15内核过程
  • 免编译安装Nginx_CentOS7编译安装nginx1.19.2
  • ik分词器的下载和使用
  • Maven仓库配置环境变量
  • PASCAL VOC数据集标注要求
  • 盘点新能源汽车常用的8种传感器
  • 建立自己的voc数据集_制作自己的数据集(VOC2007格式)用于Faster-RCNN训练
  • 【PP-YOLOE+】第18届全国大学生智能汽车竞赛百度完全模型组线上资格
  • voc数据 map f1计算
  • PASCAL VOC数据集分析(分类部分)
  • PASCAL VOC
  • VDA标准在汽车行业的应用
  • 利用VOC2007数据集创建属于自己的数据集
  • Pascal VOC 数据集介绍(tensorflow model zoo)
  • VOC格式数据分析统计和处理
  • pascal行人voc_在一个很小的Pascal VOC数据集上训练一个实例分割模型
  • 将车辆数据集kitti转换为VOC格式(车辆检测)
  • 汽车及零部件可靠性测试解决方案
  • 环保数采仪助力汽车4S店VOC排放监测
  • VS2019 error LNK1104:无法打开***.exe的报错
  • LNK1104: cannot open file MSVCRTD.lib
  • VS LINK : fatal error LNK1104: 无法打开文件.exe
  • VS 2019 VC++项目出现 LNK1104 无法打开文件“libcmtd.lib“ 的解决方法
  • VS报错LNK1104原因之:引入外部库文件却没有将其路径添加至main所在项目的链接器的附加库目录
  • 如何解决错误     error LNK1104: 无法打开文件“nafxcwd.lib”
  • 工业元宇宙讲座PPT讲义
  • 一张涵盖Linux云所有知识点的思维导图(含K8S),你需要吗?
  • 世界元宇宙大会—李伯虎院士主旨报告:工业元宇宙模式、技术与应用初探

探究Java的设计原则相关推荐

  1. 万字好文!探究Java的设计原则,看了都说好!

    点击上方 "程序员小乐"关注, 星标或置顶一起成长 后台回复"大礼包"有惊喜礼包! 关注订阅号「程序员小乐」,收看更多精彩内容 每日英文 Man has to ...

  2. java七大设计原则

    java七大设计原则 1.开闭原则(Open Close Principle) 定义:一个软件实体如类.模块和函数应该对扩展开放,对修改关闭. 开放-封闭原则的意思就是说,你设计的时候,时刻要考虑,尽 ...

  3. JAVA六大设计原则 和 23种设计模式

    相关书籍:<大话设计模式>.<Java设计模式>.<设计模式之禅>.<研磨设计模式>.<Head First 设计模式> JAVA六大设计原 ...

  4. Java七大设计原则详解与运用

    开心一笑 [婚礼上,气氛正高着,主持人问新郎:"你会不会爱新娘一辈子?新郎兴高采烈的喊:"会".主持人:"你会不会在新娘容颜憔悴,疾病缠身的时候离开她? 猴急的 ...

  5. java 七大设计原则之依赖倒置,里氏替换原则(文字代码相结合理解)

    java 七大设计原则之依赖倒置,里氏替换原则,文字代码相结合理解 七大设计原则有哪些? 为什么要使用七大设计原则? 依赖倒置原则 里氏替换原则 喜欢就争取,得到就珍惜,错过就忘记.人生也许不尽完美, ...

  6. Java面向对象设计原则

    面向对象设计原则是OOPS(Object-Oriented Programming System,面向对象的程序设计系统)编程的核心,但大多数Java程序员追逐像Singleton.Decorator ...

  7. JAVA七大设计原则总结(详解篇)

    目录 什么是设计模式 概览 开闭原则 依赖倒置原则 单一职责原则 接口隔离原则 迪米特原则 里氏替换原则 合成复用原则 设计原则总结 什么是设计模式 设计模式(Design Pattern)是一套被反 ...

  8. Java七大设计原则 - 开闭原则

    一. 什么是开闭原则(Open Closed Principle)? 开闭原则(Open Closed Principle,OCP)又称为ocp原则, 由勃兰特·梅耶(Bertrand Meyer)提 ...

  9. java七大设计原则,23种设计模式

    点击查看七大设计原则,23种设计模式 其中 简单工厂.工厂方法.抽象工厂 三种工厂模式中的工厂类的作用基本都是:根据传入的参数创建对应的对象,如果创建的种类太多,那么 简单工厂要写很多 if - el ...

最新文章

  1. 使用GDAL创建Erdas格式的金字塔
  2. matlab 怎么与运算,求助如何用MATLAB计算VAR和Expected shortfall
  3. 设定网页最小最大宽度和高度(兼容IE6)
  4. 你对Java网络编程了解的如何?Java BIO 网络编程 | Netty 前期知识
  5. WannaCry勒索比特币蠕虫病毒解决方案
  6. 报错:Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2
  7. 如何使用 C# 判断一个文件是否为程序集
  8. [css] 使用css3做一个魔方旋转的效果
  9. Java String 类的方法
  10. Mysql 数据库学习笔记02 编程
  11. Atiitt 常见机器算法 理解 总结 目录 1. 机器学习的核心是“使用算法解析数据,从中学习,然后对世界上的某件事情做出决定或预测” 1 2. 1. 五大流派 2 2.1. ①符号主义:使用
  12. 【刘润五分钟商学院】156对对抗思考到平行思考
  13. MySQL中的文本处理函数整理,收藏速查
  14. 算法系列之十八:用天文方法计算二十四节气(下)
  15. 数据库|ALTER命令
  16. python版本换行问题
  17. 回波损耗、插入损耗、反射系数等概念
  18. 期待已久的《刀剑神域》第三季终于来了
  19. Java 实现PDF转图片,去除PDF水印,图片转PDF
  20. 一小时刷完英语口语常用3000词汇(绿色护眼版)day8-词汇701-800

热门文章

  1. 前后端分离项目,如何解决跨域问题?
  2. 亚式期权_基于平均价格的期权
  3. 爱立信与RDK管理公司宣布将MediaFirst电视平台集成到RDK平台
  4. 2021个人免签支付系统详解
  5. js中document通过ID获取的一些使用方法
  6. 魅族路由器极速版刷机_好先生追剧神器 魅族路由器极速版评测
  7. cobol-PERFORM
  8. 记一次国产压铸模拟软件的使用过程
  9. 疯狂劲爆物理游戏《卡车大战骷髅》
  10. Sission与aplication 在servlet中用法汇总