设计模式:面向对象设计的六大原则

目录

  • 前言
  • 六大原则
    • 单一职责原则
    • 开闭原则
    • 里氏替换原则
    • 依赖倒置原则
    • 接口隔离原则
    • 迪米特原则
  • 总结

前言

很久没有写博客了,一直给自己找借口说太忙了,过几天有空再写,几天之后又几天,时间就这么快速的消逝。说到底就是自己太懒了,不下点决心真是不行。我决定逼自己一把,从今天开始学习设计模式系列,并写成博文记录下来,做不到的话,就罚自己一个月不玩游戏 (作孽啊。。。。)

六大原则

言归正传,这是我学习设计模式系列的第一篇文章,本文主要讲的是面向对象设计应该遵循的六大原则,掌握这些原则能帮助我们更好的理解面向对象的概念,也能更好的理解设计模式。这六大原则分别是:

  • 单一职责原则——SRP
  • 开闭原则——OCP
  • 里式替换原则——LSP
  • 依赖倒置原则——DIP
  • 接口隔离原则——ISP
  • 迪米特原则——LOD

单一职责原则

单一职责原则,Single Responsibility Principle,简称SRP。其定义是应该有且仅有一个类引起类的变更,这话的意思就是一个类只担负一个职责。

举个例子,在创业公司里,由于人力成本控制和流程不够规范的原因,往往一个人需要担任N个职责,一个工程师可能不仅要出需求,还要写代码,甚至要面谈客户,光背的锅就好几种,简单用代码表达大概如此:

public class Engineer {public void makeDemand(){}public void writeCode(){}public void meetClient(){}
}

代码看上去好像没什么问题,因为我们平时就是这么写的啊,但是细读一下就能发现,这种写法很明显不符合单一职责的原则,因为引起类的变化不只有一个,至少有三个方法都可以引起类的变化,比如有天因为业务需要,出需求的方法需要加个功能 (比如需求的成本分析),或者是见客户也需要个参数之类的,那样一来类的变化就会有多种可能性了,其他引用该类的类也需要相应的变化,如果引用类的数目很多的话,代码维护的成本可想而知会有多高。所以我们需要把这些方法拆分成独立的职责,可以让一个类只负责一个方法,每个类只专心处理自己的方法即可。

单一职责原则的优点:

  • 类的复杂性降低,实现什么职责都有明确的定义;
  • 逻辑变得简单,类的可读性提高了,而且,因为逻辑简单,代码的可维护性也提高了;
  • 变更的风险降低,因为只会在单一的类中的修改。

开闭原则

开闭原则,Open Closed Principle,是Java世界里最基础的设计原则,其定义是:

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码实现变化。这是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。

在我们编码的过程中,需求变化是不断的发生的,当我们需要对代码进行修改时,我们应该尽量做到能不动原来的代码就不动,通过扩展的方式来满足需求。

遵循开闭原则的最好手段就是抽象,例如前面单一职责原则举的工程师类,我们说的是把方法抽离成单独的类,每个类负责单一的职责,但其实从开闭原则的角度说,更好的方式是把职责设计成接口,例如把写代码的职责方法抽离成接口的形式,同时,我们在设计之初需要考虑到未来所有可能发生变化的因素,比如未来有可能因为业务需要分成后台和前端的功能,这时设计之初就可以设计成两个接口,

public interface BackCode{void writeCode();
}
public interface FrontCode{void writeCode();
}

如果将来前端代码的业务发生变化,我们只需扩展前端接口的功能,或者修改前端接口的实现类即可,后台接口以及实现类就不会受到影响,这就是抽象的好处。

里氏替换原则

里氏替换原则,英文名Liskov Substitution Principle,它的定义是

如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。

看起来有点绕口,它还有一个简单的定义:

所有引用基类的地方必须能够透明地使用其子类的对象。

通俗点说,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何异常。 但是反过来就不行了,因为子类可以扩展父类没有的功能,同时子类还不能改变父类原有的功能。

