提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
例如:随着计算机的不断发展,程序学习这门技术也越来越重要,很多人都开启了学习java编程学习,本文就介绍了Java编程学习的七大基本原则。
面向对象设计原则一共有七个:开闭原则、里氏替换原则、依赖倒转原则、单一职责原则、接口隔离原则、组合/聚合复用原则、迪米特法则。
下面就仔细来介绍一下这些原则
提示:以下是本篇文章正文内容,下面案例可供参考
一、单一职责原则
对类来说,就是一个类应该只负责一项职责,这就是单一职责原则。
如果你有多个动机去修改一个类,那么这个类就有多个职责。这可能比较难理解,因为我们通常把一组职责放在一起思考。下面来看一个具体的例子。下面是一个 bird(调制解调器或者叫猫)的接口,以愤怒的小鸟为例来
package com.oop.day12;
/**
* 以愤怒的小鸟游戏为li
*/
public interface Bird {
void attack1();
void attack2();
void cry1();
void cry2();
}
上面这个猫的接口中存在两个职责:第一个是攻击方式(attack1和attack2);第二个是攻击时的叫声(cry1和cry2)。这两个职责应该被分开,因为 :
- 它们没有共同点,而且通常会因为不同的原因被修改;
- 调用它们的代码通常属于应用的不同部分,而这部分代码也会因为不同的原因被修改。
下面是优化之后的

通过拆分猫的接口,我们可以在应用的其他部分将猫的设计分开来对待。虽然我们又在猫的实现中(Bird Implementation)将这两部分职责重新耦合在一起,但是除了初始化猫的代码以外,在使用面向接口编程的原则后,其他代码并不需要依赖于猫的实现。
SRP 是最简单的一个面向对象设计原则,但也是最难做正确的一个,因为我们习惯于将职责合并,而不是将它们分开来。找到并且拆分这些职责正是软件设计真正需要做的事情。
单一职责原则例子
package com.oop.day12;
public class Animal {
public static void main(String[] args) {
RoadAnimal roadAnimal = new RoadAnimal();
//输出: 老虎 在森林里跑
roadAnimal.run("老虎");
//输出: 狮子 在森林上跑
roadAnimal.run("摩托车");
SeaAnimal seaAnimal = new SeaAnimal();
//输出: 鲨鱼 在海洋里游
seaAnimal.run("鲨鱼");
}
}
// 陆地动物
class RoadAnimal {
public void run(String animal){
System.out.println(animal + " 在公路上跑");
}
}
//海洋动物
class SeaAnimal {
public void run(String animal){
System.out.println(animal + " 在天上跑");
}
}
小结
核心思想:应该有且仅有一个原因引起类的变更
好处:类的复杂度降低、可读性提高、可维护性提高、扩展性提高、降低了变更引起的风险。
需注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目和环境而异。
二、开闭原则
开闭原则的英文是 Open Closed Principle,缩写为 OCP。
开闭原则说的是:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的。
- 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
- 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。
两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
最简单的就是利用抽象类,将方法定义为抽象方法,然后在后代中重写方法,来进行单继承多实现。
示例:员工回家乘坐的交通方式
public abstract class Tools {
/**
* 抽象方法型行驶
*/
public abstract void run();
}
package com.oop.day10;
public class Bike extends Tools {
@Override
public void run(){
System.out.println("下班的人努力蹬三蹦子回家");
}
}
package com.oop.day10;
public class Bus extends Tools{
@Override
public void run(){
System.out.println("下班的人挤公交车回家");
}
}
package com.oop.day10;
/**
* @authorDesc 诸神在上,佑我程序,玄功护体,BUG皆去!
* @author
* @date 2022-11-04 10:34:34
* @version 1.0.0
* @description 员工类
*/
public class Employee {
private String name;
public Employee() {
}
public Employee(String name) {
this.name = name;
}
/**
* 乘坐交通工具回家
*/
public void goHome(Tools tools){
System.out.println(this.name + "准备回家");
tools.run();
}
}
package com.oop.day10;
public class Test {
public static void main(String[] args) {
Employee emp1 = new Employee("马云");
Employee emp2 = new Employee("马化腾");
Tools tools1 = new Bike();
Tools tools2 = new Bus();
emp1.goHome(tools1);
emp2.goHome(tools2);
}
}
小结
核心思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化。例如就同过抽象类来解决这个问题
通俗来讲:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
三.接口隔离原则
什么是接口隔离原则
接口对于Java开发者来说都不陌生,它几乎存在于每一个Java程序中,是抽象的代名词。在讲接口隔离原则之前,先说说接口,接口分为以下两种:
- 实例接口(Object Interface))
在 Java 中声明一个类,然后用 new 关键字产生一个实例,是对一个类型的事物的描述,这就是一种接口。或许我们乍一看会有点懵,怎么和我们原来学习的接口不一样呢,其实我们这样想,我们都知道,在 Java 中有一个 Class 类,表示正在运行的类和接口,换句话说每一个正在运行时的类或接口都是 Class 类的对象,这是一种向上的抽象。接口是一种更为抽象的定义,类是一类相同事物的描述集合,那为什么不可以抽象为一个接口呢? - 类接口(Class Interface)
这就是我们经常使用的用 interface 定义的接口
这里插一句,接口隔离原则中所说的接口并不是狭意的在 Java 中用 interface 定义的接口,而是一种更为宽泛的概念,可以是接口,抽象类或者实体类。
接口隔离原则分析
接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立角色,不多不少,不干不该干的事,该干的事都要干。
1.一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。
2.接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口
3.使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。
4.可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
一旦一个接口太大,则需要将它,分割成一些更细小的接口,使用该接口的客户端需知道与之相关的方法即可。

