Java 面向对象开发的五大原则

本文详细介绍了Java面向对象设计的五大原则:单一职责原则(SRP)、开放-封闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。每个原则都包含其核心思想、主要原则和实例,旨在帮助开发者理解如何创建可扩展、低耦合和高内聚的代码,提高软件的可维护性和可复用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0 - 概要

五大原则简称:SOLID
S-SingleResponsibility-单一职责原则
O-OpenClose-开闭原则
L-LiskovSubstituion-里氏替换原则
I-InterfaceSegregation-接口隔离原则
D-DependencyInversion-依赖倒置原则

1 - 单一职责原则

核心思想:

一个类,最好只做一件事,只有一个引起它的变化。
单一职责原则可以看作是低耦合高内聚思想在面向对象上的引申,将职责定义为引起变化的原因,通过减少引起变化的原因(单一职责),来提高类的内聚性。当一个类的职责过多时,引起它变化的原因就会增多,这将导致职责间的互相依赖增多,从而就破坏了耦合度和内聚性。

主要原则:

示例:

// 历史做法
class Calculator {
	int a;
	int b;
	public Calculator(int a, int b) {
		this.a = a;
		this.b = b;
	}
	public int add() {return a + b;}
	public int sub() {return a - b;}
	public int mul() {return a * b;}
	public int div() {return a / b;}
}
# 此时若要新增一个阶乘计算,就得在原始类中进行修改,一招不慎就可能引入新的复杂度和带来异常
// 单一职责做法
class Calculator {
	int a;
	int b;
	public Calculator(int a, int b) {
		this.a = a;
		this.b = b;
	}
}
class Add extends Calculator {
	public Add() {
		super(a, b);
	}
	public int add() {return a + b;}
}
class Sub extends Calculator {
	public Sub() {
		super(a, b);
	}
	public int sub() {return a - b;}
}
class Mul extends Calculator {
	public Mul() {
		super(a, b);
	}
	public int mul() {return a * b;}
}
class Div extends Calculator {
	public Div() {
		super(a, b);
	}
	public int div() {return a / b;}
}
# 此时若要新增一个新的职责,则再添加一个类来负责。如此这般,不会在已有类中引入额外的复杂度,也不会带来额外的理解成本,同时也保障了类间的低耦合。

图示:

单一职责的经典案例

2 - 开放-封闭原则

核心思想:

软件实体应该是可扩展、而不可修改的。也就是对扩展开放,对修改封闭。
大白话就是:在增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。

主要原则:

对扩展开放,意味着当有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况;
对修改封闭,意味着类一旦设计完成,就可以独立完成工作,而不要对其进行任何修改的尝试;
要做到这两点主要就是,要对抽象编程,不要对具体编程,因为抽象是相对稳定的。

示例:

书店售书例子
1. 不满足开闭原则
public interface IBook {
	public int price();
	public String name();
	public String author();
}

public class Book implements IBook {
	public int price;
	public String name;
	public String author;

	public Book(int price, String name, String author) {
		this.price = price;
		this.name = name;
		this.author = author;
	}

	public int price() {
		return price;
	}
	public String name() {
		return name;
	}
	public String author() {
		return author;
	}
}
书店售书的例子我们已经写好了,现在书店已经运营了一段时间,书店盈利,但是为了扩大市场规模,
书店决定做活动,50元以上打8折,50元以下打9折,当我们接到这个需求,有什么方法能解决呢:
a. 给接口添加一个 getDiscountPrice() 获取打折价格的方法,所有实现类实现这个方法。
但是这样修改结果就是实现类要改动,客户端调用也要改的动,IBook作为一个接口应该是稳定且可靠,
不应该经常变化,因此这种方式被否定。
b. 直接修改实现类Book中的getPrice()方法,在这个方法中实现打折处理,这样相当于用一种新的能力去替换了已存在的能力,风险很大。
c. 通过扩展出一个接口来实现变化。如下所示。
2. 满足开闭原则
public interface IDiscountPrice() {
	int getDiscountPrice();
}

public class DiscountBookPrice extends Book implements IDiscountPrice {
	
	public DiscountBookPrice(int price, String name, String author) {
		super(price, name, author);
	}

	public int getDiscountPrice() {
		if (price < 50) {
			return (int) price * 0.9;
		}
		return (int) price * 0.8;
	}
}

再比如,书店增加了计算机类的图书,这类书有除了原来书有的特性之外还加了一个特性,就是面向什么领域。
public interface IComputerBook extends IBook {
	String scope();
}

