软件设计原则
设计原则
衡量软件设计质量标准
衡量软件设计质量的首要标准是该设计是否能满足软件的功能需求。不过,仅仅满足功能性需求的设计并不见得是好的设计。除了功能需求以外,还有很多衡量软件设计质量的标准,例如:
- 可读性:软件的设计文档是否轻易被其他程序员理解。可读性差的设计会给大型软件的开发和维护过程带来严重的危害。
- 可复用性:软件系统的架构、类、组件等单元能否很容易被本项目的其它部分或者其它项目复用。
- 可扩展性:软件面对需求变化时,功能或性能扩展的难易程度。
- 可维护性:软件维护(主要是指软件错误的修改、遗漏功能的添加等)的难易程度。
上述衡量标准之间存在紧密的关联。例如,可读性好的软件维护起来就会容易一些,扩展性好的软件复用时往往也会非常简单。
高内聚、低耦合
如果一个软件的内聚度高和耦合度低,它也就自然具备了比较好的复用性、可扩展性和可维护性。即我们常说的“高内聚、低耦合”是所有优秀软件的共同特征。
内聚度(高)
内聚度表示一个应用程序的单个单元所负责的任务数量和多样性。内聚与单个类或者单个方法单元相关(好的软件设计应该做到高内聚)。
如果一个系统单元只负责一件事情,就说明这个系统单元有很高的内聚度;如果一个系统单元负责了很多不相关的事情,则说明这个系统单元是内聚度很低。
耦合度(低)
耦合度表示子系统、模块、类之间关系的紧密程度
耦合度决定了变更一个应用程序的容易程度。在紧密耦合的类结构中,更改一个类会导致其它的类也随之需要做出修改。•这是我们在类设计时应该避免的,因为微小的修改会迅速波动影响到整个应用程序。
设计原则
那么,在面向对象的软件设计时,如何做到高内聚、低耦合呢?这就需要我们在设计时遵循一定的设计原则。

