面向对象设计原则 7 种
作者:@kuaiquxie
作者的github:https://github.com/bitebita
本文为作者原创,如需转载,请注明出处:https://www.cnblogs.com/dzwj/p/16984033.html
面向对象设计原则
在进行软件开发时,不仅需要将基本的业务完成,还要考虑整个项目的可维护性和可复用性。
因此在编写代码时,应该尽可能的规范,不然随着项目的不断扩大,整体结构只会越来越遭。
为了避免这种情况的发生,应该尽量遵守面向对象设计原则。
1. 单一职责原则(Simple Responsibility Principle,SRP)
是最简单的面向对象设计原则,它用于控制类的粒度大小。
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
// 程序员
class Coder{
// 编程
public void coding(){
}
}
// 工人
class Worker{
// 打螺丝
public void work(){
}
}
// 骑手
class Rider {
// 送外卖
public void ride(){
}
}
在设计 Mapper、Service、Controller 等都应该采用单一职责原则根据不同的业务划分,作为实现高内聚低耦合的指导方针。
实际上微服务也是参考了单一职责原则,每个微服务只应担负一个职责。
2. 开闭原则(Open Close Principle)
是重要的面向对象设计原则。
软件实体应当 对扩展开放,对修改关闭。
一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭。
其中,对扩展开放是针对提供方来说的,对修改关闭是针对调用方来说的。
比如程序员分为前端程序员、后端程序员,他们要做的都是去打代码,具体如何打代码根据不同语言的程序员决定。
将程序员打代码的行为抽象成统一的接口或抽象类,就满足了开闭原则的第一个要求:对扩展开放。
哪个程序员使用什么语言怎么编程,是自己在负责,不需要其他程序员干涉,就满足第二个要求:对修改关闭。
// 程序员
public abstract class Coder {
public abstract void coding();
// Java程序员
class JavaCoder extends Coder{
通过提供一个 Coder 抽象类,定义出编程的行为,但是不进行实现,而是开放给其他具体类型的程序员来实现。这样就可以根据不同的业务进行灵活扩展了,具有较好的延续性。
3. 里氏替换原则(Liskov Substitution Principle)
所有引用基类的地方必须能透明地使用其子类的对象。
简单的说就是,子类可以扩展父类的功能,但不能改变父类原有的功能:
-
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
-
子类可以增加自己特有的方法。
-
子类的方法重载父类的方法时,方法的前置条件(方法的输入/形参)要比父类方法的输入参数更宽松。
-
子类的方法实现父类的方法时(重写/重载/实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格(与父类一样)。
// 程序员
public abstract class Coder {
// 写代码
public void coding() {
}
// Java程序员
class JavaCoder extends Coder{
// 打游戏
public void game(){
}
// 写代码
这里对父类的方法进行了重写,父类的行为就被子类覆盖了,这个子类已经不具备父类的原本的行为,违背了里氏替换原则。
对于这种情况,我们不需要再继承自 Coder 了,可以提升一下,将此行为定义到 People 中:
// 人类
public abstract class People {
// 写代码。这个行为还是定义出来,但是不实现
public abstract void coding();
// 程序员
class Coder extends People{
// 写代码
里氏替换也是实现开闭原则的重要方式
4. 依赖倒转原则(Dependence Inversion Principle)实现低耦合
是我们一直在使用的,最明显的就是 Spring 框架了。
高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。
高层模块,底层模块,细节 都应该依赖于抽象
1.各模块强关联
public class UserController {
UserService service = new UserService();
// 调用服务
static class UserService {
UserMapper mapper = new UserMapper();
// 业务代码......
}
static class UserMapper {
// CRUD......
}
}
公司业务需求变化
public class UserController {
UserServiceNew service = new UserServiceNew();
// 调用服务
// 服务发生变化,新的方法在新的服务类中
static class UserServiceNew {
UserMapper mapper = new UserMapper();
// 业务代码......
}
static class UserMapper {
// CRUD......
}
}
各个模块之间是强关联的,一个模块是直接指定依赖于另一个模块。虽然这样结构清晰,但是底层模块的变动,会直接影响到其他依赖于它的高层模块。如果项目很庞大,这样的修改将是一场灾难。
2.spring框架
public class Main {
public static void main(String[] args) {
UserController controller = new UserController();
}
interface UserMapper {
// 接口中只做 CRUD 方法定义
}
static class UserMapperImpl implements UserMapper {
// 实现类完成 CRUD 具体实现
}
interface UserService {
// 业务接口定义......
}
static class UserServiceImpl implements UserService {
// 现在由Spring来为我们选择一个指定的实现类,然后注入,而不是由我们在类中硬编码进行指定
通过使用接口,将原有的强关联给弱化,只需要知道接口中定义了什么方法然后去使用即可。而具体的操作由接口的实现类来完成,并由 Spring 来为我们注入,而不是我们通过硬编码的方式去指定。
5. 接口隔离原则(Interface Segregation Principle, ISP)实现高内聚
实际上是对接口的细化。
客户端不应依赖那些它不需要的接口。
定义接口的时候,注意接口的粒度,细化接口,不然就是接口不匹配
// 电子设备
interface Device {
// 获取 CPU 信息
String getCpu();
// 获取类型
String getType();
// 获取内存
String getMemory();
}
// 电脑是一种电子设备,那么就实现此接口
class Computer implements Device {
接口粒度细化
// 智能设备
interface SmartDevice {
// 获取 CPU 信息
String getCpu();
// 获取类型
String getType();
// 获取内存
String getMemory();
}
// 普通设备
interface NormalDevice {
// 获取类型
String getType();
}
// 电脑是一种智能设备,继承智能设备接口
class Computer implements SmartDevice {
6. 合成复用原则(Composite Reuse Principle)实现低耦合
核心就是委派
优先使用对象组合,而不是通过继承来达到复用的目的。
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。在考虑将某个类通过继承关系在子类得到父类已经实现的方法时,应该先考虑使用合成的方式来实现复用。
1.继承
class A {
public void connectDatabase(){
System.out.println("我是连接数据库操作!");
}
}
// 直接通过继承的方式,得到 A 的数据库连接逻辑
class B extends A{
public void test(){
System.out.println("我是B的方法,我也需要连接数据库!");
// 直接调用父类方法
connectDatabase();
}
}
耦合度太高了
通过继承的方式实现复用,是将类 B 直接指定继承自类 A 的。如果有一天,由于业务的更改,数据库连接操作不再由A来负责,而是由C去负责。就不得不将需要复用 A 中方法的子类全部进行修改,这样是费时费力的。并且还有一个问题,通过继承子类会得到一些父类中的实现细节,比如某些字段或是方法,这样直接暴露给子类,并不安全。
-
对象组合 (通过对象之间的组合,我们就大大降低了类之间的耦合度,并且 A 的实现细节,B 也不会直接得到了。)
将类对象 作为 形参 传入(或者 作为对象直接传入) 或者 抽象成为一个接口(更灵活)
形参
class A {
public void connectDatabase(){
System.out.println("我是连接数据库操作!");
}
}
// 不进行继承,而是在用的时候给我一个 A,当然也可以抽象成一个接口,更加灵活
class B {
public void test(A a){
System.out.println("我是B的方法,我也需要连接数据库!");
// 通过对象 A 去执行
a.connectDatabase();
}
}
传入对象
class A {
public void connectDatabase(){
System.out.println("我是连接数据库操作!");
}
}
class B {
A a;
// 在构造时就指定好
public B(A a){
this.a = a;
}
public void test(){
System.out.println("我是B的方法,我也需要连接数据库!");
// 通过对象 A 去执行
a.connectDatabase();
}
}
7. 迪米特法则(Law of Demeter)实现低耦合
又称最少知识原则,是对程序内部数据交互的限制。
每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
一个类/模块对其他的类/模块有越少的交互越好。当一个类发生改动,与其相关的类需要尽可能少的受影响。这样我们在维护项目的时候会更加轻松一些。其实本质还是降低耦合度
-
传入整个对象
public class Main {
public static void main(String[] args) throws IOException {
// 假设我们当前的程序需要进行网络通信
Socket socket = new Socket("localhost", 8080);
Test test = new Test();
// 现在需要执行 test 方法来做一些事情
test.test(socket);
}
static class Test {
// 比如 test 方法需要得到我们当前 Socket 连接的本地地址
public void test(Socket socket){
System.out.println("IP地址:" + socket.getLocalAddress());
}
}
}
虽然这种写法没有问题,直接提供一个 Socket 对象供使用,然后再由 test 方法来取出 IP 地址。但是这样显然违背了迪米特法则,实际上这里的 test
方法只需要一个 IP 地址即可。完全可以只传入一个字符串,而不是整个 Socket 对象,这样就保证了与其他类的交互尽可能的少。要是某一天,Socket 类中的这些方法发生修改了,那我们就得连带着去修改这些类,很麻烦。
就像在餐厅吃完了饭,应该是自己扫码付款,而不是直接把手机交给老板来帮你操作付款。
2.只传入该函数所需要的,一个字符串 (或者传入一个对象的其中一个属性)
public class Main {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8080);
Test test = new Test();
// 在外面解析好再传入
test.test(socket.getLocalAddress().getHostAddress());
}
static class Test {
// 一个字符串就搞定了
public void test(String str){
System.out.println("IP地址:"+str);
}
}
}
这样类与类之间的耦合度降低
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南