面向对象五大基本原则理解(SOLID)

本文深入探讨了面向对象设计的五大原则:单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。通过实例解释了如何应用这些原则来编写可扩展、易理解和可测试的代码,强调了类的职责分离、接口解耦、子类与父类的兼容性以及依赖抽象的重要性。

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

目录

五大基本原则

单一职责原则(The Single Responsibility Principle)

开闭原则(The Open-Closed Principle)

里氏替换原则(Liskov Substitution Principle)

接口隔离原则(Interface Segregation Principle)

依赖倒置原则(Dependency Inversion Principle)


五大基本原则

  • 单一职责原则(The Single Responsibility Principle)
  • 开闭原则(The Open-Closed Principle)
  • 里氏替换原则(The Liskov Substitution Principle)
  • 接口隔离原则(The Interface Segregation Principle)
  • 依赖倒置原则(The Dependency Inversion Principle)

这里的原则可以理解为前辈们在无数实践下总结出来的经验,大部分场合都是适用的。

原则与原则之间并非孤立的存在,需要结合起来理解。

但不管怎么样,我们的目的是不变的:为了编写易于拓展、易于理解、可测试的代码。

本文中的代码示例大多出自:The SOLID Principles of Object-Oriented Programming Explained in Plain English

 外加一些个人理解。

单一职责原则(The Single Responsibility Principle)

一个类只应该做一件事,因此这个类只有一个改变的理由。

其实可以这么理解:不要把鸡蛋放在一个篮子里。
每个类应该只实现一个大的功能点,不应该把很多功能全部糅杂在一个类里面。

例如有一个类Book

class Book {
	String name;
	String authorName;
	int year;
	int price;
	String isbn;

	public Book(String name, String authorName, int year, int price, String isbn) {
		this.name = name;
		this.authorName = authorName;
		this.year = year;
        this.price = price;
		this.isbn = isbn;
	}
}

还有一个类Invoice(发票),它有很多的功能,例如计算书本的总价、打印发票、保存到文件中等等。

public class Invoice {

	private Book book;
	private int quantity;
	private double discountRate;
	private double taxRate;
	private double total;

	public Invoice(Book book, int quantity, double discountRate, double taxRate) {
		this.book = book;
		this.quantity = quantity;
		this.discountRate = discountRate;
		this.taxRate = taxRate;
		this.total = this.calculateTotal();
	}

	public double calculateTotal() {
	        double price = ((book.price - book.price * discountRate) * this.quantity);

		double priceWithTaxes = price * (1 + taxRate);

		return priceWithTaxes;
	}

	public void printInvoice() {
            System.out.println(quantity + "x " + book.name + " " +          book.price + "$");
            System.out.println("Discount Rate: " + discountRate);
            System.out.println("Tax Rate: " + taxRate);
            System.out.println("Total: " + total);
	}

        public void saveToFile(String filename) {
	// Creates a file with given name and writes the invoice
	}

}

上面的代码因为功能实现的比较简单,代码看起来还不是很长。

试想一下,如果每个方法的逻辑更加复杂,在public方法的基础上可能还要再加上一些private方法。随着复杂程度的提升,“需求变更”带来的威胁将越来越大。

“小张啊,我觉得你的计算规则太落后了,我这有个新的计算规则,你来实现下”,“存到文件还不够,我还想存到数据库里”。。。。。。

每一次需求变更都迫使你重新阅读这个类的代码。尤其是多人合作的时候,更容易出错。

归根揭底都是这个类太“累”了,承受了不属于这个年纪应该承受的压力。

这个时候就需要给这个类“减负”了。

这个“减负”的过程,也叫做“解耦”。

编写InvoicePrinter类用于打印发票

public class InvoicePrinter {
    private Invoice invoice;

    public InvoicePrinter(Invoice invoice) {
        this.invoice = invoice;
    }