public class ComputerBook implements IComputerBook {
	public int price;
	public String name;
	public String author;
	public String scope;

	public Book(int price, String name, String author, String scope) {
		this.price = price;
		this.name = name;
		this.author = author;
		this.scope = scope;
	}

	public int price() {
		return price;
	}
	public String name() {
		return name;
	}
	public String author() {
		return author;
	}
	public String scope() {
		return scope;
	}
}
通过创建IComputerBook接口并继承IBook接口实现新增面向领域的特性,然后创建ComputerBook实现类继承IComputerBook,
客户端只要相应调用需要的实体类即可完成业务实现,整个过程我们通过扩展实现了业务需求,降低了风险。

图示:

在这里插入图片描述

3 - 里氏替换原则

核心思想:

子类必须能够替换基类。将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。

主要原则:

a. 子类可以实现父类的抽象方法,但不能覆盖父类非抽象的方法。(父类中已实现的方法其实是一种已经定好的规范或者锲约,如果进行重写及随意修改,可能就会带来意想不到的错误)
b. 子类中可以增加自己特有的方法。
c. 当子类的方法重载父类方法时,形参要比父类方法的形参更宽广。
d. 当子类方法实现父类抽象方法时,返回参数要比父类更精确。

示例:

public abstract class Animal {
	public abstract void eat();
	
	public void walk() {
		System.out.println("我要去跑步了");
	}
	
	public void swim() {
		System.out.println("我要去海里抓鱼了");
	}
	
	public void work(HaskMap map) {
		System.out.println("工作");
	}
	
	public abstract Map think(HashMap map);
}

public class Cat extends Animal {
	// 正常实现父类中的抽象方法。没有问题。
	@Override
	public void eat() {
		System.out.println("我要开始吃饭");
	}
	
	// 此处重写了父类的 walk 方法,将父类原有的跑步改成了抓老鼠,这就与里氏替换原则互相违背了。
	public void walk() {
		System.out.println("我要去抓老鼠");
	}
	// 此处也是,重写了父类的 swim 方法,本来不该有的东西因为重写了父类的非抽象方法而出现了问题。
	public void swim() {
		System.out.println("我要去海里抓鱼了");
	}
	
	// 子类中可以增加自己特有的方法。没有问题。
	public void sleep() {
		System.out.println("我要睡觉");
	}
	
	/** 
	 * Cat类的work方法形参要比父类更宽松,所以当参数输入为HashMap类型时,
	 * 只会执行父类的方法,不会执行Cat中的work方法,这是符合里氏替换原则的。
	 */
	public void work(Map map) {
		System.out.println("工作");
	}
	
	/**
	 * 当Cat重写其父类的think方法时,其返回值为HashMap,而父类返回子类型是Map,
	 * 子类比父类的返回值范围更小,更严格。如果子类返回值类型比父类还要大,在子类重写该方法时编译器就会报错。
	 */
	public HashMap think(HashMap map) {
		return new HashMap;
	}	
}

图示:

在这里插入图片描述

4 - 接口隔离原则

核心思想:

使用多个小的专门的接口,而不要使用一个大的总接口。
具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。

主要原则:

不能强迫用户去依赖那些他们不使用的接口。

接口端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
如果一个提供接口的类中对于它的子类来说不是最小的接口,那么它的子类在实现该类的时候就必须实现一些自己不需要的功能,整个系统就会慢慢变得臃肿难以维护。
分离的手段主要有以下两种:1、委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。2、多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。

示例:

// 大接口:::
public interface IExam {
	void chinese();
	void math();
	void physics();
	void geograp();
}

public class ArtsExam implements IExam {
	public void chinese() {
		System.out.println("语文");
	}
	public void math() {
		System.out.println("数学");
	}
	public void physics() {
	}
	public void geograp() {
		System.out.println("地理");
	}
}

public class PhyExam implements IExam {
	public void chinese() {
		System.out.println("语文");
	}
	public void math() {
		System.out.println("数学");
	}
	public void physics() {
		System.out.println("物理");
	}
	public void geograp() {
	}
}
// 上述代码首先创建了一个考试接口类,里面包含语文,数学,地理,物理,然后创建了两个实现类。
// 文科类,理科类,从代码可以发现,理科类中地理方法是空的,文科类里物理方法是空的。
// 如果后续还要其他科目,就会造成空的方法更多,这是不合理的。

