前言
我想用贴近生活的语句描述一下自己对六种原则的理解. 也就是不做专业性的阐述, 而是描述一种自己学习后的理解和感受, 因为能力一般而且水平有限, 也许举的例子不尽妥当, 还请谅解
原本我是想用 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 playZhiHu () {
- System.out.println("玩知乎");
- }
- public void doSports () {
- System.out.println("打乒乓球");
- }
- public void work () {
- System.out.println("工作");
- }
- }
现在看起来有点混乱, 因为这个类里面混合了三个职责:
玩知乎, 这是知乎 er 的职责
打乒乓球, 这是业余运动爱好者的职责
工作, 这是 "普普通通上班族" 的职责(似乎暴露了什么)
OK, 正如你所见, 既然我们要遵循单一职责, 那么怎么做呢? 当然是要拆分了
我们要根据接口去拆, 拆分成三个接口去约束 People 类(不是把 People 类拆了哈)
- // 知乎 er
- public interface ZhiHuer {
- public void playZhiHu();
- }
- // 上班族
- public interface OfficeWorkers {
- public void work();
- }
- // 业余运动爱好者
- public interface AmateurPlayer {
- public void doSports();
- }
然后在 People 中继承这几个接口
- public class People implements ZhiHuer,AmateurPlayer,OfficeWorkers{
- public void playZhiHu () {
- 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();
- ZhiHuer zhiHuer = new People();
- zhiHuer.playZhiHu(); // 输出: 玩知乎
- 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;
- }
- }
- // 低层模块: Windows 操作系统
- public class Windows implements OperatingSystem {
- String name;
- public Windows (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 Windows("windows10 系统"); // windows10
- coder.develop(Ubuntu);
- coder.develop(windows10);
- }
- }
虽然接口的加入让代码多了一些, 但是现在扩展性变得良好多了, 即使有新的操作系统加入进来, Coder.develop 也能处理
P4. 接口隔离原则(interface segregation principle)
接口隔离原则的要求是: 类间的依赖关系应该建立在最小的接口上. 这个原则又具体分为两点
接口要足够细化, 当然了, 这会让接口的数量变多, 但是每个接口会具有更加明确的功能
在 1 的前提下, 类应该依赖于 "最小" 的接口上
举个例子, 中秋节其实只过了一个多月, 现在假设你有一大盒 "五仁月饼" 想带回家喂猪, 但是无奈的是包包太小放不下, 而且一盒沉重的月饼对瘦弱的你是个沉重的负担. 这个时候, 我们可以把月饼盒子拆开, 选出一部分自己需要 (wei zhu) 的月饼, 放进包包里就好啦, 既轻便又灵活.
还是上代码吧, 比如我们有这样一个知乎 er 的接口, 里面涵盖了一些可能的行为. 许多知乎用户还会保持友善, 同时根据自己的专业知识认真写文章. 但也有少数的人会把生活中的负面能量带到网络中
- public interface ZhiHuer {
- // 认真撰文
- public void seriouslyWrite();
- // 友好评论
- public void friendlyComment();
- // 无脑抬杠
- public void argue();
- // 键盘攻击
- public void keyboardAttack ();
- }
我们发现, 这个接口可以进一步拆分成两个接口, 分别命名为 PositiveZhiHuer,NegativeZhihuer. 这样, 我们就把接口细化到了一个合理的范围
- public interface PositiveZhiHuer {
- // 认真撰文
- public void seriouslyWrite();
- // 友好评论
- public void friendlyComment();
- }
- public interface NegativeZhihuer {
- // 无脑抬杠
- public void argue();
- // 键盘攻击
- public void keyboardAttack ();
- }
>> 备注: 妥善处理 单一职责原则 和 接口隔离原则的关系
事实上, 有两点要说明一下
单一职责原则和接口隔离原则虽然看起来有点像, 好像都是拆分, 但是其实侧重点是不一样的,"职责" 的粒度其实是比 "隔离接口" 的粒度要大的
基于 1 中阐述的原因, 其实 单一职责原则 和 接口隔离原则是可能会产生冲突的, 因为接口隔离原则要求粒度尽可能要细, 但是单一职责原则却不同, 它要求拆分既不能过粗, 但也不能过细, 如果把原本单一职责的接口分成了 "两个 0.5 职责的接口", 那么这就是单一职责所不能允许的了.
当两者冲突时, 优先遵循 单一职责原则
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 杀, 那岂不美哉, 这就好比是 "对扩展开放".
但是呢, 如果游戏突然搞个活动, 把闪现 / 攻击 / 技能释放的键盘通通换个位置, 给你一个 "双十一的惊喜", 这恐怕就给人带来惨痛的回忆了. 所以我们希望已有的结构不要动, 也不能动, 要 "对修改封闭"
(本人不玩游戏, 这些是自己查到的, 如果错误还请指正)
总结
原则不是死板的而是灵活的
一些原则其实是存在一定的冲突的, 重要的是权衡, 是掌握好度
六大原则是 23 种设计模式的灵魂, 六大原则指导了设计模式, 设计模式体现了六大原则
来源: https://www.cnblogs.com/penghuwan/p/11769637.html