设计模式也是进行开发,面试环节中的常考内容。很多资深程序员也很难保证实现业务的过程中能够对这些原则都能实现足够的支持。
我们就在这里简明扼要地介绍一下这六大原则,可以设计模式的具体实现提供一些理论支撑,能够更好的理解那些不同的设计模式都是怎样的实现这些原则的。
单一职责原则
单一职责原则(Single Responsibility Principle,SRP):一个类应该只负责单一的任务或职责,避免一个类承担过多的职责,这样可以提高代码的可读性、可维护性和可测试性。
违反单一职责原则的代码:
public class User {
private String name;
private String email;
public void sendEmail(String content) {
// 发送邮件的代码
}
public void saveToDatabase() {
// 保存到数据库的代码
}
}
在这个例子中,User类既有保存到数据库的职责,又有发送邮件的职责,违反了单一职责原则。
遵循单一职责原则的代码:
public class User {
private String name;
private String email;
// 省略构造方法、getter和setter
public static void saveToDatabase(User user) {
// 保存到数据库的代码
}
}
public class EmailSender {
public static void sendEmail(User user, String content) {
// 发送邮件的代码
}
}
在这个例子中,User类只有保存到数据库的职责,EmailSender类只有发送邮件的职责,遵循了单一职责原则。
如果违反单一职责原则,通常会出现以下问题:
- 类的职责不清晰,难以维护和扩展。
- 一个类中的不相关代码容易相互影响,导致代码出现错误。
- 单一职责原则是面向对象编程的基础,违反它会导致代码质量下降,增加维护成本和开发成本。
开闭原则
开放封闭原则(Open-Closed Principle,OCP):一个类应该对扩展开放,对修改关闭。这意味着在增加新功能时,应该通过添加新的代码或类来扩展现有代码,而不是修改现有代码。为了使程序扩展性好,易于维护和升级:需要使用接口和抽象类
符合开放封闭原则的代码段:
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public class Drawing {
private List<Shape> shapes = new ArrayList<>();
public void addShape(Shape shape) {
shapes.add(shape);
}
public void drawShapes() {
for (Shape shape : shapes) {
shape.draw();
}
}
}
在这个代码段中,我们定义了一个Shape接口,所有形状都必须实现该接口。Circle和Square类实现了Shape接口,并且Drawing类可以将它们添加到一个List中。这个Drawing类不需要修改,如果我们想要添加其他形状,只需要实现Shape接口并添加到Drawing类中即可。这就是开放封闭原则的一个例子。
违反开放封闭原则的代码段:
public class ShoppingCart {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
public void checkout() {
double total = 0;
for (Item item : items) {
total += item.getPrice();
}
System.out.println("Total price: " + total);
}
}
public class Item {
private String name;
private double price;
public Item(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
public class DiscountItem extends Item {
private double discount;
public DiscountItem(String name, double price, double discount) {
super(name, price);
this.discount = discount;
}
@Override
public double getPrice() {
return super.getPrice() * (1 - discount);
}
}
在这个代码段中,我们有一个ShoppingCart类,它可以添加项目并在结账时计算总价格。我们还有一个Item类,它有一个名称和一个价格,DiscountItem类继承Item类并添加了一个折扣属性。但是,我们在DiscountItem类中覆盖了Item类的getPrice方法,这违反了开放封闭原则。如果我们想要添加其他类型的项目,例如按重量计算价格的项目,我们需要修改ShoppingCart类。这样做可能会导致ShoppingCart类变得非常复杂,难以维护。
里氏替换原则
里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换父类并保持程序的正确性。简单来说,子类应该符合父类的行为规范,并且不能改变父类的行为。
这一点常用Java语言的同学肯定不陌生,Queue有多种实现,List有多重实现等等
符合里氏替换原则的代码:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
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);
}
}
这个例子展示了一个矩形和正方形之间的关系。矩形有宽和高两个属性,以及计算面积的方法。正方形继承自矩形,并重写了设置长宽的方法,使得它们总是相等的。这个例子符合里氏替换原则,因为正方形可以替换矩形在任何需要使用矩形的地方,而不会影响程序的正确性。
违背里氏替换原则的代码:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
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) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
这个例子也展示了一个矩形和正方形之间的关系。正方形继承自矩形,并重写了设置长宽的方法,使得它们总是相等的。但是这个例子违反了里氏替换原则,因为正方形不能替换矩形在所有需要使用矩形的地方,例如需要设置长和宽不相等的情况。如果程序中有这样的代码,那么这个代码段将会导致错误。
违反里氏替换原则通常会导致程序的不稳定性和不可维护性。如果一个子类违反了它的父类的行为约定,那么在使用该子类的地方就会出现意外的行为。这将导致程序的行为不可预测,并且在更改和维护代码时会变得更加困难。
接口隔离原则
接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。这意味着应该将接口分离成更小的接口,以确保客户端只需知道与其相关的接口。
符合接口隔离原则的代码:
public interface Animal {
void eat();
void sleep();
}
public class Dog implements Animal {
public void eat() {
// 狗的吃法
}
public void sleep() {
// 狗的睡法
}
}
public class Cat implements Animal {
public void eat() {
// 猫的吃法
}
public void sleep() {
// 猫的睡法
}
}
违背接口隔离原则的代码:
public interface Animal {
void eat();
void sleep();
void fly();
}
public class Dog implements Animal {
public void eat() {
// 狗的吃法
}
public void sleep() {
// 狗的睡法
}
public void fly() {
// 狗不能飞,空实现
}
}
public class Cat implements Animal {
public void eat() {
// 猫的吃法
}
public void sleep() {
// 猫的睡法
}
public void fly() {
// 猫不能飞,空实现
}
}
如果违背接口隔离原则,通常会有以下问题:
- 接口过于臃肿,实现类需要实现不必要的方法,增加了代码复杂度。
- 如果某个实现类只需要部分方法,却要实现整个接口,会影响代码的可维护性和可读性。
- 接口的修改会影响所有实现类,如果某个实现类不需要被修改的方法,也要重新编译和部署,增加了开发成本和时间成本。
依赖倒置原则
依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖低层模块,它们都应该依赖于抽象。同时,抽象不应该依赖于具体实现,具体实现应该依赖于抽象。简单来说,就是要面向接口编程,而不是面向具体实现。
以下是一个违反DIP原则的Java代码段:
public class OrderService {
private MysqlDao mysqlDao;
public OrderService() {
mysqlDao = new MysqlDao();
}
public void saveOrder(Order order) {
mysqlDao.save(order);
}
}
上述代码中,OrderService依赖于具体的MysqlDao类,而不是依赖于抽象的接口或者抽象的Dao类。如果我们需要在将来使用其他的数据库,就需要修改OrderService类的代码,这违背了开闭原则(OCP)。
以下是一个符合DIP原则的Java代码:
public interface Dao {
void save(Order order);
}
public class MysqlDao implements Dao {
public void save(Order order) {
// ...
}
}
public class OrderService {
private Dao dao;
public OrderService(Dao dao) {
this.dao = dao;
}
public void saveOrder(Order order) {
dao.save(order);
}
}
在这个代码段中,OrderService依赖于Dao接口而不是具体的实现类,并且Dao接口的具体实现是通过构造函数注入的。这样的设计可以让OrderService更加灵活,可以轻松切换不同的Dao实现类,而不需要修改OrderService的代码。
如果违反DIP原则,通常会出现以下问题:
-
高层模块的修改会影响低层模块的实现,这会增加系统的耦合度。
-
高层模块无法独立进行单元测试,需要依赖于底层模块的实现。
-
一旦底层模块发生变化,高层模块也需要进行修改,这会增加系统的维护成本。
迪米特原则
迪米特原则(Law of Demeter,LoD):一个对象应该对其他对象有最少的了解。这意味着对象之间应该尽可能地解耦,一个对象应该只与它直接交互的对象进行通信,而不应该了解其他对象的详细信息。
符合迪米特原则的代码:
public class School {
private List<Teacher> teachers;
private List<Student> students;
public School(List<Teacher> teachers, List<Student> students) {
this.teachers = teachers;
this.students = students;
}
public List<Teacher> getTeachers() {
return teachers;
}
public List<Student> getStudents() {
return students;
}
}
public class Teacher {
private String name;
public Teacher(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Teacher> teachers = new ArrayList<>();
teachers.add(new Teacher("Tom"));
teachers.add(new Teacher("Jerry"));
List<Student> students = new ArrayList<>();
students.add(new Student("Alice"));
students.add(new Student("Bob"));
School school = new School(teachers, students);
for (Teacher teacher : school.getTeachers()) {
System.out.println(teacher.getName());
}
for (Student student : school.getStudents()) {
System.out.println(student.getName());
}
}
}
上面的代码展示了迪米特原则的实践,School 类只与 Teacher 和 Student 类交互,不与其他类直接交互。这符合迪米特原则,也称为最少知识原则。
不符合迪米特原则的代码:
public class School {
private List<Teacher> teachers;
private List<Student> students;
public School(List<Teacher> teachers, List<Student> students) {
this.teachers = teachers;
this.students = students;
}
public List<Teacher> getTeachers() {
return teachers;
}
public List<Student> getStudents() {
return students;
}
public void sendNotification(String message) {
for (Teacher teacher : teachers) {
teacher.receiveNotification(message);
}
for (Student student : students) {
student.receiveNotification(message);
}
}
}
public class Teacher {
private String name;
public Teacher(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void receiveNotification(String message) {
System.out.println("Teacher " + name + " received notification: " + message);
}
}
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void receiveNotification(String message) {
System.out.println("Student " + name + " received notification: " + message);
}
}
public class Main {
public static void main(String[] args) {
List<Teacher> teachers = new ArrayList<>();
teachers.add(new Teacher("Tom"));
teachers.add(new Teacher("Jerry"));
List<Student> students = new ArrayList<>();
students.add(new Student("Alice"));
students.add(new Student("Bob"));
School school = new School(teachers, students);
school.sendNotification("School is closed tomorrow.");
}
}
上面的代码违反了迪米特原则,School 类直接调用 Teacher 和 Student 类的方法,导致 School 类和 Teacher、Student 类耦合度过高。如果违反迪米特原则,通常会有以下问题:
- 类间的耦合度过高,一旦其中一个类发生变化,可能会影响到其他类的功能,导致代码不易维护;
- 代码的可扩展性差,如果需要新增一个类,就需要修改多个类的代码,增加了修改的复杂度;
- 可重用性差,因为类间的耦合度过高,所以很难将其中的一个类重用到其他的系统中。
参考资料:
openai-chatgpt