在软件开发的领域中,随着技术的不断进步和市场需求的不断变化,软件系统的设计和维护变得越来越重要。为了确保软件系统能够长期有效地运行,并且能够在未来的发展中适应新的需求和技术变化,提高软件系统的可维护性和可复用性成为了开发过程中的关键目标。此外,增加软件的可扩展性和灵活性也至关重要,因为它们可以确保软件系统能够轻松地添加新功能或者修改现有功能,而不会引发大量的结构重组或代码重写。
为了实现这些目标,程序员在开发程序时应该遵循一系列的设计原则。其中,有7条原则被广泛认为是提高软件开发效率、节约软件开发成本和维护成本的关键。
开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不需要去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。以下代码中ShoppingCart 类是对支付方式这一变化点开放的(可以轻易地添加新的 PaymentStrategy 实现),但对修改是关闭的(不需要为了支持新的支付方式而去修改 ShoppingCart 类的源代码)。
定义一个支付方式策略:
interface PaymentStrategy {
void pay(double amount);
}
// Step 2: 创建具体实现类,实现支付策略的扩展
class CashPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using cash.");
}
}
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using credit card.");
}
}
定义一个使用支付策略的上下文类,它对支付方式进行抽象:
class ShoppingCart {
private List<Item> items;
private PaymentStrategy paymentStrategy;
public ShoppingCart(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout() {
double total = calculateTotal();
paymentStrategy.pay(total); // 这里使用了多态,无需修改ShoppingCart就能添加新的支付方式
}
private double calculateTotal() {
// 计算购物车总价的逻辑...
return 100.0; // 示例金额
}
}
里氏替换原则
任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,相对于细节的多变性,抽象的东西要稳定的多。
// 抽象(接口)
interface MessageSender {
void sendMessage(String message);
}
// 具体实现(细节)
class EmailSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending email with message: " + message);
// 实现发送邮件的逻辑
}
}
class SMSSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS with message: " + message);
// 实现发送短信的逻辑
}
}
// 高层模块(不依赖具体实现)
class Staff {
private MessageSender sender;
// 构造函数通过依赖注入设置消息发送器
public Staff(MessageSender sender) {
this.sender = sender;
}
public void receiveMessageAndSendNotification(String receivedMessage) {
String notification = "Received message: " + receivedMessage;
sender.sendMessage(notification);
}
}
// 客户端代码
public class Main {
public static void main(String[] args) {
// 创建高层模块实例时,可以选择不同的具体实现
Staff staff = new Staff(new EmailSender()); // 或者 new SMSSender()
staff.receiveMessageAndSendNotification("An important notice!");
// 更改通知方式只需要更换MessageSender的实现即可
staff = new Staff(new SMSSender());
staff.receiveMessageAndSendNotification("An urgent alert!");
}
}
Staff 类是高层模块,它依赖于 MessageSender 接口,而不是具体的发送器实现,这使得 Staff 类可以根据需要灵活切换不同的消息发送方式,而不必更改其自身的代码。这就是依赖倒转原则的体现。
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)指出客户端不应该被迫依赖它们不使用的方法。换句话说,每个接口都应该有明确的责任,接口中的方法应该是高内聚的,避免“胖”接口。
// 未遵守ISP的“胖”接口
interface Employee {
void calculateSalary();
void printDetails();
void sendEmailReminder();
void updateWorkLog();
}
// 遵守ISP,将接口拆分为多个具有单一责任的小接口
interface SalaryCalculator {
void calculateSalary();
}
interface EmployeeInfoPrinter {
void printDetails();
}
interface EmailService {
void sendEmailReminder();
}
interface WorkLogUpdater {
void updateWorkLog();
}
// 实体类分别实现所需的接口
class Developer implements SalaryCalculator, EmployeeInfoPrinter {
// 实现calculateSalary和printDetails方法
// ...
}
class Manager implements SalaryCalculator, EmailService {
// 实现calculateSalary和sendEmailReminder方法
}
迪米特法则
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
不遵守迪米特法则:
class Order {
private Customer customer;
public Order(Customer customer) {
this.customer = customer;
}
// 直接访问Customer的地址信息,违反迪米特法则
public String getShippingAddressZipCode() {
return customer.getAddress().getZipCode();
}
}
class Customer {
private Address address;
public Customer(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}
class Address {
private String zipCode;
public Address(String zipCode) {
this.zipCode = zipCode;
}
public String getZipCode() {
return zipCode;
}
}
Order 类原本直接访问 Customer 的 Address 对象来获取邮编,这违反了迪米特法则。可以通过在 Customer 类中提供一个方法,Order 类只需与它的直接朋友 Customer 交流,从而降低了耦合度,遵循了迪米特法则。
合成复用原则
尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
原则强调了在设计中应尽量使用关联(Association)、组合(Composition)或聚合(Aggregation)关系来组合现有对象形成更复杂的结构或行为,而不是通过创建类层次结构进行功能扩展。
示例:添加日志记录功能
在一个系统中,若要为UserManager类添加日志记录功能,不是通过让UserManager直接继承具有日志功能的类,而是让它包含(组合或聚合)一个Logger对象,通过委托给这个对象来执行日志记录的操作:
public class UserManager {
private Logger logger;
public UserManager(Logger logger) {
this.logger = logger;
}
// 其他业务逻辑方法...
public void addUser(User user) {
// 执行添加用户逻辑
// ...
// 委托给logger记录操作日志
logger.log("User added: " + user.getUsername());
}
}
过这种方式,可以轻易更换不同的Logger实现,而无需更改UserManager的结构,同时也避免了因过度使用继承带来的设计复杂性和维护困难。
单一职责原则
这一原则的核心在于控制类的粒度大小、将对象解耦、提高其内聚性。
- 违反单一职责原则的例子
假设有一个Employee类,它既处理员工的工资计算也负责员工的个人信息管理,Employee类同时承担了员工信息管理和薪资计算两种职责。如果因为绩效计算规则改变需要修改calculateBonus方法,理论上不应影响到员工地址管理这部分功能:
public class Employee {
private String name;
private String address;
private double salary;
public Employee(String name, String address, double salary) {
this.name = name;
this.address = address;
this.salary = salary;
}
// 负责员工个人信息管理
public void updateAddress(String newAddress) {
this.address = newAddress;
}
// 负责工资计算
public double calculateBonus(double performanceRating) {
return salary * (performanceRating / 10);
}
}
- 遵循单一职责原则的例子
我们将上述职责分开到两个类中,EmployeeInfo类专门负责个人信息管理,而EmployeeSalaryCalculator类则专注于薪资计算。这样,当任何一个职责的需求发生改变时,都不会直接影响到另一个职责的实现,从而提高了代码的可读性和可维护性:
public class EmployeeInfo {
private String name;
private String address;
public EmployeeInfo(String name, String address) {
this.name = name;
this.address = address;
}
public void updateAddress(String newAddress) {
this.address = newAddress;
}
}
public class EmployeeSalaryCalculator {
private double salary;
public EmployeeSalaryCalculator(double salary) {
this.salary = salary;
}
public double calculateBonus(double performanceRating) {
return salary * (performanceRating / 10);
}
}
// 员工实体可以持有这两个类的引用
public class Employee {
private EmployeeInfo info;
private EmployeeSalaryCalculator salaryCalculator;
public Employee(String name, String address, double salary) {
this.info = new EmployeeInfo(name, address);
this.salaryCalculator = new EmployeeSalaryCalculator(salary);
}
}