单一职责原则
单一职责原则是指责任应该分配到单一的类或者模块中,使得这个类或者模块只需要负责单一的职责。这样可以提高代码的可维护性和可重用性,减少不必要的耦合。
下面用一个简单的例子来演示:
一个在线商城系统,其中订单(Order)是系统中的一个重要概念,我们来看看如何遵循单一职责原则进行设计。
首先,订单(Order)具有哪些职责呢?
- 记录订单编号、下单时间、总价等属性;
- 包含购买的商品明细,即一组订单项(OrderItem);
- 处理订单状态的流转,例如确认订单、支付订单、发货、完成等。
根据单一职责原则,我们需要将这些职责分别分配给不同的类或模块。
对于第一个职责,即记录订单基本属性的部分,我们可以设计一个名为 Order 的类,表示一个订单实例。示例代码如下:
public class Order {
private String orderId;
private Date orderTime;
private BigDecimal totalPrice;
// 构造函数、getter 和 setter 等
// ...
// 计算总价
public void calculateTotalPrice() {
// ...
}
}
对于第二个职责,即包含购买的商品明细的部分,我们可以设计一个名为 OrderItem 的类,表示一个订单项,同时在 Order 类中使用 List 来存储所有的订单项。示例代码如下:
public class OrderItem {
private String productId;
private int quantity;
private BigDecimal price;
// 构造函数、getter 和 setter 等
// ...
}
public class Order {
private String orderId;
private Date orderTime;
private BigDecimal totalPrice;
private List<OrderItem> orderItems;
// 构造函数、getter 和 setter 等
// ...
}
对于第三个职责,即处理订单状态的流转的部分,我们可以将其分配给一个名为 OrderService 的服务类,其中包含各种订单操作方法,例如确认订单、支付订单、发货、完成等。示例代码如下:
public class OrderService {
public void confirmOrder(Order order) {
// ...
}
public void payOrder(Order order) {
// ...
}
public void shipOrder(Order order) {
// ...
}
public void completeOrder(Order order) {
// ...
}
}
通过将订单(Order)的不同职责分配给不同的类或模块,我们遵循了单一职责原则的要求,使得代码更加清晰、可维护和可重用。
开闭原则
开闭原则是指一个软件实体应该对扩展开放,对修改关闭。这意味着一个模块、类或者方法等在需要增加新功能时,应该通过扩展来实现,而不是修改已有的代码来实现。遵循开闭原则可以使得代码更加健壮和灵活,减少代码的复杂度,并且易于维护和扩展。
例如假设我们要设计一个计算器程序,该程序可以执行加、减、乘、除等运算。首先,我们可以定义一个名为 Arithmetic 的接口,表示进行数学运算的一组操作。该接口只包含一个方法 calculate,用于执行具体的运算。代码如下:
public interface Arithmetic {
double calculate(double num1, double num2);
}
然后,对于每一种运算,我们都可以实现一个相应的类来实现 Arithmetic 接口。例如,实现 Add 类来进行加法运算,Multiply 类来进行乘法运算等。示例代码如下:
public class Add implements Arithmetic {
@Override
public double calculate(double num1, double num2) {
return num1 + num2;
}
}
public class Multiply implements Arithmetic {
@Override
public double calculate(double num1, double num2) {
return num1 * num2;
}
}
// 其他类似的运算类...
现在,如果我们需要新增一种运算,例如取模(Mod)运算,我们不需要修改已有的代码,只需要新增一个相应的实现类就可以了。这样做即符合开闭原则的要求,也避免了对已有代码的影响。示例代码如下:
public class Mod implements Arithmetic {
@Override
public double calculate(double num1, double num2) {
return num1 % num2;
}
}
遵循开闭原则我们保持了代码的稳定性,并且能够容易地扩展新功能,当然在实际项目中我们还要考虑更多其他方面的细节,比如代码的复用、抽象程度等。
里氏替换原则
里氏替换原则是指父类对象可以被子类对象替换,并且不会影响程序的正确性。也就是说,在使用父类对象的地方,应该能够使用其任何子类对象,而不需要修改原有代码。遵循里氏替换原则可以保证程序的稳定性和可扩展性。
假设我们要设计一个图形库,其中包含一系列的图形类型,例如矩形(Rectangle)、正方形(Square)、圆形(Circle)等等。我们定义一个名为 Shape 的抽象类,作为所有图形类型的父类。Shape 类定义了一个名为 draw 的抽象方法,以及一些公共属性和方法。代码如下:
public abstract class Shape {
protected int x;
protected int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public abstract void draw();
// 其他公共方法...
}
然后,我们定义各个具体图形类,并且继承自 Shape 类。这里我们以矩形(Rectangle)和正方形(Square)为例,其中 Square 类继承自 Rectangle 类。代码如下:
public class Rectangle extends Shape {
protected int width;
protected int height;
public Rectangle(int x, int y, int width, int height) {
super(x, y);
this.width = width;
this.height = height;
}
@Override
public void draw() {
// 绘制矩形的逻辑...
}
// 其他方法...
}
public class Square extends Rectangle {
public Square(int x, int y, int sideLength) {
super(x, y, sideLength, sideLength);
}
// 重写父类的方法
@Override
public void draw() {
// 绘制正方形的逻辑...
}
// 其他方法...
}
在这个设计中,Square 类继承自 Rectangle 类,符合里氏替换原则的要求。因为一个正方形也可以被当做一个特殊的矩形来看待,它具有矩形的所有属性和方法,同时又有一些额外的特定属性和方法。
如果我们遵循了里氏替换原则,我们应该能够使用 Shape 类型的对象来替换任何 Rectangle 或者 Square 的对象,而不会影响程序的正确性。例如,我们可以定义一个名为 drawAll 的方法,用于绘制多个图形,在该方法中可以使用 Shape 类型的数组作为参数,其中可以包含 Rectangle 或者 Square 类型的实例。示例代码如下:
public static void drawAll(Shape[] shapes) {
for (Shape shape : shapes) {
shape.draw();
}
}
通过这种设计方式,我们不仅保证了程序的正确性,同时也增加了程序的灵活性和可扩展性。
依赖倒置原则
依赖倒置原则核心思想是高层次模块不应该依赖于低层次模块,而是应该通过抽象来依赖于底层模块。具体来说就是要面向接口或者抽象编程,而不是具体实现类编程,这样做可以减少组件间的耦合,提高系统的灵活性和可维护性。
包括一下几个方面:
- 高层模块不应该依赖低层模块。高层模块和低层模块都应该依赖于抽象,并且抽象不应该依赖于具体实现。
- 抽象不应该依赖于具体实现。抽象应该是稳定的、保持不变的,并且应该提供定义良好的接口。
- 具体实现应该依赖于抽象。具体实现应该通过接口或抽象类来实现并向上层提供稳定的服务。
假设我们有一个汽车销售系统,其中包含了 Salesman 类和 Customer 类,Salesman 类用于销售汽车,而 Customer 类用于购买汽车。下面是代码:
public class Car {
// 汽车的属性和方法...
}
public class Salesman {
public void sellCar(Car car) {
// 销售汽车的逻辑...
}
}
public class Customer {
public void browseCars(List<Car> cars) {
// 浏览汽车的逻辑...
}
public void buyCar(Car car) {
// 购买汽车的逻辑...
}
}
在上面的代码中,Salesman 的 sellCar 方法和 Customer 的 browseCars 和 buyCar 方法都是直接使用了 Car 类作为参数,这就说明了它们依赖了 Car 类。
这样的设计有一个缺点,如果我们要对 Car 类进行修改,那么 Salesman 和 Customer 类的代码也会受到影响,导致系统稳定性下降。因此,在面向对象设计中,依赖倒置原则建议我们避免直接依赖具体实现类,而是应该依赖于抽象,从而降低模块之间的耦合性。
下面是对上面代码进行改进后的示例。我们首先定义了一个名为 CarProvider 的接口,里面有一个 getAllCars 方法用于获取所有汽车的信息。Salesman 类和 Customer 类现在依赖于 CarProvider 接口,而不再直接依赖于 Car 类。
public interface CarProvider {
List<Car> getAllCars();
}
public class Salesman {
public void sellCar(CarProvider provider, Car car) {
// 销售汽车的逻辑...
}
}
public class Customer {
public void browseCars(CarProvider provider) {
List<Car> cars = provider.getAllCars();
// 浏览汽车的逻辑...
}
public void buyCar(CarProvider provider, Car car) {
// 购买汽车的逻辑...
}
}
通过这种方式,Customer 类现在不再依赖于具体的 Car 类,而是依赖于一个提供汽车信息的 CarProvider 接口。这样,如果我们要修改 Car 类的实现,只需要修改它实现的 CarProvider 接口即可,而不必修改 Customer 类的代码,也不用考虑对 Salesman 类的影响。这种方式就是依赖倒置原则的应用。
接口隔离原则
接口隔离原则指类间的依赖关系应该建立在最小的接口上,它强调客户端不应该依赖于它不需要的接口。也就是说,一个类不应该依赖于它不需要的接口,而应该仅依赖于它需要的接口。这样可以避免出现臃肿、冗余的接口,同时减少代码耦合,提高系统的可维护性和扩展性。
接口隔离原则有一下几个具体的实现要点:
- 将大接口拆分成多个小接口,客户端只依赖自己需要的接口。
- 接口设计应该基于实际需求,而不是为了简化实现而进行的合并。
- 不要让客户端依赖他们不需要的接口。
假设我们有一个名为 Shape 的接口,它定义了一个计算图形面积的方法,如下所示:
public interface Shape {
double getArea();
}
现在我们有三种不同的图形:矩形、正方形和圆形。我们可以通过实现 Shape 接口来计算它们的面积,如下所示:
public class Rectangle implements Shape {
// 矩形的属性和方法...
@Override
public double getArea() {
// 计算矩形面积的逻辑...
}
}
public class Square implements Shape {
// 正方形的属性和方法...
@Override
public double getArea() {
// 计算正方形面积的逻辑...
}
}
public class Circle implements Shape {
// 圆形的属性和方法...
@Override
public double getArea() {
// 计算圆形面积的逻辑...
}
}
在这个示例中,我们遵循了接口隔离原则。每个图形类都仅依赖于自己所需的方法,而不是强制性地实现其他方法。
假设我们现在要新增一个三角形类,它计算面积的方式与其他图形不同。如果按照原来的设计,我们需要重新定义 Shape 接口,并让所有类都去实现这个新方法。但是,在遵循接口隔离原则的情况下,我们可以考虑为三角形类定义一个新的接口,如 Triangle,让它只提供计算三角形面积的方法。这样,Shape 接口不需要变化,Triangle 只依赖于自己所需的方法,客户端代码也不用修改,从而避免了对整个系统的影响。
迪米特原则
迪米特原则也叫最小知道原则,它是指一个对象对其他对象保持最少的了解。也就是说一个对象应该尽可能少地依赖其他对象,减少对象间的耦合度,从而提高系统的可维护性和扩展性。
迪米特原则有以下几个实现要点:
- 一个对象应该对其他对象保持最少的了解,仅了解自己相关的对象。
- 对象之间应该通过接口进行通信,而不是直接依赖于具体的实现类。
- 在设计对象方法时,仅传递必要的参数,而不是整个对象。
假设我们有一个名为 Team 的类,它表示一个团队。Team 类需要获取每个成员的姓名和年龄,并且需要统计出所有成员的平均年龄。我们可以定义成员类 Member 如下所示:
public class Member {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
下面是 Team 类的实现:
public class Team {
private List<Member> members = new ArrayList<>();
public void addMember(Member member) {
members.add(member);
}
public double getAverageAge() {
int sum = 0;
for (Member member : members) {
sum += member.getAge();
}
return (double)sum / members.size();
}
public List<Member> getMembers() {
return members;
}
}
在上面的代码中,我们遵循了迪米特原则。Team 类仅依赖于 Member 类提供的 getAge 方法,而不用知道 Member 类的其他信息。这样,如果 Member 类发生变化,只要不影响到 getAge 方法,Team 类不需要进行任何修改。
另外,在实现 Team 类的时候,我们只传递了必要的参数,而不是整个 Member 对象。这样做的好处是,可以尽量减少对象之间的耦合度,防止对象之间的信息交互过于复杂,从而降低系统的复杂度,提高系统的可维护性和可扩展性。