概述
面向对象设计原则是学习设计模式的基础,每一种设计模式都符合某一种或多种面向对象原则。通过在软件开发中使用这些原则,可以提高软件的可维护性和可复用性,让我们可以设计出更加灵活也更容易扩展的软件系统,实现可维护性复用的目标。
面向对象七大原则
- 单一职责原则
- 类的职责要单一,不能将太多的职责放在一个类中
- 用于控制类的粒度大小
- 开闭原则
- 软件实体对扩展是开放的,但是对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能
- 最重要的面向对象设计原则
- 里氏替换原则
- 在软件系统中,一个可以接收基类对象的地方必然可以接收一个子类对象
- 核心是对系统进行抽象
- 依赖倒转原则
- 要针对抽象层编程,而不是针对具体类编程
- 代码要依赖于抽象的类,而不是具体的类
- 接口隔离原则
- 使用多个专门的接口来取代一个统一的接口
- 合成复用原则
- 在复用功能时,应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
- 迪米特法则
- 一个软甲实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互
单一职责原则实例
某基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现
classDiagram
class Login
Login : + init() void
Login : + display() void
Login : + validate() void
Login : + getConnection() Connection
Login : + findUser(String name, String pwd) boolean
Login : + main() void
说明
- init() 方法用于初始化初始化按钮、文本框等界面控件
- display() 方法用于向界面容器中添加界面控件并显示窗口
- validate() 方法用于校验登录数据
- getConnection() 方法用于获取数据库连接对象
- findUser() 方法用于查询用户
- main() 方法是程序的入口
使用单一职责原则对其进行重构
拆分为如下4个类
- LoginForm 负责界面展示,它只包含与界面有关的方法和事件处理方法
- UserDAO 负责用户表的CRUD操作
- DBUtil 负责数据库的连接
- MainClass 负责启动系统
重构后类图如下
classDiagram
class MainClass{
+ main(String args[]) void
}
class LoginForm{
- dao : UserDAO
+ init() void
+ display() void
+ validate() void
}
class UserDAO{
- db : DBUtil
+ findUser(String name, String pwd) boolean
}
class DBUtil{
+ getConnection() Connection
}
MainClass ..> LoginForm
LoginForm --> UserDAO
UserDAO --> DBUtil
总结
通过单一职责原则重构后系统中类的个数增加,但是类的复用性很好。DBUtil类可供多个DAO类使用,而UserDAO类也可供多个界面类使用,一个类的修改不会对其他类产生影响,系统的可维护性也将增强
开闭原则实例
说明
某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求,要求使用不同的按钮,原始设计方案如下:
classDiagram
direction LR
class LoginForm{
- button : CircleButton
+ display() void
}
class CircleButton{
+ display() void
}
LoginForm --> CircleButton
classDiagram
direction LR
class LoginForm{
- button : RectangleButton
+ display() void
}
class RectangleButton{
+ display() void
}
LoginForm --> RectangleButton
如果界面类 LoginForm需要将圆形按钮(CircleButton)改为矩形按钮(RectangleButton),则需要修改LoginForm的源代码,修改按钮类的类名,由于圆形按钮和矩形按钮的显示方法不相同,因此还需要修改LoginForm的display()方法
使用开闭原则重构
增加一个抽象类,只需要修改配置文件,不需要修改类的源代码,即可使用不同的按钮
classDiagram
direction LR
class LoginForm{
- button : RectangleButton
+ display() void
}
class CircleButton{
+ display() void
}
class RectangleButton{
+ display() void
}
class AbstracButton{
+ display() void
}
LoginForm --> AbstracButton
CircleButton --|> AbstracButton
RectangleButton --|> AbstracButton
里氏替换原则
注意事项
- 如果一个方法只存在子类中,父类中不提供相应的声明,则无法在父类对象中直接使用该方法
- 使用里氏替换原则时,尽量把父类设计为抽象类或接口
里氏替换原则实例
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵");
}
}
class Test {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound();
animal2.makeSound();
}
}
在这个例子中,Dog和Cat类都是Animal类的子类。在main方法中,我们可以将Dog和Cat的对象赋值给Animal类型的变量,这体现了里氏替换原则,即子类可以替换父类出现在任何父类能够出现的地方,并且不会产生错误的行为。当调用makeSound方法时,会根据实际的对象类型执行相应子类的方法。
依赖倒转原则
以一个电商系统为例:
-
假设没有遵循依赖倒转原则:
- 业务逻辑层的订单处理类直接依赖于具体的数据库实现类,如
MySQLOrderDao。如果以后需要更换数据库,比如从 MySQL 切换到 Oracle,那么订单处理类就需要进行大量的修改,因为它与具体的数据库实现紧密耦合。
- 业务逻辑层的订单处理类直接依赖于具体的数据库实现类,如
-
遵循依赖倒转原则:
- 定义一个抽象的数据访问接口
OrderDao,其中包含了订单数据的增删改查等方法。 MySQLOrderDao和OracleOrderDao等具体的数据库实现类都实现这个接口。- 订单处理类只依赖于
OrderDao接口,而不关心具体的数据库实现。这样,无论以后更换哪种数据库,只需要提供新的数据库实现类,而订单处理类无需修改。
- 定义一个抽象的数据访问接口
接口隔离原则
假设我们正在开发一个图形绘制系统,有不同类型的图形需要绘制,如圆形(Circle)、矩形(Rectangle)和三角形(Triangle)。
- 不遵循接口隔离原则的情况:
interface Shape {
void drawCircle();
void drawRectangle();
void drawTriangle();
}
class Circle implements Shape {
@Override
public void drawCircle() {
// 绘制圆形的代码
}
@Override
public void drawRectangle() {
// 什么也不做,因为圆形不需要绘制矩形的方法
}
@Override
public void drawTriangle() {
// 什么也不做,因为圆形不需要绘制三角形的方法
}
}
class Rectangle implements Shape {
@Override
public void drawCircle() {
// 什么也不做,因为矩形不需要绘制圆形的方法
}
@Override
public void drawRectangle() {
// 绘制矩形的代码
}
@Override
public void drawTriangle() {
// 什么也不做,因为矩形不需要绘制三角形的方法
}
}
class Triangle implements Shape {
@Override
public void drawCircle() {
// 什么也不做,因为三角形不需要绘制圆形的方法
}
@Override
public void drawRectangle() {
// 什么也不做,因为三角形不需要绘制矩形的方法
}
@Override
public void drawTriangle() {
// 绘制三角形的代码
}
}
- 遵循接口隔离原则的情况:
interface CircleShape {
void drawCircle();
}
interface RectangleShape {
void drawRectangle();
}
interface TriangleShape {
void drawTriangle();
}
class Circle implements CircleShape {
@Override
public void drawCircle() {
// 绘制圆形的代码
}
}
class Rectangle implements RectangleShape {
@Override
public void drawRectangle() {
// 绘制矩形的代码
}
}
class Triangle implements TriangleShape {
@Override
public void drawTriangle() {
// 绘制三角形的代码
}
}
合成复用原则
假设我们正在开发一个图形绘制系统,有不同类型的图形需要绘制,并且我们希望能够对图形进行移动和缩放操作。
- 不遵循合成复用原则,过度使用继承的情况:
class Shape {
public void draw() {
// 绘制图形的通用方法
}
}
class Circle extends Shape {
@Override
public void draw() {
// 绘制圆形的具体方法
}
}
class Rectangle extends Shape {
@Override
public void draw() {
// 绘制矩形的具体方法
}
}
class MoveableShape extends Shape {
public void move() {
// 移动图形的方法
}
}
class ScalableShape extends Shape {
public void scale() {
// 缩放图形的方法
}
}
在这个例子中,为了给图形添加移动和缩放功能,我们使用了继承。但是这样会导致一些问题,比如如果父类Shape的draw方法发生改变,所有的子类包括MoveableShape和ScalableShape都可能受到影响。而且,如果我们有更多的功能需要添加,就需要不断地创建新的子类,导致类的层次结构变得复杂。
- 遵循合成复用原则,使用组合的情况:
interface Drawable {
void draw();
}
interface Moveable {
void move();
}
interface Scalable {
void scale();
}
class Circle implements Drawable {
@Override
public void draw() {
// 绘制圆形的具体方法
}
}
class Rectangle implements Drawable {
@Override
public void draw() {
// 绘制矩形的具体方法
}
}
class MoveableDecorator implements Moveable {
private Moveable decoratedShape;
public MoveableDecorator(Moveable decoratedShape) {
this.decoratedShape = decoratedShape;
}
@Override
public void move() {
// 移动图形的方法
decoratedShape.move();
}
}
class ScalableDecorator implements Scalable {
private Scalable decoratedShape;
public ScalableDecorator(Scalable decoratedShape) {
this.decoratedShape = decoratedShape;
}
@Override
public void scale() {
// 缩放图形的方法
decoratedShape.scale();
}
}
在这个例子中,我们使用了接口来定义不同的行为,然后通过组合的方式将这些行为动态地添加到图形对象上。如果我们需要一个可移动的圆形,我们可以这样创建:
Moveable moveableCircle = new MoveableDecorator(new Circle());
moveableCircle.move();
如果我们需要一个可移动和可缩放的矩形,我们可以这样创建:
Scalable scalableRectangle = new ScalableDecorator(new Rectangle());
Moveable moveableScalableRectangle = new MoveableDecorator(scalableRectangle);
moveableScalableRectangle.move();
scalableRectangle.scale();
迪米特法则
只依赖直接相关的对象,不依赖间接相关的对象,通过已经依赖的对象去访问间接相关的对象
如下文中的通过student通过course去访问teacher信息
假设我们有一个学校管理系统,其中有学生(Student)、教师(Teacher)和课程(Course)三个类。
- 不遵循迪米特法则的情况:
class Student {
private String name;
private Course[] courses;
public Student(String name) {
this.name = name;
}
public void addCourse(Course course) {
// 添加课程的方法
//...
}
public void showTeacherInfo(Teacher teacher) {
// 直接访问教师的信息,违反迪米特法则
System.out.println("Teacher " + teacher.getName() + " teaches " + teacher.getCoursesTaught());
}
}
class Teacher {
private String name;
private Course[] coursesTaught;
public Teacher(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Course[] getCoursesTaught() {
return coursesTaught;
}
}
class Course {
private String name;
public Course(String name) {
this.name = name;
}
}
在这个例子中,Student类直接访问了Teacher类的内部信息,违反了迪米特法则。这样会导致Student类和Teacher类之间的耦合度增加,当Teacher类的内部实现发生变化时,Student类可能也需要进行修改。
- 遵循迪米特法则的情况:
class Student {
private String name;
private Course[] courses;
public Student(String name) {
this.name = name;
}
public void addCourse(Course course) {
// 添加课程的方法
//...
}
public void showTeacherInfo(Teacher teacher, CourseService courseService) {
// 通过课程服务类间接获取教师信息,遵循迪米特法则
System.out.println("Teacher " + courseService.getTeacherNameByCourse(teacher) + " teaches " + courseService.getCoursesTaughtByTeacher(teacher));
}
}
class Teacher {
private String name;
private Course[] coursesTaught;
public Teacher(String name) {
this.name = name;
}
public String getName() {
return name;
}
public Course[] getCoursesTaught() {
return coursesTaught;
}
}
class Course {
private String name;
public Course(String name) {
this.name = name;
}
}
class CourseService {
public String getTeacherNameByCourse(Teacher teacher) {
// 根据课程获取教师名称的方法
//...
return teacher.getName();
}
public Course[] getCoursesTaughtByTeacher(Teacher teacher) {
// 获取教师所教课程的方法
//...
return teacher.getCoursesTaught();
}
}
在这个改进后的例子中,Student类不再直接访问Teacher类的内部信息,而是通过一个中间的CourseService类来获取教师的信息。这样降低了Student类和Teacher类之间的耦合度,当Teacher类的内部实现发生变化时,Student类不需要进行修改。