Java中的单一职责原则与示例
SOLID 是一个首字母缩写词,用于指代软件开发中遵循的一组五个重要原则。该原则是以下五个原则的首字母缩写词……
- 单一职责原则 (SRP)
- 开闭原则
- Liskov 替换原则 (LSP)
- 接口隔离原则 (ISP)
- 依赖倒置原则 (DIP)
在这篇文章中,我们将详细了解单一职责原则。顾名思义,它声明所有类和模块应该只有 1 个明确定义的职责。根据罗伯特·C·马丁的说法,
A class should have one, and only one reason to change.
这意味着当我们设计我们的类时,我们需要确保我们的类只负责 1 个任务或功能,并且当该任务/功能发生变化时,该类才应该发生变化。
在软件世界中,变化是唯一不变的因素。当需求发生变化并且我们的类不遵守此原则时,我们将对我们的类进行过多的更改,以使我们的类能够适应新的业务需求。这可能涉及许多副作用、重新测试和引入新错误。此外,我们的依赖类需要更改,从而重新编译类并更改测试用例。因此,需要重新测试整个应用程序,以确保新功能不会破坏现有的工作代码。
通常在长期运行的软件应用程序中,当新需求出现时,开发人员很想向现有代码添加新方法和功能,这会使类变得臃肿且难以测试和理解。查看现有类并查看新需求是否适合现有类或者是否应该为相同的类设计一个新类始终是一种很好的做法。
单一职责原则的好处
- 当一个应用程序有多个类,每个类都遵循这个原则时,那么应用程序变得更易于维护,更易于理解。
- 应用程序的代码质量更好,从而缺陷更少。
- 新成员入职很容易,而且他们可以更快地开始贡献。
- 测试和编写测试用例要简单得多
例子
在Java世界中,我们有很多框架都遵循这个原则。 JSR 380 验证 API 是遵循这一原则的一个很好的例子。它具有@NotNull、@Max、@MIn、@Size 等注释,这些注释应用于 bean 属性以确保 bean 属性满足特定标准。因此,验证 API 只有 1 个职责,即在 bean 属性上应用验证规则,并在 bean 属性与特定标准不匹配时通知错误消息
另一个例子是 Spring Data JPA,它负责所有的 CRUD 操作。它有一个职责是定义一种标准化的方式来存储,从持久存储中检索实体数据。它消除了编写样板 JDBC 代码以在数据库中存储实体的繁琐任务,从而简化了开发工作。
总的来说,Spring 框架也是实践中单一职责的一个很好的例子。 Spring 框架非常庞大,有很多模块——每个模块都满足一个特定的职责/功能。我们只根据需要在我们的依赖 pom 中添加相关模块。
让我们再看一个例子来更好地理解这个概念。考虑一个食品配送应用程序,它接受食品订单、计算账单并将其交付给客户。我们可以为每个要执行的任务使用 1 个单独的类,然后主类可以调用这些类来一个接一个地完成这些操作。
Java
import java.io.*;
import java.util.*;
class GFG {
public static void main(String[] args)
{
Customer customer1 = new Customer();
customer1.setName("John");
customer1.setAddress("Pune");
Order order1 = new Order();
order1.setItemName("Pizza");
order1.setQuantity(2);
order1.setCustomer(customer1);
order1.prepareOrder();
BillCalculation billCalculation
= new BillCalculation(order1);
billCalculation.calculateBill();
DeliveryApp deliveryApp = new DeliveryApp(order1);
deliveryApp.delivery();
}
}
class Customer {
private String name;
private String address;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address)
{
this.address = address;
}
}
class Order {
private Customer customer;
private String orderId;
private String itemName;
private int quantity;
private int totalBillAmt;
public Customer getCustomer() { return customer; }
public void setCustomer(Customer customer)
{
this.customer = customer;
}
public String getOrderId() { return orderId; }
public void setOrderId(String orderId)
{
Random random = new Random();
this.orderId = orderId + "-" + random.nextInt(500);
}
public String getItemName() { return itemName; }
public void setItemName(String itemName)
{
this.itemName = itemName;
setOrderId(itemName);
}
public int getQuantity() { return quantity; }
public void setQuantity(int quantity)
{
this.quantity = quantity;
}
public int getTotalBillAmt() { return totalBillAmt; }
public void setTotalBillAmt(int totalBillAmt)
{
this.totalBillAmt = totalBillAmt;
}
public void prepareOrder()
{
System.out.println("Preparing order for customer -"
+ this.getCustomer().getName()
+ " who has ordered "
+ this.getItemName());
}
}
class BillCalculation {
private Order order;
public BillCalculation(Order order)
{
this.order = order;
}
public void calculateBill()
{
/* In the real world, we would want a kind of lookup
functionality implemented here where we look for
the price of each item included in the order, add
them up and add taxes, delivery charges, etc on
top to reach the total price. We will simulate
this behaviour here, by generating a random number
for total price.
*/
Random rand = new Random();
int totalAmt
= rand.nextInt(200) * this.order.getQuantity();
this.order.setTotalBillAmt(totalAmt);
System.out.println("Order with order id "
+ this.order.getOrderId()
+ " has a total bill amount of "
+ this.order.getTotalBillAmt());
}
}
class DeliveryApp {
private Order order;
public DeliveryApp(Order order) { this.order = order; }
public void delivery()
{
// Here, we would want to interface with another
// system which actually assigns the task of
// delivery to different persons
// based on location, etc.
System.out.println("Delivering the order");
System.out.println(
"Order with order id as "
+ this.order.getOrderId()
+ " being delivered to "
+ this.order.getCustomer().getName());
System.out.println(
"Order is to be delivered to: "
+ this.order.getCustomer().getAddress());
}
}
Preparing order for customer -John who has ordered Pizza
Order with order id Pizza-57 has a total bill amount of 46
Delivering the order
Order with order id as Pizza-57 being delivered to John
Order is to be delivered to: Pune
我们有一个 Customer 类,它具有诸如姓名、地址之类的客户属性。订单类包含所有订单信息,如商品名称、数量。
BillCalculation 类计算总账单,在订单对象中设置账单金额。 DeliveryApp 有 1 个任务将订单交付给客户。在现实世界中,这些类会更复杂,并且可能需要将它们的功能进一步分解为多个类。
例如,账单计算逻辑可能需要实现某种查找功能,我们根据某种数据库查找订单中包含的每件商品的价格,将它们相加,加上税费、运费等,最后到达总价。根据代码开始变得多么复杂,我们可能希望将税收、数据库查询等移到其他单独的类中。类似地,交付类可能希望与另一个任务管理系统交互,该系统实际上根据位置、轮班时间、交付人员是否实际上班等将交付任务分配给不同的交付代理。这些单独的步骤可以移动在需要专门处理时将类分开。
如果账单计算和订单交付的功能被添加到同一个类中,那么每当账单计算逻辑或交付代理逻辑需要改变时,该类就会被修改;这违背了单一职责原则。根据示例,我们有一个单独的类来处理这些函数中的每一个。理想情况下,任何单个业务需求更改都应该只对一个类产生影响,从而满足单一职责原则。