    public void print() {
        System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
        System.out.println("Discount Rate: " + invoice.discountRate);
        System.out.println("Tax Rate: " + invoice.taxRate);
        System.out.println("Total: " + invoice.total + " $");
    }
}

编写InvoicePersistence用于存储。

public class InvoicePersistence {
    Invoice invoice;

    public InvoicePersistence(Invoice invoice) {
        this.invoice = invoice;
    }

    public void saveToFile(String filename) {
        // Creates a file with given name and writes the invoice
    }
}

类职责的单一化,也能带来其他的好处,例如每一个功能点都可以有多种实现策略,当需求变更时,只要切换策略即可。

开闭原则(The Open-Closed Principle)

类应该对拓展开放,对修改关闭

SRP原则可以让一个功能点的修改不影响到其他的功能点。

而开闭原则则有着更高一点要求,我们在新增方案时,可以复用之前的方案、可以拓展之前的方案,但是不能直接修改之前的方案。

通过拓展一个类的方式,来规避修改一个类可能带来的风险。

如上面的类InvoicePersistence:

假设需要新增一种基于数据库的存储方式,如果直接在InvoicePersistence上做补充

public class InvoicePersistence {
    Invoice invoice;

    public InvoicePersistence(Invoice invoice) {
        this.invoice = invoice;
    }

    public void saveToFile(String filename) {
        // Creates a file with given name and writes the invoice
    }

    public void saveToDatabase() {
        // Saves the invoice to database
    }
}

InvoicePersistence本身就是一个大的功能点——“存储”(save),但是这个功能点的实现方案有两种,所以我们可以先定义功能接口

interface InvoicePersistence {

    public void save(Invoice invoice);
}

再分配实现两种方案

public class DatabasePersistence implements InvoicePersistence {

    @Override
    public void save(Invoice invoice) {
        // Save to DB
    }
}
public class FilePersistence implements InvoicePersistence {

    @Override
    public void save(Invoice invoice) {
        // Save to file
    }
}

两种方案之间互相独立,互不影响。

调用方需要使用到发票存储功能时,只要持有InvoicePersistence类型的引用,即可借助Spring之类的对象管理工具快速完成策略的替换,调用方无需与具体的实现方案耦合。

说到底,这又是一种“解耦”,将实现方案与实现方案解耦、调用方(客户端)与具体服务解耦。

里氏替换原则(Liskov Substitution Principle)

子类(派生类)可以替换他们的基类

 当我们定义一个子类时,通常是为了拓展基类的功能。“拓展”意味着保留原有的功能,在原有的功能基础上,加一些定制化的功能。但如果随意修改原有的功能,将会带来语义上的不明确,以及一些奇奇怪怪的bug。

以JDK中的List为例

 

List代表着一种特征,List种定义了很多的接口,用于维护这种特征。

所有List的实现类,都需要满足这种特征。不管是基于链表的LinkedList,还是基于数组的ArrayList,都是一种List。当客户端调用size()方法时,只能返回这个List的长度,而不是容量;调用get(int) 方法时,只能是获取指定下标的元素,至于获取方式可以定制化,数组的随机读取亦或者是链表的遍历读取都可以。

基类的方法(接口亦是),起到约束功能的作用,当子类覆盖(实现)父类方法时,可以采用不同的实现方式,但是总体方向不能变。

原文中有一个比较有意思的案例,虽说这个案例有些争议,但不影响我们学习这种思想。

在数学上,正方形是一种特殊的长方形。

定义一个类:Rectangle(长方形)

class Rectangle {
	protected int width, height;

	public Rectangle() {
	}

	public Rectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public int getArea() {
		return width * height;
	}
}

定义一个类正方形Square,继承自Rectangle。

class Square extends Rectangle {
	public Square() {}

	public Square(int size) {
		width = height = size;
	}

	@Override
	public void setWidth(int width) {
		super.setWidth(width);
		super.setHeight(width);
	}

