Java 学习笔记 面向对象的七大设计原则

本文介绍了Java编程中的面向对象设计七大原则,包括单一职责原则、开闭原则、里氏替换原则、依赖倒转原则、接口隔离原则、合成复用原则和迪米特法则。详细阐述了每个原则的含义、应用示例和遵循这些原则的重要性,旨在提升代码的可维护性和可扩展性。

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

参考资料


参考资料:视频资料

面向对象设计,Object Oriented Design,简称OOD。

在进行软件开发时,需要考虑项目的可维护性和可复用性,开发项目一般是由一个开发团队来维护,因此我们在编写代码时,应可能规范,防止项目出现越来越多的BUG。

一、单一职责原则 SRP


参考资料:查看文档

单一职责原则, Single Responsibility Principle 又称单一功能原则,是最简单的面向对象设计原则,用于控制类的粒度大小。
出自《敏捷软件开发:原则、模式与实践》 Robert C. Martin

一个对象应该只包含单一的职责,并且该职责被完整的封装在一个类中

例:用类描述人们的会做的事情

  • 反例:违背 SRP 单一职责原则
public class People{
    // 程序员: 敲代码
    public void coding(){}
    // 飞行员: 驾驶飞机
    public void pilot(){}
}

类中出现了范围偏多的敲代码和驾驶飞机,实际上,生活中很少有人同时具备这两项技能,故根据 SRP 原则,上述的类应变更为:

  • 正例:符合 SRP 原则
// 程序员
class Programmer{
     public void coding(){}   
}

// 飞行员
class Piloter{
     public void pilot(){}   
}

总结:SRP 单一职责原则 要求在设计类的时候,里面的方法或属性是同一类的,颗粒度尽可能小的。

二、开闭原则 OCP


参考资料:查看文档
开闭原则,Open Close Principle,OCP 原则规定软件中的对象(类、模块、函数等)应对扩展开放,对修改关闭。
出自:《面向对象软件构造》

软件对扩展(提供方)开放,对修改(调用方)关闭

正例:用类描述不同编程语言领域的程序员

public abstract class Programmer{
     public abstract void Codding(); 
}

// Java程序员
class JavaProgrammer extends Programmer{
    @Override
    public void coding(){}
}

// Python 程序员
class PythonProgrammer extends Programmer{
    @Override
    public void coding(){}
}

// C++语言程序员
class CppProgrammer extends Programmer{
    @Override
    public void coding(){}
}

通过提供一个 Programmer 抽象类,定义出编程的抽象行为,这样给其他具体类型的程序员来实现,这样就可以根据不同的业务进行灵活扩展,具有更好的扩展性。除了抽象类,更多的则是使用接口和接口实现类来遵循OCP原则

三、里氏替换原则 LSP


参考资料:查看文档

里氏替换原则,Liskov Substitution Principle,LSP 原则是对子类型的特别定义。

出自:演讲《数据的抽象与层次》 Barbara Liskov

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