我们都知道,面向对象的三大特征是封装、继承和多态,这三者缺一不可,但三者之间却并不 “和谐“。因为继承有很多缺点,当子类继承父类时,虽然可以复用父类的代码,但是父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。如果需求变更,子类对父类的方法进行了一些复写的时候,其他的子类可能就需要随之改变,这在一定程度上就违反了封装的原则,解决的方案就是引入里氏替换原则。

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

1、子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。

2、子类可以有自己的个性,可以有自己的属性和方法。

3、子类覆盖或重载父类的方法时输入参数可以被放大。

比如父类有一个方法,参数是HashMap

public class Father {public void test(HashMap map){System.out.println("父类被执行。。。。。");}
}

那么子类的同名方法输入参数的类型可以扩大,例如我们输入参数为Map

public class Son extends Father{public void test(Map map){System.out.println("子类被执行。。。。");}
}

我们写一个场景类测试一下父类的方法执行效果,

public class Client {public static void main(String[] args) {Father father = new Father();HashMap map = new HashMap();father.test(map);}
}

结果输出:父类被执行。。。。。

因为里氏替换原则,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何异常。我们改下代码,调用子类的方法,

public class Client {public static void main(String[] args) {Son son = new Son();HashMap map = new HashMap();father.test(map);}
}

运行结果是一样的,因为子类方法的输入参数类型范围扩大了,子类代替父类传递到调用者中,子类的方法永远不会被执行,这样的结果其实是正确的,如果想让子类方法执行,可以重写方法体。

反之,如果子类的输入参数类型范围比父类还小,比如父类中的参数是Map,而子类是HashMap,那么执行上述代码的结果就会是子类的方法体,有人说,这难道不对吗?子类显示自己的内容啊。其实这是不对的,因为子类没有复写父类的同名方法,方法就被执行了,这会引起逻辑的混乱,如果父类是抽象类,子类是实现类,你传递一个这样的实现类就违背了父类的意图了,容易引起逻辑混乱,所以子类覆盖或重载父类的方法时输入参数必定是相同或者放大的。

4、子类覆盖或重载父类的方法时输出结果可以被缩小,也就是说返回值要小于或等于父类的方法返回值。

确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,所以,它跟开闭原则往往是相互依存的。

依赖倒置原则

依赖倒置原则,Dependence Inversion Principle,简称DIP,它的定义是:

高层模块不应该依赖底层模块,两者都应该依赖其抽象;

抽象不应该依赖细节;

细节应该依赖抽象;

什么是高层模块和底层模块呢?不可分割的原子逻辑就是底层模块,原子逻辑的再组装就是高层模块。

在Java语言中,抽象就是指接口或抽象类,两者都不能被实例化;而细节就是实现接口或继承抽象类产生的类,也就是可以被实例化的实现类。依赖倒置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的,这就是俗称的面向接口编程。

我们用歌手唱歌来举例,比如一个歌手唱国语歌,用代码表示就是:

public class ChineseSong {public String language() {return "国语歌";}
}
public class Singer {//唱歌的方法public void sing(ChineseSong song) {System.out.println("歌手" + song.language());}
}
public class Client {public static void main(String[] args) {Singer singer = new Singer();ChineseSong song = new ChineseSong();singer.sing(song);}
}

运行main方法,结果就会输出:歌手唱国语歌

现在,我们需要给歌手加一点难度,比如说唱英文歌,在这个类中,我们发现是很难做的。因为我们Singer类依赖于一个具体的实现类ChineseSong,也许有人会说可以在加一个方法啊,但这样一来我们就修改了Singer类了,如果以后需要增加更多的歌种,那歌手类不是一直要被修改?也就是说,依赖类已经不稳定了,这显然不是我们想看到的。

所以我们需要用面向接口编程的思想来优化我们的方案,改成如下的代码:

