原则分类和说明
在软件设计中,设计模式是一种被广泛应用的概念,它可以帮助开发者解决常见的设计问题,提高代码的可维护性和可扩展性。为了保证设计模式的有效性和可用性,需要遵守以下八大准则:
1. 单一职责原则(Single Responsibility Principle,SRP):一个类或者方法只应该有一个单一的职责,不要将多个职责耦合在一起。
2. 开闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即当需要改变一个软件系统的功能或者添加新的功能时,应该尽量避免修改已有的代码,而是通过扩展已有的代码来实现。
3. 里氏替换原则(Liskov Substitution Principle,LSP):子类必须能够替换掉它们的父类,而不会影响程序的正确性。即在使用继承时,子类应该保持父类的行为,而不应该改变它们的行为。
4. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们应该依赖于抽象接口。同时,抽象接口也不应该依赖于具体实现,具体实现应该依赖于抽象接口。
5. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。即一个类不应该强制实现它不需要的方法,而应该将这些方法拆分成多个接口,让实现类只依赖于它们需要的接口。
6. 组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):优先使用组合/聚合复用,而不是继承复用。组合/聚合复用可以更加灵活地定义对象之间的关系,而继承复用会导致类之间的耦合度过高,难以修改和维护。
7. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。即一个对象应该只和它的直接朋友进行交互,而不应该和它的朋友的朋友进行交互。
8. 最少知识原则(Least Knowledge Principle,LKP):一个软件实体(类、方法等)应该尽可能少地了解其他实体。即一个对象应该只和它的直接朋友进行交互,而不应该了解它的朋友的内部细节。
遵循这八大准则可以帮助开发者设计出更加健壮、灵活、可维护、可扩展的软件系统。
不遵守的缺点
不遵守上述八大准则可能会带来以下问题:
-
单一职责原则:如果一个类或者方法承担了多个职责,会导致代码的复杂性增加,可读性和可维护性降低,同时修改一个职责可能会影响其他职责的实现。
-
开闭原则:如果一个软件实体没有遵循开闭原则,每次需要添加新的功能或者改变一个功能时,都需要修改已有的代码,这会导致代码的不稳定性和可维护性降低。
-
里氏替换原则:如果一个子类不能完全替换掉它的父类,会导致代码的不稳定性和可维护性降低,同时可能会引入错误和不一致性。
-
依赖倒置原则:如果高层模块依赖于低层模块,会导致代码的耦合度过高,不利于代码的复用和维护。
-
接口隔离原则:如果一个类实现了不需要的接口,会导致类的复杂性增加,不利于代码的维护和扩展。
-
组合/聚合复用原则:如果使用继承复用而不是组合/聚合复用,会导致类之间的耦合度过高,不利于代码的维护和扩展。
-
迪米特法则:如果一个对象和它的朋友的朋友进行交互,会导致代码的复杂性增加,不利于代码的维护和扩展。
-
最少知识原则:如果一个对象了解其他实体的内部细节,会导致代码的耦合度过高,不利于代码的维护和扩展。
以上问题会导致代码的可读性、可维护性和可扩展性降低,增加代码的复杂性和不稳定性,增加代码出错的可能性。因此,在软件开发中遵循这八大准则非常重要,可以帮助开发者设计出更加健壮、灵活、可维护、可扩展的软件系统。
适用场景
- 单一职责原则(Single Responsibility Principle,SRP):一个类或者方法只应该有一个单一的职责,不要将多个职责耦合在一起。
适用场景:当一个类或者方法承担的职责过多时,可以使用单一职责原则来将其拆分成多个职责更加清晰的类或者方法。
示例代码:
public class UserService {
public void login(String username, String password) {
// 登录逻辑
}
public void register(String username, String password) {
// 注册逻辑
}
public void updateUserInfo(String username, String password, UserInfo userInfo) {
// 更新用户信息逻辑
}
public void deleteUser(String username, String password) {
// 删除用户逻辑
}
}
在上述示例代码中,UserService 类承担了登录、注册、更新用户信息和删除用户等多个职责,这会导致代码的复杂性增加,可读性和可维护性降低。可以使用单一职责原则将其拆分成多个职责更加清晰的类,比如 LoginService、RegisterService、UserInfoService 和 UserService 等。
- 开闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。即当需要改变一个软件系统的功能或者添加新的功能时,应该尽量避免修改已有的代码,而是通过扩展已有的代码来实现。
适用场景:当需要添加新的功能或者改变一个软件系统的功能时,可以使用开闭原则来尽量避免修改已有的代码,而是通过扩展已有的代码来实现。
示例代码:
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Draw a circle.");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Draw a rectangle.");
}
}
public class ShapeFactory {
public static Shape getShape(String type) {
if ("circle".equalsIgnoreCase(type)) {
return new Circle();
} else if ("rectangle".equalsIgnoreCase(type)) {
return new Rectangle();
} else {
return null;
}
}
}
在上述示例代码中,Shape 接口定义了一个 draw() 方法,Circle 和 Rectangle 类分别实现了 Shape 接口,并重写了 draw() 方法。ShapeFactory 类根据传入的参数类型来创建相应的对象。如果需要添加新的图形类型,只需要增加一个新的类并实现 Shape 接口即可,而不需要修改已有的代码,这符合开闭原则的要求。
- 里氏替换原则(Liskov Substitution Principle,LSP):子类必须能够替换掉它们的父类,而不会影响程序的正确性。即在使用继承时,子类应该保持父类的行为,而不应该改变它们的行为。
适用场景:当使用继承时,子类需要保持父类的行为,而不应该改变它们的行为。
示例代码:
public class Rectangle {
private int width;
private int 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;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
在上述示例代码中,Square 类继承自 Rectangle 类,但是修改了 setWidth() 和 setHeight() 方法的实现,导致 Square 类的行为和 Rectangle 类的行为不一致。在这种情况下,如果将 Square 对象传递给需要 Rectangle 对象的方法,可能会导致程序出错,违反了里氏替换原则。
- 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们应该依赖于抽象接口。同时,抽象接口也不应该依赖于具体实现,具体实现应该依赖于抽象接口。
适用场景:当高层模块需要依赖低层模块时,可以使用依赖倒置原则来避免高层模块直接依赖低层模块的具体实现,而是依赖于低层模块的抽象接口。
示例代码:
public interface ILogger {
void log(String message);
}
public class ConsoleLogger implements ILogger {
@Override
public void log(String message) {
System.out.println("[ConsoleLogger] " + message);
}
}
public class FileLogger implements ILogger {
@Override
public void log(String message) {
System.out.println("[FileLogger] " + message);
}
}
public class LoggerManager {
private ILogger logger;
public LoggerManager(ILogger logger) {
this.logger = logger;
}
public void log(String message) {
logger.log(message);
}
}
在上述示例代码中,ILogger 接口定义了一个 log() 方法,ConsoleLogger 和 FileLogger 类分别实现了 ILogger 接口,并重写了 log() 方法。LoggerManager 类依赖于 ILogger 接口而不是具体的实现类,这符合依赖倒置原则的要求。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。即一个类不应该强制实现它不需要的方法,而应该将这些方法拆分成多个接口,让实现类只依赖于它们需要的接口。
适用场景:当一个类需要实现的接口过多时,可以使用接口隔离原则将这些方法拆分成多个接口,让实现类只依赖于它们需要的接口。
示例代码:
public interface IWorker {
void work();
}
public interface IEater {
void eat();
}
public class Person implements IWorker, IEater {
@Override
public void work() {
System.out.println("Person is working...");
}
@Override
public void eat() {
System.out.println("Person is eating...");
}
}
public class Robot implements IWorker {
@Override
public void work() {
System.out.println("Robot is working...");
}
}
在上述示例代码中,IWorker 接口定义了一个 work() 方法,IEater 接口定义了一个 eat() 方法。Person 类实现了 IWorker 和 IEater 接口,而 Robot 类只实现了 IWorker 接口。如果没有使用接口隔离原则,Person 类和 Robot 类都需要实现 eat() 方法,这样就违反了接口隔离原则的要求。
- 组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):优先使用组合/聚合关系来复用代码,而不是使用继承关系来复用代码。即应该优先使用对象组合或者聚合的方式来复用已有的代码,而不是通过继承的方式来复用已有的代码。
适用场景:当需要复用已有的代码时,可以使用组合/聚合复用原则来优先使用对象组合或者聚合的方式来复用已有的代码,而不是通过继承的方式来复用已有的代码。
示例代码:
public class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car is started.");
}
}
public interface Engine {
void start();
}
public class GasolineEngine implements Engine {
@Override
public void start() {
System.out.println("Gasoline engine is started.");
}
}
public class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Electric engine is started.");
}
}
在上述示例代码中,Car 类依赖于 Engine 接口而不是具体的实现类,GasolineEngine 和 ElectricEngine 类分别实现了 Engine 接口。Car 类通过组合 Engine 对象来实现启动引擎的功能,而不是通过继承的方式来复用已有的代码,这符合组合/聚合复用原则的要求。
- 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有最少的了解,即一个对象不应该直接和其他对象交互,而应该通过中介对象来进行交互。
适用场景:当一个对象需要和其他对象进行交互时,可以使用迪米特法则来减少对象之间的耦合,提高代码的灵活性和可维护性。
示例代码:
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
public class UserService {
public User getUser(String username) {
// 从数据库中获取用户信息
return new User(username, "password");
}
}
public class UserController {
private UserService userService;
public UserController() {
this.userService = new UserService();
}
public User getUser(String username) {
return userService.getUser(username);
}
}
在上述示例代码中,UserController 类依赖于 UserService 类来获取用户信息,而不是直接和 User 类交互。这样可以避免 UserController 类了解 User 对象的内部细节,减少对象之间的耦合,提高代码的灵活性和可维护性,符合迪米特法则的要求。
- 最少知识原则(Least Knowledge Principle,LKP):一个对象应该只和它直接的朋友交流,而不应该和它的朋友的朋友交流。即一个对象应该尽可能少地了解其他对象,只和它的直接朋友进行交互。
适用场景:当一个对象需要和其他对象进行交互时,可以使用最少知识原则来尽可能减少对象之间的耦合,提高代码的灵活性和可维护性。
示例代码:
public class Teacher {
private String name;
public Teacher(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void teach(Student student) {
System.out.println(name + " is teaching " + student.getName());
}
}
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Classroom {
private List<Student> students;
private Teacher teacher;
public Classroom(List<Student> students, Teacher teacher) {
this.students = students;
this.teacher = teacher;
}
public void startClass() {
for (Student student : students) {
teacher.teach(student);
}
}
}
在上述示例代码中,Classroom 类依赖于 Teacher 类和 Student 类,但是只和它们直接的朋友进行交流,符合最少知识原则的要求。这样可以尽可能减少对象之间的耦合,提高代码的灵活性和可维护性。
文章评论