设计存在问题:接口承担了太多职责
导致该接口的实现类很庞大,实现类中都需要实现接口所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;
由于客户端针对大接口编程,将在一定程度上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。
因此需要按照接口隔离原则和单一职责原则进行重构。将其中的一些方法封装在不同的小的接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块和类)所需的方法即可。
优化后:

四.里氏替换原则
*里氏替换原则:**所有引用基类(父类)的地方必须能透明地使用其子类的对象。也就是说子类可以扩展父类的功能,但不能改变父类原有功能。
透明使用的关键就是,子类不能改变父类原有功能。
里氏替换原则由 Barbara Liskov 提出,这个原则很明显,Java 的多态或者 C++ 的虚函数本身就允许把指向基类的指针或引用,在调用其方法或函数的时候,调用实际类型的方法或函数。
里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程式中代替其基类(超类)对象。”
继承
优点
- 提高代码的重用性,子类拥有父类的方法和属性。
- 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性。
缺点(侵入性、不够灵活、高耦合)
- 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性。
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
为什么需要里氏替换原则
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用。
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。

我们来看一个简单的例子:Circle 和 Square 继承了基类 Shape,然后在应用的方法中,根据输入 Shape 对象类型进行判断,根据对象类型选择不同的绘图函数将图形画出来。
void drawShape(Shape shape) {
if (shape.type == Shape.Circle ) {
drawCircle((Circle) shape);
} else if (shape.type == Shape.Square) {
drawSquare((Square) shape);
} else {
……
}
}
这种写法的代码既常见又糟糕,它同时违反了开闭原则和里氏替换原则。
- 首先看到这样的 if/else 代码,就可以判断违反了(我们刚刚在上个部分讲过的)开闭原则:当增加新的 Shape 类型的时候,必须修改这个方法,增加 else if 代码。
- 其次也因为同样的原因违反了里氏替换原则:当增加新的 Shape 类型的时候,如果没有修改这个方法,没有增加 else if 代码,那么这个新类型就无法替换基类 Shape。
要解决这个问题其实也很简单,只需要在基类 Shape 中定义 draw 方法,所有 Shape 的子类,Circle、Square 都实现这个方法就可以了:
public abstract Shape{
public abstract void draw();
}
上面那段 drawShape() 代码也就可以变得更简单:
void drawShape(Shape shape) {
shape.draw();
}
注意事项
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
小结
核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。
通俗来讲:只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。
好处:增强程序的健壮性,即使增加了子类,原有的子类还可以继续运行。
需注意:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系 采用依赖、聚合、组合等关系代替继承。
五.依赖倒置原则
依赖倒置原则的英文是 Dependency Inversion Principle,缩写为 DIP。依赖倒置原则说的是:高层模块不依赖低层模块,它们共同依赖同一个抽象,这个抽象接口通常是由高层模块定义,低层模块实现。同时抽象不要依赖具体实现细节,具体实现细节依赖抽象。高层模块就是调用端,低层模块就是具体实现类,抽象就是指接口或抽象类,细节就是实现类。
依赖倒置原则:
- 高层模块不应该依赖底层模块,二者都应该依赖其抽象。(高层理解为吃动作,底层理解为面包等具体食物)
- 抽象不应该依赖细节,细节应该依赖抽象。
- 依赖倒置的中心思想就是面向接口编程。
在继承时要遵循里氏替换原则。
错误示例:
class DependencyInversionTest {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
//电子邮箱类
class Email{
public String getInfo(){
return "获得电子邮件信息";
}
}
//完成Person接受消息的功能
//缺点:如果要接受消息的对象是 微信类、短信类,就得增加相应的方法。
class Person{
public void receive(Email email){
System.out.println(email.getInfo());
}
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932
解决示例:
引入一个抽象的接口IReceiver,表示接收者,这样Person类就只与接口发送依赖
class DependencyInversionTest {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new Wechat());
}
}
interface IReceiver{
String getInfo();
}
//电子邮箱类
class Email implements IReceiver{
public String getInfo(){
return "获得电子邮件信息";
}
}
//微信类
class Wechat implements IReceiver{
@Override
public String getInfo() {
return "获得微信信息";
}
}
//完成Person接受消息的功能
class Person{
//这样就不需要每添加一个消息对象就写相应的方法
public void receive(IReceiver receiver){
System.out.println(receiver.getInfo());
}
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932
上面的示例是基于接口实现的,下面的示例是基于构造函数来实现的:
class DependencyInversionTest {
public static void main(String[] args) {
Person person = new Person(new Email());
person.receive();
person = new Person(new Wechat());
person.receive();
}
}
interface IReceiver{
String getInfo();
}
//电子邮箱类
class Email implements IReceiver{
public String getInfo(){
return "获得电子邮件信息";
}
}
//微信类
class Wechat implements IReceiver{
@Override
public String getInfo() {
return "获得微信信息";
}
}
//完成Person接受消息的功能
class Person{
private IReceiver iReceiver;
public Person(IReceiver iReceiver) {
this.iReceiver = iReceiver;
}
public void receive(){
System.out.println(iReceiver.getInfo());
}
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932
下面的示例是基于setter方法来实现:
class DependencyInversionTest {
public static void main(String[] args) {
Person person = new Person();
person.setiReceiver(new Email());
person.receive();
person.setiReceiver(new Wechat());
person.receive();
}
}
interface IReceiver{
String getInfo();
}
//电子邮箱类
class Email implements IReceiver{
public String getInfo(){
return "获得电子邮件信息";
}
}
//微信类
class Wechat implements IReceiver{
@Override
public String getInfo() {
return "获得微信信息";
}
}
//完成Person接受消息的功能
class Person{
private IReceiver iReceiver;
public void setiReceiver(IReceiver iReceiver) {
this.iReceiver = iReceiver;
}
public void receive(){
System.out.println(iReceiver.getInfo());
}
}
————————————————
版权声明:本文为优快云博主「做不秃头的程序员」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932
小结
核心思想:高层模块不应该依赖底层模块,二者都该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象;
通俗来讲:依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,互不影响,实现模块间的松耦合。
好处:依赖倒置的好处在小型项目中很难体现出来。但在大中型项目中可以减少需求变化引起的工作量。使并行开发更友好。
六.迪米特法则
什么是迪米特法则
迪米特法则(Law of Demeter )又叫做最少知识原则,也就是说,一个对象应当对其他对象尽可能少的了解。不和陌生人说话。英文简写为: LoD。
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,于1987年秋天由 lan holland 在美国东北大学为一个叫做迪米特的项目设计提出的。
迪米特法则的模式与意义
迪米特法则可以简单说成:talk only to your immediate friends。
对于OOD来说,又被解释为下面几种方式:
一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增
加了系统的复杂度。
狭义的迪米特法则
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。
这么看不太形象,我们来举个例子,和陌生人说话,甲和朋友认识,朋友和陌生人认识,而甲和陌生人不认识,这时甲可以直接和朋友说话,朋友可以直接和陌生人说话,而如果甲想和陌生人说话,就必须通过朋友

这种方式,甲和陌生人之间就没有了任何直接联系,这样就避免了甲和陌生人的耦合度过高。当然还有一种更好的方式,与依赖倒转原则结合,为陌生人创建一个接口。

这样的方式,和甲直接通信的是陌生人的抽象父类,和具体陌生人没有直接关系,所以符合迪米特法则。
小结
缺点
- 在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
- 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
应用实例
- 外观模式
- 中介者模式
七.合成复用原则
合成复用原则的定义
合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
实现
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
下面以汽车分类管理程序为例来介绍合成复用原则的应用。
分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。下图所示是用继承关系实现的汽车分类的类图。
原则是尽量使用合成/聚合的方式。而不是使用继承。

小结
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
参考文章
参考文链接:https://blog.youkuaiyun.com/weixin_41804870/article/details/109132932
总结
例如:以上就是今天要讲的内容,本文仅仅简单介绍了面向对象设计原则一共有七个:开闭原则、里氏替换原则、依赖倒转原则、单一职责原则、接口隔离原则、组合/聚合复用原则、迪米特法则。举出了大量的实例
1万+