public interface Song {public String language();
}
public class ChineseSong implements Song{public String language() {return "唱国语歌";}
}
public class EnglishSong implements Song {public String language() {return "唱英语歌";}
}
public class Singer {//唱歌的方法public void sing(Song song) {System.out.println("歌手" + song.language());}
}
public class Client {public static void main(String[] args) {Singer singer = new Singer();EnglishSong englishSong = new EnglishSong();// 唱英文歌singer.sing(englishSong);}
}

我们把歌单独抽成一个接口Song,每个歌种都实现该接口并重写方法,这样一来,歌手的代码不必改动,如果需要添加歌的种类,只需写多一个实现类继承Song即可。

通过这样的面向接口编程,我们的代码就有了更好的扩展性,同时也降低了耦合,提高了系统的稳定性。

接口隔离原则

接口隔离原则,Interface Segregation Principle,简称ISP,其定义是:

客户端不应该依赖它不需要的接口

意思就是客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,这就需要对接口进行细化,保证接口的纯洁性。换成另一种说法就是,类间的依赖关系应该建立在最小的接口上,也就是建立单一的接口。

你可能会疑惑,建立单一接口,这不是单一职责原则吗?其实不是,单一职责原则要求的是类和接口职责单一,注重的是职责,一个职责的接口是可以有多个方法的,而接口隔离原则要求的是接口的方法尽量少,模块尽量单一,如果需要提供给客户端很多的模块,那么就要相应的定义多个接口,不要把所有的模块功能都定义在一个接口中,那样会显得很臃肿。

举个例子,现在的智能手机非常的发达,几乎是人手一部的社会状态,在我们年轻人的观念里,好的智能手机应该是价格便宜,外观好看,功能丰富的,由此我们可以定义一个智能手机的抽象接口 ISmartPhone,代码如下所示:

public interface ISmartPhone {public void cheapPrice();public void goodLooking();public void richFunction();
}

接着,我们定义一个手机接口的实现类,实现这三个抽象方法,

public class SmartPhone implements ISmartPhone{public void cheapPrice() {System.out.println("这手机便宜~~~~~");}public void goodLooking() {System.out.println("这手机外观好看~~~~~");}public void richFunction() {System.out.println("这手机功能真多~~~~~");}
}

然后,定义一个用户的实体类 User,并定义一个构造方法,以ISmartPhone 作为参数传入,同时,我们也定义一个使用的方法usePhone 来调用接口的方法,

public class User {private ISmartPhone phone;public User(ISmartPhone phone){this.phone = phone;}public void usePhone(){phone.cheapPrice();phone.goodLooking();phone.richFunction();}
}

可以看出,当我们实例化User类并调用其方法usePhone后,控制台上就会显示手机接口三个方法的方法体信息,这种设计看上去没什么大毛病,但是我们可以仔细想下,ISmartPhone这个接口的设计是否已经达到最优了呢?很遗憾,答案是没有,接口其实还可以再优化。

因为除了年轻人之外,中年商务人士也在用智能手机,在他们的观念里,智能手机并不需要丰富的功能,甚至不用考虑是否便宜 (有钱就是任性~~~~),因为成功人士都比较忙,对智能手机的要求大多是外观大气,功能简单即可,这才是他们心中好的智能手机的特征,这样一来,我们定义的 ISmartPhone 接口就无法适用了,因为我们的接口定义了智能手机必须满足三个特性,如果实现该接口就必须三个方法都实现,而对商务人员的标准来说,我们定义的方法只有外观符合且可以重用而已。你可能会说,我可以重写一个实现类啊,只实现外观的方法,另外两个方法置空,什么都不写,这不就行了吗?但是这也不行,因为 User 引用的是ISmartPhone 接口,它调用三个方法,你只实现了两个,那么打印信息就少了两条了,只靠外观的特性,使用者怎么知道智能手机是否符合自己的预期?

分析到这里,我们大概就明白了,其实ISmartPhone的设计是有缺陷的,过于臃肿了,按照接口隔离原则,我们可以根据不同的特性把智能手机的接口进行拆分,这样一来,每个接口的功能就会变得单一,保证了接口的纯洁性,也进一步提高了代码的灵活性和稳定性。

迪米特原则

迪米特原则,Law of Demeter,简称LoD,也被称为最少知识原则,它描述的规则是:

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

也就是说,一个类应该对自己需要耦合或调用的类知道的最少,类与类之间的关系越密切,耦合度越大,那么类的变化对其耦合的类的影响也会越大,这也是我们面向设计的核心原则:低耦合,高内聚。

迪米特法则还有一个解释:只与直接的朋友通信。

什么是直接的朋友呢?每个对象都必然与其他对象有耦合关系,两个对象的耦合就成为朋友关系,这种关系的类型很多,例如组合、聚合、依赖等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

举个例子,上体育课之前,老师让班长先去体务室拿20个篮球,等下上课的时候要用。根据这一场景,我们可以设计出三个类 Teacher(老师),Monitor (班长) 和 BasketBall (篮球),以及发布命令的方法command 和 拿篮球的方法takeBall

public class Teacher {// 命令班长去拿球public void command(Monitor monitor) {List<BasketBall> ballList = new ArrayList<BasketBall>();// 初始化篮球数目for (int i = 0;i<20;i++){ballList.add(new BasketBall());}// 通知班长开始去拿球monitor.takeBall(ballList);}
}
public class BasketBall {
}
public class Monitor {// 拿球public void takeBall(List<BasketBall> balls) {System.out.println("篮球数目:" + balls.size());}
}

然后,我们写一个情景类进行测试:

public class Client {public static void main(String[] args) {Teacher teacher = new Teacher();teacher.command(new Monitor());}
}

结果显示如下:

篮球数目:20

虽然结果是正确的,但我们的程序其实还是存在问题,因为从场景来说,老师只需命令班长拿篮球即可,Teacher只需要一个朋友----Monitor,但在程序里,Teacher的方法体中却依赖了BasketBall类,也就是说,Teacher类与一个陌生的类有了交流,这样Teacher的健壮性就被破坏了,因为一旦BasketBall类做了修改,那么Teacher也需要做修改,这很明显违背了迪米特法则。

因此,我们需要对程序做些修改,在Teacher的方法中去掉对BasketBall类的依赖,只让Teacher类与朋友类Monitor产生依赖,修改后的代码如下:

public class Teacher {// 命令班长去拿球public void command(Monitor monitor) {// 通知班长开始去拿球monitor.takeBall();}
}
public class Monitor {// 拿球public void takeBall() {List<BasketBall> ballList = new ArrayList<BasketBall>();// 初始化篮球数目for (int i = 0;i<20;i++){ballList.add(new BasketBall());}System.out.println("篮球数目:" + ballList.size());}
}

这样一来,Teacher类就不会与BasketBall类产生依赖了,即时日后因为业务需要修改BasketBall也不会影响Teacher类。

总结

好了,面向对象的六大原则就介绍到这里了。其实,我们不难发现,六大原则虽说是原则,但它们并不是强制性的,更多的是建议。遵照这些原则固然能帮助我们更好的规范我们的系统设计和代码习惯,但并不是所有的场景都适用,就例如接口隔离原则,在现实系统开发中,我们很难完全遵守一个模块一个接口的设计,否则业务多了就会出现代码设计过度的情况,让整个系统变得过于庞大,增加了系统的复杂度,甚至影响自己的项目进度,得不偿失啊。

所以,还是那句话,在合适的场景选择合适的技术!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/765020.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MNN 执行推理(九)

系列文章目录 MNN createFromBuffer&#xff08;一&#xff09; MNN createRuntime&#xff08;二&#xff09; MNN createSession 之 Schedule&#xff08;三&#xff09; MNN createSession 之创建流水线后端&#xff08;四&#xff09; MNN Session 之维度计算&#xff08;五…

编程思想-状态机

前言 对于开发者来说&#xff0c;状态机思想的重要性体现在多个方面&#xff0c;无论是设计复杂的系统还是处理简单的逻辑流程&#xff0c;状态机都能提供清晰、可维护且易于扩展的解决方案。以下是状态机思想对开发者而言的几个关键重要性&#xff1a; 逻辑清晰&#xff1a;状…

两台电脑简单的通信过程详解(经过两个路由器,不同网段)

一、eNSP拓扑图 二、配置4台电脑的IP地址、子网掩码、网关地址。 三、配置路由器 注意拓扑图的接口与本博客是否相符&#xff0c;判断以下命令中的ip是否需要修改。 1.AR1-接口对应IP <Huawei>sys #进入系统视图 [Huawei]int g0/0/0 #进入0/0/0接口 [Huawei-GigabitE…

【计算机网络篇】数据链路层(1)数据链路层的地位,问题

文章目录 &#x1f354;数据链路层在网络体系结构中的地位&#x1f354;链路&#xff0c;数据链路&#xff0c;帧&#x1f354;数据链路层的三个重要问题&#x1f95a;封装成帧和透明传输&#x1f95a;差错检测&#x1f95a;可靠传输 &#x1f354;数据链路层在网络体系结构中的…

飞天使-k8s知识点26-kubernetes温故知新1-pod

文章目录 创建一个podpod的启动命令和参数更改pod 镜像拉取策略 pod 的三种探针pod 探针的实现方式prestop 和 prestart 创建一个pod apiVersion: v1 # 必选&#xff0c;API 的版本号 kind: Pod # 必选&#xff0c;类型 Pod metadata: # 必选&#xff0c;元数据name: nginx # …

Linux文件 profile、bashrc、bash_profile区别

Linux系统中&#xff0c;有三种文件 出现的非常频繁&#xff0c;那就是 profile、bash_profile、bashrc 文件。 1、profile 作用 profile&#xff0c;路径&#xff1a;/etc/profile&#xff0c;用于设置系统级的环境变量和启动程序&#xff0c;在这个文件下配置会对所有用户…

欣瑞达信息技术邀您莅临2024长三角快递物流展

2024数字物流技术展 2024新能源商用车及物流车展 2024电商物流包装展 2024冷链物流展 2024年7月8-10日 | 杭州国际博览中心 参展企业介绍 深圳市欣瑞达信息技术有限公司&#xff08;曾用名&#xff1a;深圳市欣瑞达液晶显示技术有限公司&#xff09;成立于1997年&#xff0c;是…

【Unity】Stream最好用的Selfhost开源轻量服务

【背景】 有好几种场景的投屏或者远控应用希望实现&#xff0c;无论用哪种方式&#xff0c;都绕不开如何构建服务这一关。 【分析】 外网有很多直接付费使用的信令传输类型或是提供流服务的服务器&#xff0c;但我的目标场景是断绝外网的局域网&#xff0c;而且付费也总觉得…

浙江IGM机器人K5控制柜维修需要注意哪些问题?

IGM机器人K5控制柜常见故障及维修方法 1、电源故障&#xff1a; 表现为IGM机器人K5控制柜不能开机或突然断电。 检查&#xff1a;检查电源线是否连接良好&#xff0c;有无破损&#xff1b;检查电源模块的输出电压是否正常&#xff1b; 维修方法&#xff1a;如电源模块损坏&…

Tonghttpserver6.0.1.0部署指引优化版+基本操作指引+部分问题收集持续更新(by lqw)

文章目录 1.准备工作2.控制台安装解压和设置crt配置http.yaml配置grpc.yaml初始化数据库启动和访问ths管控台上传安装包 3.新增分組管理4.新增节点自动安装&#xff08;如果自动安装失败&#xff0c;可参考下一部分的手动安装&#xff09;手动安装&#xff08;自动安装成功的请…

SQL运维_Unix下MySQL-5.5.11配置文件示例

Unix运维_MySQL-5.5.11配置文件示例 MySQL 是一个关系型数据库管理系统, 由瑞典 MySQL AB 公司开发, 属于 Oracle 旗下产品。 MySQL 是最流行的关系型数据库管理系统之一, 在 WEB 应用方面, MySQL 是最好的 RDBMS (Relational Database Management System, 关系数据库管理系统…

C语言从入门到实践——常用字符函数和字符串函数的使用和模拟实现

目录 前言&#xff1a; 1. 字符分类函数 2. 字符转换函数 3.strlen的使用和模拟实现 4. strcpy 的使⽤和模拟实现 5. strcat 的使⽤和模拟实现 6. strcmp 的使⽤和模拟实现 7. strncpy 函数的使用 8. strncat 函数的使用 9. strncmp函数的使用 10. strstr 的使用和模…

QML新建QML后编译出错的解决方法

解决方法&#xff1a; 原有工程可以编译通过&#xff0c;添加了新的qml文件后&#xff0c;编译出现以下错误&#xff1a; undefined reference to QmlCacheGeneratedCode XXX 重新构建工程&#xff0c;错误仍未解决。 将编译文件夹下的.qmake.stash文件删掉&#xff0c;重新编译…

【索引失效】MySQL索引失效场景

1、对索引使用左或者左右模糊匹配 当我们使用左或者左右模糊匹配的时候&#xff0c;也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。 比如下面的 like 语句&#xff0c;查询 name 后缀为「林」的用户&#xff0c;执行计划中的 typeALL 就代表了全表扫描&#xff…

不要再封装各种 Util 工具类了,神级框架值Hutool

Hutool 谐音 “糊涂”&#xff0c;寓意追求 “万事都作糊涂观&#xff0c;无所谓失&#xff0c;无所谓得” 的境界。 Hutool 是一个 Java 工具包&#xff0c;也只是一个工具包&#xff0c;它帮助我们简化每一行代码&#xff0c;减少每一个方法&#xff0c;让 Java 语言也可以 …

【项目】基于MiniOS的CFS调度和增量式sleep

基于MiniOS的CFS调度和增量式sleep 文章目录 基于MiniOS的CFS调度和增量式sleep一、项目内容二、项目需求及分析CFS调度策略nicevruntime红黑树 增量式sleep/delay延时队列系统延时队列的插入延迟队列的操作延时队列的实现 三、具体实现3.1 实验环境与搭建3.2 实验设计CFS调度策…

推荐一款很不错的vscode高亮插件

用过很多款高亮插件&#xff0c;总感觉大部分显示都很乱&#xff0c;但是其中有一款用起来很清晰明了&#xff0c;很喜欢&#xff01; 插件名字&#xff1a;select-highlight-cochineal-color 使用效果&#xff1a; 底色高亮让人感觉很清晰&#xff0c;一个好的高亮插件能让你…

详细分析PyAutoGUI中的locate函数(附Demo)

目录 前言1. 基本知识2. 源代码分析3. Demo 前言 起因是实战中locate对个别定位会有偏差&#xff0c;导致一直识别错误 相应的基本知识推荐阅读&#xff1a;详细分析Python中的Pyautogui库&#xff08;附Demo&#xff09; 1. 基本知识 pyautogui.locate()函数用于在屏幕上定…

TWT:一个让WiFi6更省电的特性

更多精彩内容在公众号。 再wifi6前&#xff0c;已经有了不少节能特性&#xff1a;PSM,PSMP,APSD。在一个 Beacon 周期内&#xff0c;终端 会观察 AP 是否会向其发送数据&#xff0c;如果是&#xff0c;那么终端就保持等待&#xff0c;直到接收完成后&#xff0c; 才会进入休眠模…

微服务cloud--抱团取暖吗 netflix很多停更了

抱团只会卷&#xff0c;卷卷也挺好的 DDD 高内聚 低耦合 服务间不要有业务交叉 通过接口调用 分解技术实现的复杂性&#xff0c;围绕业务概念构建领域模型&#xff1b;边界划分 业务中台&#xff1a; 数据中台&#xff1a; 技术中台&#xff1a; 核心组件 eureka&#x…