	@Override
	public void setHeight(int height) {
		super.setHeight(height);
		super.setWidth(height);
	}
}

测试类

class Test {

   static void getAreaTest(Rectangle r) {
      int width = r.getWidth();
      r.setHeight(10);
      System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
   }

   public static void main(String[] args) {
      Rectangle rc = new Rectangle(2, 3);
      getAreaTest(rc);

      Rectangle sq = new Square();
      sq.setWidth(5);
      getAreaTest(sq);
   }
}

Square由于本身的特征约束(正方形四条边相等),height与width必须相等。

但是这种约束不应该放在set方法中维持,对于客户端来说,Square就是一种Rectangle。用多态来描述就是 Rectangle sq = new Square(),那么Square类型的对象也可以作为getAreaTest(Rectangle r)的合法传参,这个时候就可能得到意料之外的结果。

更好的方式是Square和Rectangle定义成两个独立的类。在现实世界中比较合理的关系,直接抽象成代码时,可能就没那么合理了。

关于长方形和正方形的抽象问题,颇有争议,有兴趣的自行研究下。

里氏替换原则,注重的是子类与父类之间的兼容性。子类继承父类的方法(功能),可以换一种实现方式,但必须达到同样的目的。

接口隔离原则(Interface Segregation Principle)

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

 接口隔离原则很好理解,以停车场ParkingLot为例:

public interface ParkingLot {

	void parkCar();	// Decrease empty spot count by 1  开始停车
	void unparkCar(); // Increase empty spots by 1     结束停车
	void getCapacity();	// Returns car capacity        获取停车场的容量
	double calculateFee(Car car); // Returns the price based on number of hours 
	void doPayment(Car car);
}

class Car {

}

此接口在某种意义上讲,属于“胖接口”。如果现在需要实现一个免费的停车场只能这么做:

public class FreeParking implements ParkingLot {

	@Override
	public void parkCar() {
		
	}

	@Override
	public void unparkCar() {

	}

	@Override
	public void getCapacity() {

	}

	@Override
	public double calculateFee(Car car) {
		return 0;
	}

	@Override
	public void doPayment(Car car) {
		throw new Exception("Parking lot is free");
	}
}

费用计算固定返回0、支付接口直接抛异常,这显然是一种不优雅的做法。究其根本,还是抽象的不够彻底,并非所有的停车场都是付费的,对于“付费”这一特征可以单独抽象出来。

搬个图

 “接口”代表着一种特征,接口内所有的方法构成了这种特征。当特征无法适用于所有子类时,需要进行“隔离”,拆分成更多接口。粒度的细化有利于实现下面即将讲到的“依赖倒置”,也更加符合上面讲的SRP原则。

依赖倒置原则(Dependency Inversion Principle)

我们定义的类应该依赖于接口或者抽象类,不应该依赖具体的类

 当我们平时开发时,习惯性定义一个XXXService,以及对应的实现方法XXXServiceImpl时,是否有想过为什么要这么做。

我曾经也有过这种疑问,当时不明白为什么每写一个service时,都要先定义接口,再写实现类。其实如果能保证需求不会变更,代码后续不会改动,对复用性没有要求,或者每个功能都只有一种实现方式,那也可以不定义接口。但现实往往......你懂的~

在Spring环境下,需要使用某个类的功能时,我们通常会这么写

@Autowired
private XXXService  service;

这其实就是一种依赖倒置思想,当某个类与其他类产生联系时,最好只与接口(或抽象类)做绑定,只依赖接口,不依赖具体的实现类。

 当经历需求变更时,可以在不修改调用方代码的前提下,替换掉之前的XXXServiceImpl,换成XXXServiceImpl2、XXXServiceImpl3等等即可。

这也是开闭原则的一种体现。当一个功能需求改变时,最好的结果是替换整个功能的实现(或者说做成策略),而不是牵一发动全身,一处修改,处处修改。

如有错误,欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值