// 根据接口隔离原则将接口改造的更合理,代码如下:::
public interface IExam {
	void chinese();
	void math();
}

public interface IArtsExam extends IExam {
	void geograp();
}

public interface IPhyExam extends IExam {
	void physics();
}

public class ArtsExam implements IArtsExam {
	public void chinese() {
		System.out.println("语文");
	}
	public void math() {
		System.out.println("数学");
	}
	
	public void geograp() {
		System.out.println("地理");
	}
}

public class PhyExam implements IPhyExam {
	public void chinese() {
		System.out.println("语文");
	}
	public void math() {
		System.out.println("数学");
	}
	public void physics() {
		System.out.println("物理");
	}
}
// 通过按照接口隔离原则进行改造,代码更加合理.
// 但是在使用接口隔离原则的时候,还是需要根据情况来控制接口的粒度,
// 接口太小会引起系统中接口泛滥,不利于维护;太大则有违背了接口隔离规则,容易出现“胖大”接口。
// 所以一般接口中只为被依赖的类提供定制的方法即可,不要让客户去实现他们不需要的方法。

图示:

在这里插入图片描述

5 - 依赖倒置原则

核心思想:

依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。

主要原则:

a. 高层次的模块不应该依赖低层次的模块,二者都应该依赖于抽象。
b. 抽象不应该依赖于细节,细节应该依赖于抽象。
一般来说,抽象的变化很小,让客户端依赖于抽象,实现类也依赖于抽象即使实现类进行了变动,只有抽象没变,客户端就不需要发生变化,从而较低了耦合度,使代码更加健壮。

示例:

// 例如一个公司是摩拜单车和ofo单车的合作伙伴,现在开发一套自动定位系统,只要自行车上装上该系统,
// 就可以实时定位车主的具体位置。常规写法代码示例:
public class OfoBike {
	public void gps() {
		System.out.println("ofo 位置");
	}
}

public class MoBike {
	public void gps() {
		System.out.println("摩拜位置");
	}
}

public class AutoSystem {
	private OfoBike ofoBike;
	private MoBike moBike;
	private String type;
	
	public AutoSystem(String type) {
		this,type = type;
		this.ofoBike = new OfoBike();
		this.moBike = new Mobike();
	}

	public void auto() {
		if("ofo".equals(type)){
			ofoBike.gps();
		} else {
			moBike.gps();
		}
	}
}
// 通过代码可知,用if语句进行盘虽然简单,但不利于扩展,
// 如果后续这套自动系统可以支持更多类型自行车,那就要改变很多代码,写很多if语句,效率就很低。

// 改造后的代码:::
public interface IBike() {
	void gps();
}

public class OfoBike implements IBike {
	public void gps() {
		System.out.println("ofo 位置");
	}
}

public class MoBike implements IBike {
	public void gps() {
		System.out.println("摩拜位置");
	}
}

public class AutoSystem {
	private IBike iBike;
	
	public AutoSystem(IBike iBike) {
		this,iBike = iBike;
	}

	public void auto() {
		iBike.gps();
	}
}

// 首先发现OfoBike和MoBike都有共同的方法gps(),于是创建接口类IBike,然后这两个子类实现IBike接口。
// 同时自动系统类也将之前依赖于两个实现类改成依赖于接口,通过接口,这样就符合接口隔离原则了,即使后续加上凤凰单车,捷安特等都可以不用更改原有代码,通过扩展实现功能。

图示:

在这里插入图片描述
你会把灯直接焊接到墙上的电线上吗?

6 - 总结

理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;接口隔离原则指导我们在设计接口的时候要精简单一;依赖倒置原则指导我们要面向接口编程。

设计模式就是通过这些个原则,来指导我们如何做一个好的设计。但是设计模式不是一套“奇技淫巧”,它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。接下来我们将通过外卖营销业务的实践,来探讨如何用设计模式来实现可重用、易维护的代码。

后来面向对象的设计模式又提出了七大基本原则:
开闭原则(Open Closed Principle,OCP)
单一职责原则(Single Responsibility Principle, SRP)
里氏代换原则(Liskov Substitution Principle,LSP)
依赖倒转原则(Dependency Inversion Principle,DIP)
接口隔离原则(Interface Segregation Principle,ISP)
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)

7 - 参考资料

面向对象的5个基本设计原则
面向对象五大基本原则详解
面向对象设计五大原则SOLID
设计模式在外卖营销业务中的实践

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值