面向对象设计原则
面向对象设计(Object-Oriented Design, OOD)的原则是软件工程中的一套指导思想,根据面向对象中的原则来设计代码,有助于构建结构良好、易于维护和拓展的代码。主要的设计原则有以下:
单一职责原则
单一职责原则(Simple Responsibility Pinciple,SRP)是最简单的面向对象设计原则,它用于控制类的粒度大小。
一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。即每个类应该专注于完成一项工作。
举例一个违反单一职责原则地例子,假设有一个员工管理系统,里面有一个EmployeeManager
类,它负责员工的基本信息管理以及员工薪资计算:
public class EmployeeManager {
public void addEmployee(Employee employee) {
// 添加员工到数据库
}
public void removeEmployee(Employee employee) {
// 从数据库删除员工
}
public void updateEmployee(Employee employee) {
// 更新员工信息
}
public double calculateSalary(Employee employee) {
// 根据员工的工作时间和工资率计算薪资
return employee.getHoursWorked() * employee.getHourlyRate();
}
}
在这个例子中,EmployeeManager
类同时承担了员工信息管理和薪资计算两个职责,这违反了单一职责原则。如果公司政策变化,比如调整了薪资计算方式,那么EmployeeManager
类就需要修改calculateSalary
方法,而如果数据库结构发生变化,则需要修改与数据库交互的方法。这种情况下,一个类的变化可能会引起连锁反应,导致其他部分也需要修改
为了遵循单一职责原则,应该将员工管理和计算薪资地功能剔开分解为两个独立的类。
public class EmployeeInfoManager {
public void addEmployee(Employee employee) {
// 添加员工到数据库
}
public void removeEmployee(Employee employee) {
// 从数据库删除员工
}
public void updateEmployee(Employee employee) {
// 更新员工信息
}
}
public class SalaryCalculator {
public double calculateSalary(Employee employee) {
// 根据员工的工作时间和工资率计算薪资
return employee.getHoursWorked() * employee.getHourlyRate();
}
}
现在,EmployeeInfoManager
专门负责员工信息的管理,而SalaryCalculator
专门负责薪资的计算。这样,如果薪资计算逻辑需要修改,只需要改动SalaryCalculator
类,而不会影响到EmployeeInfoManager
类。同样,如果员工信息管理的规则发生变化,也只需要修改EmployeeInfoManager
类,而不会影响到薪资计算的部分。
开放封闭原则
开放封闭原则(Open/Closed Principle, OCP)是一种常用的设计原则,具体内容如下
软件实体(如类、模块、函数等)应该是可扩展的,但不可修改的。当需求发生变化时,应该通过添加新的代码来实现,而不是修改已有的代码。
这意味着,当需求发生变化时,我们应该能够通过添加新的代码来扩展系统功能,而不是修改现有代码。这样可以保持现有代码的稳定性和可维护性。
假设我们正在开发一个图形处理软件,目前支持处理两种类型的图形:圆形和正方形。我们定义了一个Shape
接口和两个实现类Circle
与Square
:
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
我们还有一个ShapeProcessor
类,用于计算图形的面积:
public class ShapeProcessor {
public double processArea(Shape shape) {
return shape.area();
}
}
现在,客户要求我们在软件中加入对三角形的支持。如果我们直接修改ShapeProcessor
类,让它能够处理Triangle
类,那么我们将违反开放封闭原则,因为我们修改了现有的代码。
正确的做法是,我们可以通过添加一个新的Triangle
类来扩展系统,而不需要修改ShapeProcessor
类:
class Triangle implements Shape {
private double base;
private double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
Triangle
实现了Shape
接口,所以它可以直接被ShapeProcessor
类所使用,而不需要对ShapeProcessor
类进行任何修改。
里氏替换原则
里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义。
子类必须能够替换其基类并保持程序的正确性。换句话说,任何可以使用父类的地方,都应该能够使用子类而不改变程序的期望行为。
简单的说就是,子类可以扩展父类的功能,但不能改变父类原有的功能:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。
比如一个程序员工作的例子:
public abstract class Coder {
public void coding() {
System.out.println("我会打代码");
}
class JavaCoder extends Coder{
/**
* 子类除了会打代码之外,还会打游戏
*/
public void game(){
System.out.println("艾欧尼亚最强王者已上号");
}
}
}
可以看到JavaCoder
虽然继承自Coder
,但是并没有对父类方法进行重写,并且还在父类的基础上进行额外扩展,符合里氏替换原则。但是我们再来看下面的这个例子:
public abstract class Coder {
public void coding() {
System.out.println("我会打代码");
}
class JavaCoder extends Coder{
public void game(){
System.out.println("艾欧尼亚最强王者已上号");
}
/**
* 这里我们对父类的行为进行了重写,现在它不再具备父类原本的能力了
*/
public void coding() {
System.out.println("我寒窗苦读十六年,到最后还不如培训班三个月出来的程序员");
System.out.println("想来想去,房子车子结婚彩礼,为什么这辈子要活的这么累呢?");
System.out.println("难道来到这世间走这一遭就为了花一辈子时间买个房子吗?一个人不是也能活的轻松快乐吗?");
System.out.println("摆烂了,啊对对对");
//好了,emo结束,继续卷吧,人生因奋斗而美丽,这个世界虽然满目疮痍,但是还是有很多美好值得期待
}
}
}
可以看到,现在我们对父类的方法进行了重写,显然,父类的行为已经被我们给覆盖了,这个子类已经不具备父类的原本的行为,很显然违背了里氏替换原则。
要是程序员连敲代码都不会了,还能叫做程序员吗?
所以,对于这种情况,我们不需要再继承自Coder
了,我们可以提升一下,将此行为定义到People
中:
public abstract class People {
public abstract void coding(); //这个行为还是定义出来,但是不实现
class Coder extends People{
@Override
public void coding() {
System.out.println("我会打代码");
}
}
class JavaCoder extends People{
public void game(){
System.out.println("艾欧尼亚最强王者已上号");
}
public void coding() {
System.out.println("摆烂了,啊对对对");
}
}
}
里氏替换也是实现开闭原则的重要方式之一。
依赖倒转原则
依赖倒转原则(Dependence Inversion Principle)最典型的有Spring的设计。Spring框架是依赖倒置原则的一个典型例子,它提供了依赖注入(Dependency Injection, DI)机制,使得组件间的依赖关系可以被外部管理,而不是由组件自己管理,这有助于解耦组件,提高系统的可测试性和可维护性。
高层次的模块不应该依赖于低层次的模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
以Spring框架为例,在实际开发中,我们通常会使用一系列的分层,来将业务分为Controller
、Mapper
、Service
、ServiceImpl
各种层,在Mapper
、Service
层定义接口,在ServiceImpl
中实现
假设我们有一个UserService
类,它需要使用UserMapper
来操作数据库中的用户信息。在没有使用Spring的情况下,我们可能会这样编写代码:
public class UserService {
private UserMapper userMapper;
public UserService() {
this.userMapper = new UserMapper(); // 紧耦合
}
public void saveUser(User user) {
userMapper.save(user);
}
}
在这个例子中,UserService
类直接实例化了UserMapper
,这违反了依赖倒置原则,因为UserService
直接依赖于UserMapper
的具体实现。
使用Spring框架,我们可以改写上面的代码如下:
@Service
public class UserService {
private final UserMapper userMapper;
@Autowired
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public void saveUser(User user) {
userMapper.save(user);
}
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 定义一些数据操作方法
}
在这个版本中,UserService
不再直接实例化UserMapper
,而是通过构造器注入的方式,依赖于UserMapper
接口。Spring框架会负责实例化UserMapper
的具体实现,并将其注入到UserService
中。这种做法使得UserService
依赖于抽象的UserMapper
接口,而不是具体的实现,符合依赖倒置原则。
这样做的好处是,如果将来需要更换UserMapper
的实现,只需要更改配置,而无需修改UserService
的代码。同时,这也使得单元测试更加容易,因为我们可以很容易地为UserService
提供一个模拟的UserMapper
实现。
接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP)客户端不应该被迫依赖于它不使用的方法。如果一个接口太大,应该考虑将其拆分为更小、更具体的接口,这样客户端只需知道它们关心的部分。即
客户端不应依赖那些它不需要的接口。
假设我们有一个Machine
接口,其中定义了一些机器可能具有的通用方法,比如打印、扫描和复印:
public interface Machine {
void print(String document);
void fax(String document);
void scan(String document);
}
然后,我们有三种机器:打印机、扫描仪和多功能一体机。打印机和扫描仪分别只实现了其中的一部分功能,而多功能一体机实现了所有功能:
public class Printer implements Machine {
@Override
public void print(String document) {
System.out.println("Printing " + document);
}
@Override
public void fax(String document) {
throw new UnsupportedOperationException("Fax not supported");
}
@Override
public void scan(String document) {
throw new UnsupportedOperationException("Scan not supported");
}
}
public class Scanner implements Machine {
@Override
public void print(String document) {
throw new UnsupportedOperationException("Print not supported");
}
@Override
public void fax(String document) {
throw new UnsupportedOperationException("Fax not supported");
}
@Override
public void scan(String document) {
System.out.println("Scanning " + document);
}
}
public class MultiFunctionMachine implements Machine {
@Override
public void print(String document) {
System.out.println("Printing " + document);
}
@Override
public void fax(String document) {
System.out.println("Faxing " + document);
}
@Override
public void scan(String document) {
System.out.println("Scanning " + document);
}
}
这个例子问题在于,Printer
和Scanner
类都不得不实现它们并不需要的方法,这违反了ISP。
为了遵守接口隔离原则,我们可以将Machine
拆分成更具体地接口
public interface PrinterMachine {
void print(String document);
}
public interface FaxMachine {
void fax(String document);
}
public interface ScannerMachine {
void scan(String document);
}
然后实现他们真正需要的接口即可
public class Printer implements PrinterMachine {
@Override
public void print(String document) {
System.out.println("Printing " + document);
}
}
public class Scanner implements ScannerMachine {
@Override
public void scan(String document) {
System.out.println("Scanning " + document);
}
}
public class MultiFunctionMachine implements PrinterMachine, FaxMachine, ScannerMachine {
@Override
public void print(String document) {
System.out.println("Printing " + document);
}
@Override
public void fax(String document) {
System.out.println("Faxing " + document);
}
@Override
public void scan(String document) {
System.out.println("Scanning " + document);
}
}
迪米特法则
迪米特法则(Law of Demeter, LoD),又称最少知识原则(Least Knowledge Principle, LKP),主张一个软件实体应当尽可能少地与其他实体发生相互作用。
一个对象应该对其他对象有最少的知识。一个对象不应与太多其他对象发生直接交互,以减少对象之间的耦合度。
简单来说就是:一个类应该对其他类有最少的了解,以降低类之间的耦合度,使系统更易于理解、维护和扩展。迪米特法则通过避免类之间的过度交互来实现。如A类需要与B类通信,而B类又需要与C类通信,那么通常A类不应该直接访问C类,除非确实有必要。如果A类需要C类的信息,它应该通过B类来获取,或者通过某种中介者模式来间接获取。
举例:一个学生管理系统,包含老师、学生、课程等实体。如果在学生类中能直接访问到教师类,就可能会违背迪米特法则
public class Student {
private String name;
private Teacher teacher;
public Student(String name, Teacher teacher) {
this.name = name;
this.teacher = teacher;
}
public void printTeacherDetails() {
System.out.println("My teacher's name is " + teacher.getName());
System.out.println("He teaches at " + teacher.getDepartment().getName());
}
}
public class Teacher {
private String name;
private Department department;
public Teacher(String name, Department department) {
this.name = name;
this.department = department;
}
public String getName() {
return name;
}
public Department getDepartment() {
return department;
}
}
public class Department {
private String name;
public Department(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
在这个例子中,Student
类不仅知道它的Teacher
,还知道了Teacher
的Department
,这增加了类之间的耦合。
为了遵守迪米特法则,我们可以修改Student
类,使其只知道自己的Teacher
,而不知道Teacher
的Department
。如果Student
需要知道Department
的信息,它可以请求Teacher
提供这些信息,或者通过另一个实体(如UniversitySystem
)来获取
public class UniversitySystem {
public String getTeacherDepartmentName(Teacher teacher) {
return teacher.getDepartment().getName();
}
}
public class Student {
private String name;
private Teacher teacher;
private UniversitySystem universitySystem;
public Student(String name, Teacher teacher, UniversitySystem universitySystem) {
this.name = name;
this.teacher = teacher;
this.universitySystem = universitySystem;
}
public void printTeacherDetails() {
System.out.println("My teacher's name is " + teacher.getName());
System.out.println("He teaches at " + universitySystem.getTeacherDepartmentName(teacher));
}
}
public class Teacher {
private String name;
private Department department;
public Teacher(String name, Department department) {
this.name = name;
this.department = department;
}
public String getName() {
return name;
}
public Department getDepartment() {
return department;
}
}
public class Department {
private String name;
public Department(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
在这个修改后的例子中,Student
类只与Teacher
类和UniversitySystem
类交互,而UniversitySystem
类则负责处理Teacher
和Department
之间的交互,这样就减少了类之间的直接依赖,遵循了迪米特法则。
合成/聚合复用原则
合成/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)主张通过合成或聚合来重用代码,而不是通过继承。
在需要复用功能时,应优先使用对象组合或聚合而不是继承。
假设我们正在开发一个系统来管理不同类型的交通工具。我们有几种交通工具,比如汽车(Car)、自行车(Bicycle)和卡车(Truck)。每种交通工具都有一个引擎(Engine),但引擎的具体类型可能不同。
如果我们使用继承,可能会创建一个抽象的Vehicle
类,然后让Car
、Bicycle
和Truck
从Vehicle
继承。然而,Bicycle
实际上并不需要一个引擎,这会引入不必要的复杂性。而且,如果我们想更改引擎的实现,那么所有依赖于Vehicle
的子类都需要进行修改,这违反了开放封闭原则
相反,我们可以使用合成/聚合来解决这个问题。我们定义一个Engine
接口或抽象类,并让具体的引擎实现这个接口。然后,在Vehicle
类中,我们通过构造函数注入一个Engine
对象。这样,Vehicle
可以拥有一个引擎,而Bicycle
可以选择不拥有一个引擎
interface Engine {
void start();
void stop();
}
class PetrolEngine implements Engine {
@Override
public void start() {
System.out.println("Petrol engine started.");
}
@Override
public void stop() {
System.out.println("Petrol engine stopped.");
}
}
class Vehicle {
private Engine engine;
public Vehicle(Engine engine) {
this.engine = engine;
}
public void start() {
if (engine != null) {
engine.start();
}
}
public void stop() {
if (engine != null) {
engine.stop();
}
}
}
class Car extends Vehicle {
public Car(Engine engine) {
super(engine);
}
}
// 自行车类不需要引擎
class Bicycle extends Vehicle {
public Bicycle() {
super(null); // 自行车不需要引擎
}
}
public class Main {
public static void main(String[] args) {
Engine petrolEngine = new PetrolEngine();
Car car = new Car(petrolEngine);
car.start();
car.stop();
Bicycle bicycle = new Bicycle(); // 自行车没有引擎
bicycle.start(); // 没有引擎,所以啥都不做
}
}
在这个例子中,Vehicle
类通过构造函数接收一个Engine
对象,这是合成的一个典型应用。Car
类使用了一个具体的引擎实现(PetrolEngine
),而Bicycle
类则没有引擎,展示了聚合的灵活性。这种设计使得系统更易于扩展和维护,因为我们可以轻松地添加新的引擎类型或新的交通工具类型,而不必修改现有的代码