注意:这些设计原则并不是孤立存在的,它们相互依赖,相互补充。
单一职责原则
职责单一原则((Single Responsibility Principle, SRP)指出一个类应该只有一个引起它变化的原因(即一个类应该只有一个职责)
违反这个原则的一个典型案例是将过多的职责合并到同一个类中,导致该类难以维护和扩展。
以下是一个违反职责单一原则的Java代码示例:
public class InventoryManager {
private Map<String, Integer> stock = new HashMap<>();
// 管理库存的方法
public void addStock(String item, int quantity) {
stock.put(item, stock.getOrDefault(item, 0) + quantity);
}
// 销售商品的方法
public boolean sellItem(String item, int quantity) {
if (stock.containsKey(item) && stock.get(item) >= quantity) {
stock.put(item, stock.get(item) - quantity);
return true;
}
return false;
}
// 打印库存报告的方法
public void printInventoryReport() {
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
// 计算总价的方法
public double calculateTotalPrice() {
double total = 0.0;
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
// 假设每个商品有固定的价格
double pricePerItem = 10.0;
total += entry.getValue() * pricePerItem;
}
return total;
}
}
在这个例子中,InventoryManager 类不仅负责管理库存,还负责销售商品、打印库存报告和计算总价。这违反了职责单一原则,因为这个类现在有多个引起它变化的原因。例如,如果销售逻辑变得更加复杂或者需要添加税收计算,那么就需要修改 InventoryManager 类。同样,如果报告格式或内容需要更改,也需要修改相同的类。
为了遵循职责单一原则,可以将这些职责分解到不同的类中:
// 库存管理类
public class StockManager {
private Map<String, Integer> stock = new HashMap<>();
public void addStock(String item, int quantity) {
// ...
}
public boolean sellItem(String item, int quantity) {
// ...
}
}
// 报告生成类
public class ReportGenerator {
private StockManager stockManager;
public ReportGenerator(StockManager stockManager) {
this.stockManager = stockManager;
}
public void printInventoryReport() {
// ...
}
}
// 价格计算类
public class PriceCalculator {
private StockManager stockManager;
public PriceCalculator(StockManager stockManager) {
this.stockManager = stockManager;
}
public double calculateTotalPrice() {
// ...
}
}
这样,每个类都只负责一项任务,当需要改变或增加功能时,只需修改相应的类。
开闭原则
开闭原则(Open-Close Principle,OCP )是指一个软件实体(类、模块、方法等)应该对扩展开放、对修改关闭。
开闭原则应该尽量避免修改已有的代码,而是通过扩展来实现新功能。
开闭原则是面向对象设计中“可复用设计”的基石
遵循开闭原则可以带来灵活性、可重用性和可维护性。其它设计原则(里氏替换原则、依赖倒转原则、组合/聚合复用原则、迪米特法则、接口隔离原则)是实现开闭原则的手段和工具。
里氏替换原则
里氏替换原则(The Liskov Substitution Principle,LSP)是指在一个软件系统中,子类应该能够完全替换任何父类能够出现的地方,并且经过替换后,不会让调用父类的客户程序从行为上有任何改变
里氏替换原则是使代码符合开闭原则的一个重要保证
违反里氏替换原则的案例通常涉及到子类改变了父类方法的预期行为,Java 代码示例:
// 基类 Rectangle
class Rectangle {
protected double width;
protected double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getArea() {
return width * height;
}
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
}
// 子类 Square
class Square extends Rectangle {
public Square(double side) {
super(side, side);
}
// 违反了LSP,因为正方形的宽度和高度应该总是相等
@Override
public void setWidth(double width) {
// 如果调用setWidth,则必须同时设置height,否则违反了Square的规则
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(double height) {
// 同上,违反了Square的规则
super.setWidth(height);
super.setHeight(height);
}
}
// 使用场景
public class AreaCalculator {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(4, 5);
System.out.println("Rectangle area: " + rectangle.getArea()); // 输出: 20.0
Rectangle square = new Square(3);
// 这里存在问题,因为我们期望Square的setWidth和setHeight是等价的,但实际上它们被重写了
// 如果我们用setWidth或setHeight改变了一边的大小,另一边也会改变,这违背了我们对Square的期望
square.setWidth(10); // 期望输出: 100.0,实际输出: 100.0
System.out.println("Square area: " + square.getArea());
}
}
在这个例子中,Square 类继承自 Rectangle 类,并重写了 setWidth 和 setHeight 方法。然而,对于 Square 来说,宽度和高度应该是相等的,不应该单独改变其中一个属性而不改变另一个。这样,当我们在 AreaCalculator 类中使用 Square 对象时,我们发现 setWidth 方法的调用改变了 height 的值,这违反了我们对 Square 的预期,也就是违反了里氏替换原则。正确的做法是在 Square 类中保持宽度和高度的同步,或者提供一个单独的方法来设置边长,而不是重写 setWidth 和 setHeight。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
// 类 Rectangle
class Rectangle {
protected double width;
protected double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getArea() {
return width * height;
}
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
}
// 类 Square
class Square {
protected double side;
public Square(double side) {
this.side = side;
}
public void setSide(double side) {
this.side = side;
}
public double getArea() {
return side * side;
}
}
public class AreaCalculator {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(4, 5);
System.out.println("Rectangle area: " + rectangle.getArea()); // 输出: 20.0
Square square = new Square(3);
square.setSide(10); // 期望输出: 100.0,实际输出: 100.0
System.out.println("Square area: " + square.getArea());
}
}
依赖倒置原则
依赖倒置原则(Dependency Inversion Principle, DIP)指出高层模块不应依赖于低层模块,二者都应依赖于抽象。抽象不应依赖于细节;细节应依赖于抽象。
违反这个原则通常发生在代码中直接依赖于具体类,而不是依赖于接口或抽象类。
违反依赖倒置原则的Java代码示例:
// 具体的数据库操作类
class MySQLDatabase {
public void connect() {
System.out.println("连接到MySQL数据库");
}
public void executeQuery() {
System.out.println("执行MySQL查询操作");
}
}
// 高层模块,直接依赖于具体的MySQLDatabase类
class OrderService {
private MySQLDatabase database;
public OrderService() {
this.database = new MySQLDatabase(); // 依赖于具体的实现
}
public void saveOrder() {
database.connect();
database.executeQuery();
// 保存订单的其他逻辑...
}
}
public class Client {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.saveOrder();
}
}
在这个例子中,OrderService 类直接依赖于 MySQLDatabase 类,这违反了依赖倒置原则。如果将来需要更换数据库,比如改用Oracle数据库,OrderService 类将需要修改以适应新的数据库类。
为了遵守依赖倒置原则,我们可以让 OrderService 依赖于一个抽象的数据库接口,如下所示:
// 数据库操作接口
interface IDatabase {
void connect();
void executeQuery();
}
// 具体的数据库操作实现类
class MySQLDatabase implements IDatabase {
@Override
public void connect() {
System.out.println("连接到MySQL数据库");
}
@Override
public void executeQuery() {
System.out.println("执行MySQL查询操作");
}
}
// 高层模块,依赖于IDatabase接口
class OrderService {
private IDatabase database;
public OrderService(IDatabase database) { // 通过构造器注入依赖
this.database = database;
}
public void saveOrder() {
database.connect();
database.executeQuery();
// 保存订单的其他逻辑...
}
}
public class Client {
public static void main(String[] args) {
IDatabase database = new MySQLDatabase();
OrderService orderService = new OrderService(database);
orderService.saveOrder();
// 如果需要切换数据库,只需更换具体的数据库实现即可,OrderService不需要修改
// IDatabase database = new OracleDatabase();
// OrderService orderService = new OrderService(database);
// orderService.saveOrder();
}
}
在重构后的代码中,OrderService 类依赖于 IDatabase 接口,这样就不直接依赖于任何具体的数据库实现。当需要更换数据库类型时,只需在 Client 类中更换具体的数据库实现,而不需要修改 OrderService 类,从而遵守了依赖倒置原则。
组合/聚合复用原则
组合/聚合复用原则(Composite/Aggregation Reuse Principle,CARP)是指要尽量使用组合/聚合而非继承来达到复用目的
违反组合/聚合复用原则的案例通常是过度使用继承来实现代码复用,而不是使用组合或聚合。以下是一个Java代码示例:
// 基类 Employee
class Employee {
protected String name;
protected double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public void work() {
System.out.println(name + " is working.");
}
// 其他员工共有的方法...
}
// 子类 Developer,继承自Employee
class Developer extends Employee {
public Developer(String name, double salary) {
super(name, salary);
}
// 开发人员特有的方法
public void code() {
System.out.println(name + " is coding.");
}
}
// 违反了组合/聚合复用原则,因为Designer并不是Employee的特殊类型
class Designer extends Employee {
public Designer(String name, double salary) {
super(name, salary);
}
// 设计师特有的方法
public void design() {
System.out.println(name + " is designing.");
}
}
// 项目经理类,需要同时管理Developer和Designer,但是错误地通过继承Employee来复用代码
public class ProjectManager extends Employee {
public ProjectManager(String name, double salary) {
super(name, salary);
}
// 项目经理特有的方法
public void manageProject() {
System.out.println(name + " is managing a project.");
}
}
在这个例子中,ProjectManager 错误地通过继承 Employee 类来实现复用代码,即使项目经理并不是一名员工。这种设计导致了类层次结构混乱,并且不利于后续的维护和扩展。
为了遵守组合/聚合复用原则,我们可以将共享的行为抽取到一个单独的类中,并通过组合或聚合的方式在需要的类中使用这个共享行为的实例,如下所示:
// 共享行为的接口
interface Workable {
void work();
}
// 具体的工作行为实现
class EmployeeWork implements Workable {
private String name;
public EmployeeWork(String name) {
this.name = name;
}
@Override
public void work() {
System.out.println(name + " is working.");
}
}
// 员工类,通过组合的方式使用Workable
class Employee {
private Workable workable;
private String name;
private double salary;
public Employee(Workable workable, String name, double salary) {
this.workable = workable;
this.name = name;
this.salary = salary;
}
public void work() {
workable.work();
}
// 其他员工共有的方法...
}
// 开发人员类,通过聚合的方式使用Employee
class Developer {
private Employee employee;
public Developer(Employee employee) {
this.employee = employee;
}
// 开发人员特有的方法
public void code() {
employee.work(); // 使用Employee的work方法
System.out.println("Developer is coding.");
}
}
// 设计师类,通过聚合的方式使用Employee
class Designer {
private Employee employee;
public Designer(Employee employee) {
this.employee = employee;
}
// 设计师特有的方法
public void design() {
employee.work(); // 使用Employee的work方法
System.out.println("Designer is designing.");
}
}
// 项目经理类,通过组合的方式使用多个Employee
public class ProjectManager {
private List<Employee> employees;
public ProjectManager(List<Employee> employees) {
this.employees = employees;
}
// 项目经理特有的方法
public void manageProject() {
for (Employee employee : employees) {
employee.work(); // 使用Employee的work方法
}
System.out.println("Project Manager is managing a project with employees.");
}
}
在重构后的代码中,我们创建了一个 Workable 接口来表示工作的能力,并将其实现类 EmployeeWork 通过组合的方式提供给 Employee 类使用。Developer 和 Designer 则通过聚合的方式拥有一个 Employee 实例。ProjectManager 类通过组合的方式管理多个 Employee 对象,这样的设计更符合组合/聚合复用原则。
接口隔离原则
接口隔离原则是指客户不应该依赖它们用不到的方法,只给每个客户它所需要的接口(不能强迫用户去依赖那些他们不使用的接口)
换句话说,应该提供细粒度的接口,而不是大而全的接口。
违反接口隔离原则的案例通常表现为一个接口包含了很多方法,但客户端程序只需要其中的几个方法。以下是一个Java代码示例:
// 一个大而全的服务接口
interface IService {
void operation1();
void operation2();
void operation3();
void operation4();
// 更多的操作...
}
// 客户端类,只需要使用IService接口中的部分方法
public class Client {
private IService service;
public Client(IService service) {
this.service = service;
}
public void doTaskA() {
service.operation1();
service.operation2();
}
public void doTaskB() {
service.operation3();
service.operation4();
// 可能还有其他任务...
}
}
在上面的例子中,IService 接口包含了四个方法,但客户端类 Client 只需要使用其中的两个方法。这意味着客户端类被迫依赖于一个它不需要全部方法的接口,违反了接口隔离原则。
为了遵守接口隔离原则,我们应该把 IService 接口拆分成多个小的、专门的接口,如下所示:
package com.dtinone._23_design_principle.ISP.correct;
/**
* @author binge
* @Description 遵守接口隔离原则案例
* @date 2024年03月08日 下午 4:15
*/
// 分离出的专门接口
interface IService1 {
void operation1();
void operation2();
}
interface IService2 {
void operation3();
void operation4();
}
// 实现了两个专门接口的服务类
class ServiceImpl implements IService1, IService2 {
@Override
public void operation1() {
// 实现细节...
}
@Override
public void operation2() {
// 实现细节...
}
@Override
public void operation3() {
// 实现细节...
}
@Override
public void operation4() {
// 实现细节...
}
}
// 修改后的客户端类,分别依赖于两个专门的接口
public class Client {
private IService1 service1;
private IService2 service2;
public Client(IService1 service1, IService2 service2) {
this.service1 = service1;
this.service2 = service2;
}
public void doTaskA() {
service1.operation1();
service1.operation2();
}
public void doTaskB() {
service2.operation3();
service2.operation4();
}
// 可能还有其他任务...
}
在重构后的代码中,IService1 和 IService2 是两个专门的接口,分别对应客户端类 Client 需要使用的操作。这样,客户端类只需要依赖于它真正需要的接口,而不需要依赖一个庞大的、包含许多不需要的方法的接口,从而遵守了接口隔离原则。
迪米特法则
迪米特法则(Law of Demeter,LOD)又称为“最少知识原则”,是指一个软件实体应当尽可能少的与其他实体发生相互作用
一个对象应该与其直接的朋友(即它所引用的对象)交互。违反迪米特原则的代码通常表现为对象调用了它不应该知道的其他对象的方法。
下面是一个违反迪米特原则的Java代码示例:
// 一个简单的购物车类
public class ShoppingCart {
private ProductCatalog catalog;
public ShoppingCart(ProductCatalog catalog) {
this.catalog = catalog;
}
public void checkout() {
// ... 执行结账流程
// 这里违反了迪米特原则,因为ShoppingCart直接调用了User的方法
User user = new User();
user.pay(this);
}
// 其他购物车相关的方法...
}
// 产品目录类
class ProductCatalog {
// 产品目录的方法...
}
// 用户类
class User {
public void pay(ShoppingCart cart) {
// ... 处理支付流程
}
}
在这个例子中,ShoppingCart 类在 checkout 方法中直接创建了 User 的实例,并调用了 User 的 pay 方法。这意味着 ShoppingCart 类对 User 类有了过多的了解,并且直接依赖于 User 类的实现细节。
为了遵守迪米特原则,我们可以修改 ShoppingCart 类,使其通过参数传递一个能够处理支付的对象,而不是直接与 User 类交互,如下所示:
// 修改后的购物车类
public class ShoppingCart {
private ProductCatalog catalog;
public ShoppingCart(ProductCatalog catalog) {
this.catalog = catalog;
}
public void checkout(PaymentProcessor processor) {
// ... 执行结账流程
// 现在ShoppingCart通过参数传递了一个PaymentProcessor对象
// 它不需要知道谁来处理支付,只需要知道如何请求支付
processor.process(this);
}
// 其他购物车相关的方法...
}
// 产品目录类
class ProductCatalog {
// 产品目录的方法...
}
// 支付处理器接口
interface PaymentProcessor {
void process(ShoppingCart cart);
}
// 用户类,实现了支付处理器接口
class User implements PaymentProcessor {
@Override
public void process(ShoppingCart cart) {
// ... 处理支付流程
}
}
在重构后的代码中,ShoppingCart 类不再直接依赖于 User 类,而是通过 PaymentProcessor 接口与支付逻辑解耦。现在 ShoppingCart 类不需要知道谁来处理支付,它只需要调用接口中定义的 process 方法。这样,ShoppingCart 遵循了迪米特原则,减少了类之间的耦合。
综合性案例
我们有一个[电子商务系统,其中包含商品、订单和客户等模块。我们将使用一些 Java 设计原则来改进这个系统的设计。
- 首先,我们使用单一职责原则将系统拆分为多个独立的类,每个类负责一项特定的任务。例如,我们可以创建一个
Product类来表示商品,一个Order类来表示订单,一个Customer类来表示客户等。 - 接下来,我们使用里氏替换原则来确保我们可以在不修改使用这些类的代码的情况下替换它们。例如,如果我们需要更改商品的表示方式,我们只需要修改
Product类,而不需要修改使用Product类的代码。 - 然后,我们使用接口隔离原则来确保我们的接口是高内聚的,并且它们之间没有不必要的依赖关系。例如,我们可以创建一个
IProduct接口来表示商品,一个IOrder接口来表示订单等。 - 我们使用依赖倒置原则来确保我们的高层模块不依赖于低层模块,而是依赖于抽象。例如,我们可以创建一个
CatalogService类来处理商品目录,该类依赖于IProduct接口,而不依赖于具体的Product类。 - 我们使用迪米特法则来减少类之间的直接交互。例如,如果
CatalogService类需要获取商品的价格,它不应该直接与Product类交互,而是应该通过IPriceService接口与价格服务交互。 - 我们使用开闭原则来确保我们的系统是可扩展的。例如,如果我们需要添加一种新的商品类型,我们只需要添加一个新的
Product类来实现IProduct接口,而不需要修改现有的代码。 - 我们使用组合/聚合原则来选择使用组合还是继承来实现类的功能。例如,如果我们需要在
Order类中添加商品信息,我们可以使用组合来实现,而不是继承Product类。 - 我们使用好莱坞原则来确保我们的系统是可替换的。例如,如果我们需要替换价格服务,我们只需要修改
CatalogService类的依赖关系,而不需要修改现有的代码。 - 最后,我们使用重构原则来改进系统的结构,使其更加清晰和易于理解。例如,我们可以将重复的代码提取为一个公共的方法,或者将复杂的类拆分为更小的类。
interface IProduct {
String getName();
double getPrice();
}
interface IOrder {
List<IProduct> getProducts();
void setProducts(List<IProduct> products);
}
interface IPriceService {
double getPrice(String productId);
}
class Product implements IProduct {
private String productId;
private String name;
private double price;
public Product(String productId, String name, double price) {
this.productId = productId;
this.name = name;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public double getPrice() {
return price;
}
}
class Order implements IOrder {
private List<IProduct> products;
private String orderId;
public Order(String orderId) {
this.orderId = orderId;
}
@Override
public List<IProduct> getProducts() {
return products;
}
@Override
public void setProducts(List<IProduct> products) {
this.products = products;
}
}
class CatalogService {
private IPriceService priceService;
public CatalogService(IPriceService priceService) {
this.priceService = priceService;
}
public List<Order> getOrders() {
// 获取所有订单
List<Order> orders = Arrays.asList(
new Order("ORDER1"),
new Order("ORDER2")
);
// 为每个订单设置商品
orders.forEach(order -> {
List<IProduct> products = Arrays.asList(
new Product("PRODUCT1", "Product 1", 100.0),
new Product("PRODUCT2", "Product 2", 200.0)
);
// 设置订单的商品列表
order.setProducts(products);
});
return orders;
}
public void calculatePrices() {
// 获取所有订单
List<Order> orders = getOrders();
// 遍历每个订单
orders.forEach(order -> {
// 获取订单的商品列表
List<IProduct> products = order.getProducts();
// 遍历每个商品
products.forEach(product -> {
// 获取商品的价格
double price = priceService.getPrice(product.getProductId());
// 设置商品的价格
product.setPrice(price);
});
});
}
}
class PriceService implements IPriceService {
private Map<String, Double> prices;
public PriceService() {
// 初始化价格服务
prices = new ConcurrentHashMap<>();
prices.put("PRODUCT1", 100.0);
prices.put("PRODUCT2", 200.0);
}
@Override
public double getPrice(String productId) {
// 获取商品的价格
return prices.get(productId);
}
}
public class Main {
public static void main(String[] args) {
// 创建价格服务
IPriceService priceService = new PriceService();
// 创建目录服务
CatalogService catalogService = new CatalogService(priceService);
// 获取订单列表
List<Order> orders = catalogService.getOrders();
// 打印每个订单的名称和价格
orders.forEach(order -> {
System.out.println("Order " + order.getOrderId() + ":");
// 获取订单的商品列表
List<IProduct> products = order.getProducts();
// 遍历每个商品
products.forEach(product -> {
System.out.println(" - " + product.getName() + ": " + product.getPrice());
});
});
// 计算商品价格
catalogService.calculatePrices();
// 获取订单列表
List<Order> orders = catalogService.getOrders();
// 打印每个订单的名称和价格
orders.forEach(order -> {
System.out.println("Order " + order.getOrderId() + ":");
// 获取订单的商品列表
List<IProduct> products = order.getProducts();
// 遍历每个商品
products.forEach(product -> {
System.out.println(" - " + product.getName() + ": " + product.getPrice());
});
});
}
}
在这个例子中,我们使用了一些 Java 设计原则,例如单一职责原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特法则、开闭原则、组合/聚合原则和迪米特法则。我们创建了一些接口和类来表示电子商务系统中的不同模块,例如商品、订单、客户和价格服务等。每个类负责一项特定的任务,并且它们之间的依赖关系是基于接口的,这使得系统更加灵活和可扩展。此外,我们使用了迪米特法则来减少类之间的直接交互,这使得系统更加易于测试和维护。最后,我们使用了重构原则来改进系统的结构,使其更加清晰和易于理解。

浙公网安备 33010602011771号