简单来说,子类可以扩展父类的功能,但不能改变父类原有的功能:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类原有的功能
  2. 子类可以增加自己持有的方法
  3. 子类重载父类方法时,方法的前置条件(即方法的入参)要比父类的该方法入参更宽松
  4. 子类实现父类方法时,(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样

例: 描述一个Java程序员会做的事情
反例:违背 LSP 原则

public abstract class Programmer{
    public void coding(){}  
}

// 描述会经常运动的Java程序员
class JavaProgrammer extends Programmer{   
    // 违背 LSP 原则,重写了父类非抽象的方法。
    public void codding(){}
    // 拓展功能: 做运动
    public void doSport(){}
}

正确例子:遵循 以上三种原则

public abstract class Programmer{
    public abstract void coding(){}  
    // 程序员都需要休息
    public void sleep(){}
}

// 描述会经常运动的Java程序员
class JavaProgrammer extends Programmer{
    
    // public void sleep() , 遵循 LSP原则,不覆盖原有的功能, 除非sleep()为抽象的方法
    // 重写父类方法
    @Override
    public void codding(){}
    // 拓展功能: 做运动
    public void doSport(){}
}

四、依赖倒转原则 DIP


依赖倒转原则,Dependence Inversion Procinple,DIP原则指出程序要依赖抽象接口,不要依赖于具体实现。这在Spring框架中得到了广泛运用。

高层模块不应依赖于底层模块。抽象不应依赖于细节,细节应依赖于抽象。

以 JavaEE开发中,传统的MVC架构为例:
反例:违背了 DIP 原则

public class Solution{
    public static void main(String[] args){
        UserController controller = new UserController();
        // ... 具体的应用
    }
    static class UserMapper {
        // CRUD   
    }
    static class UserService{
        UserMapper mapper = new UserMapper();
        // 业务层代码
    }
    static class UserController{
         UserServiceservice = new UserService();
        // 控制层代码
    }
}

在上述代码中,假如UserMapper() 发生变更,那么 类UserService就需要进行重构,由于UserController依赖于UserService,故UserController也需要进行重构。

public class Solution{
    public static void main(String[] args){
        UserController controller = new UserController();
        // ... 具体的应用
    }
    static class UserMapperNew {
        // CRUD   
    }
    static class UserService{
        // mapper 发生改变,业务层代码需进行重构
        UserMapperNew mapper = new UserMapperNew();
        // 业务层代码
    }
    static class UserController{
        // service 发生改变,控制层代码需进行重构
        UserServiceservice = new UserService();
        // 控制层代码
    }
}

综上,在传统MVC架构中,Controller模块依赖于 Service模块,而Service模块依赖于Mapper模块,这样一来结构清晰,但底层模块的变动,会直接影响其他依赖于该模块的高层模块,以下是Spring框架中的代码,高内聚,低耦合:

public class Solution{
    public static void main(String[] args){
        UserController controller = new UserController();
        // ... 具体的应用
    }
    interface UserMapper {
        // 接口声明CRUD方法
    }
    @Mapper
    static class UserMapperImpl implements UserMapper{
        // 实现类完成CURD具体实现
    }
    interface UserService{
        // 接口声明业务层方法
    }
    @Service
    class UserServiceImpl implements UserService{
         // 实现类完成业务层具体实现   
    }
    @Controller
    class UserController{
        @Resource
        private UserService service;	// 直接引入Spring IOC 容器中的接口
        // 控制层代码
    }
}

通过上述代码可以看出,我们可以将原有的强关联弱化,只需要知道接口中定义了什么方法然后去使用即可,而具体的操作由接口的实现类来完成,并由Spring注入接口的实现类,而不是通过之前硬编码的方式去定义。

五、接口隔离原则 ISP


参考资料:查看文档

接口隔离原则,Interfacce Segregation Principle ,ISP 原则是对接口的细化。

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

例:在定义接口的时候,一定要注意控制接口的粒度

// 表示设备的接口
interface Device{
    String getCpu();
    String getType();
    String getMemory();
}
// 电脑设备
class Computer implements Device{
    @Override
    public String getCpu(){
        return "i5";
    }
    @Override
    public String getType(){
         return "电脑";   
    }
    @Override
    public String getMemory(){
         return "16G";   
    }
}
// 电风扇设备
class Fan implements Device{
    @Override
    public String getCpu(){
         return null;   
    }
    @Override
    public String getType(){
         return "风扇";   
    }
    @Override
    public String getMemory(){
         return null;   
    }
}
如上述代码,定义的 Device 接口粒度不够细,不能适用于多种设备。故需要再进行划分,如下所示:
interface SmartDevice{	// 智能设备
    String getCpu();
    String getType();
    String getMemory();
}
interface NormalDevice{ // 普通设备
    String getType();
}
class Computer implements SmartDevice{
    ...
}

class Fan implements NomarlDevice{
    ...   
}
如此一来,接口更加细化,符合了ISP 的设计原则。

六、合成复用原则 CRP


合成复用原则,Composite Reuse Principle ,CRP 原则的核心是委派。

优先使用对象组合,而不是通过继承来达到复用的目的

如果定义的类C需要用到类A的功能,那么应该优先考虑使用 **合成 **的方式来实现复用。
反例:使用继承来实现复用

class A{
    public void connect(){ }    
}

class C extends A{
    public void test(){
         connnect();   
    }
}

存在的问题:

  • 代码耦合度过高,若类A发生改变,那么类C需要进行重构。
  • 安全性低,类C将会拥有类A所有的方法和变量,不安全。

遵循 合成服用原则(CRP)后:

class A{
    public void connect(){ }    
}

class C {
    public void test(A a){
         a.connnect();   
    }
}

或者是将类A定义到类C中:

class A{
    public void connect(){ }    
}

class C {
    A a;
    public C(A a){
         this.a = a;   
    }
    public void test(){
         a.connnect();   
    }
}
通过对象之间的组合,降低了类之间的耦合度。

七、迪米特法则 LOD


迪米特法则,Law of Demeter ,LOD法则又称最少知识原则,是对程序内部数据交互的限制。

每一个软件单位对其他单位都只有最少的知识,而且局限于那些本单位密切相关的软件单位。

简单说,一个类 / 模块对其他的类 / 模块之间交互越少越好。当一个类发生改动,与之相关的类会受到影响,所以交互越少越好,便于降低耦合度。

public class Main{
     public static void main(String[] args) throws IOException{
          Socket socket = new Socket("localhost", 8080);
          
          new Test().test(socket);
     }
    static class Test{
         public void test(Socket socket){
             System.out.println(socket.getLocalAddress());
         }
    }
}
上面这种写法每问题,但是在调用 test方法时,需要传入一个Socket对象,在这里我们的逻辑只是输出socket的IP地址,不符合 LOD 设计原则,可以简化如下:
public class Main{
     public static void main(String[] args) throws IOException{
          Socket socket = new Socket("localhost", 8080);
          new Test().test(socket.getLocalAddress());
     }
    static class Test{
         public void test(String str){
             System.out.println(str);
         }
    }
}

八、总结


通过本次学习,我了解了面向对象的七大设计原则,对我来说,一次性记住是不太现实的,需要在平时编码里时刻注意,比如在完成一段代码后,可以思考一下这个是关于什么设计原则的,或者在看框架源码时,去体会源码设计的方式,从而更深地理解七大设计原则,接下来我将学习23种设计模式,希望通过这样的方式,能提升一定的编码水平。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
OSZAR »