设计模式的七大原则旨在提高软件的可维护性、可复用性和可扩展性,包括:
这些原则指导开发者设计出更加健壯、灵活、易于维护的软件系统。
单一职责原则(Single Responsibility Principle, SRP)指一个类应该仅有一个引起它变化的原因。这意味着一个类应该只负责一项职责。
通俗地讲,单一职责原则就像是说,一个人只应该有一个工作。想象你有一个朋友,他既是厨师也是司机。如果有一天他因为烹饪而分心,导致开车出事了,那就是因为他承担了太多的责任。在编程中,如果一个类同时负责多件事情(比如,既存储数据又显示数据),那么当其中一部分需要改变时,很容易影响到其他部分。遵循单一职责原则,意味着每个类只负责一件事情,这样当需求变化时,只需修改有限的部分,减少错误,使代码更容易维护和理解。
class User {
private String userName;
private String userEmail;
public void saveUser(User user) {
// 保存用户信息到数据库
}
public void printUserDetails() {
// 打印用户详情
}
}
这个User
类违反了单一职责原则,因为它既处理用户信息的保存,又处理用户信息的打印,这两个职责应该由不同的类来承担。
为了遵守单一职责原则,我们可以将上述User
类分解为两个类:一个负责用户数据的管理,另一个负责用户数据的显示。
class User {
private String userName;
private String userEmail;
// 仅包含用户数据和基本操作
}
class UserPersistence {
public void saveUser(User user) {
// 保存用户信息到数据库
}
}
class UserDisplay {
public void printUserDetails(User user) {
// 打印用户详情
}
}
通过这种方式,我们将用户信息的保存和显示分别封装到了UserPersistence
和UserDisplay
类中,每个类都只负责单一的功能,从而遵循了单一职责原则。
Java生态系统中单一职责原则的体现可以在其标准库中找到许多例子,例如:
FileReader
、BufferedReader
等都专注于单一功能。List
、Map
、Set
接口分别专注于不同类型的集合操作。doGet
或doPost
方法来处理HTTP GET或POST请求,而不必同时处理两者,遵循了单一职责。这些类和接口的设计遵循了单一职责原则,每个类或接口都专注于一组特定的功能,使得Java生态系统更加模块化和易于维护。
开闭原则(Open-Closed Principle, OCP)是面向对象设计的核心原则之一,它指出软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着在不修改现有代码的情况下,应该能够添加新功能。
开闭原则就像是给你的应用程序装上了一个“扩展插槽”,让你可以随时增加新功能而不需要打开机器去重新焊接电路板。想象一下,如果你有一个游戏机,每当出现新游戏时,你不需要更换游戏机内部的硬件就能玩,只需要购买新的游戏卡带插上去即可。这样,游戏机的设计就允许了扩展(新增游戏),而不需要修改(打开游戏机更换部件),这正是开闭原则的精髓。
假设我们有一个根据用户类型计算折扣的系统:
class DiscountCalculator {
public double calculateDiscount(String userType) {
if ("VIP".equals(userType)) {
return 0.2;
} else {
return 0.1;
}
}
}
如果需要添加新的用户类型和折扣规则,我们必须修改DiscountCalculator
类,违反了开闭原则。
为了遵循开闭原则,我们可以定义一个接口和多个实现该接口的类,每个类对应一种用户类型的折扣计算方式:
interface DiscountStrategy {
double calculateDiscount();
}
class VIPDiscount implements DiscountStrategy {
public double calculateDiscount() {
return 0.2;
}
}
class RegularDiscount implements DiscountStrategy {
public double calculateDiscount() {
return 0.1;
}
}
class DiscountCalculator {
public double calculateDiscount(DiscountStrategy strategy) {
return strategy.calculateDiscount();
}
}
这样,当需要添加新的用户类型时,只需添加新的DiscountStrategy
实现类,无需修改现有的DiscountCalculator
类,从而遵循了开闭原则。
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的一个原则,它指出如果类A是类B的一个子类型,那么程序中使用类B的地方就可以不改变程序的行为的前提下,用类A来替换类B。
里氏替换原则就像是说,如果你有一个苹果,那么任何地方需要苹果的地方,你都可以用一个红苹果或一个绿苹果来替代,而不会影响到使用它的地方的正常运作。在编程中,如果你用一个子类对象替换了一个父类对象,那么程序还应该能够像原来一样运行,而不会出现错误或者异常行为。这就要求子类在继承父类的时候,不仅要继承父类的特性,还要保证这些特性的行为不被改变。
假设我们有一个矩形类和一个正方形类,正方形继承自矩形:
class Rectangle {
protected int width, height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
这里,Square
违反了LSP,因为它改变了Rectangle
的行为,使得设置宽度或高度的操作同时改变了另一方。
为了遵循LSP,我们可以抽象出一个共同的基类或接口,然后让Rectangle
和Square
分别实现它们自己的行为:
interface Shape {
int getArea();
}
class Rectangle implements Shape {
protected int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
在这个例子中,Rectangle
和Square
都遵循LSP,因为它们都独立实现了Shape
接口,且使用它们的代码不需要知道具体是哪一个形状类,从而保证了类型的替换不会导致程序行为的改变。
依赖倒置原则(Dependency Inversion Principle, DIP)指的是高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。这个原则的核心在于促进系统的解耦,从而使得系统更易于扩展和维护。
依赖倒置原则(Dependency Inversion Principle, DIP)的核心思想是高层模块不应该依赖低层模块,它们都应该依赖于抽象;抽象不应该依赖细节,细节应该依赖抽象。用通俗的语言来说,就像是建筑的设计不应该基于具体的砖块类型,而是基于砖块的一般特性。这样,无论使用什么样的砖块,只要符合这些特性,就能构建出建筑。在编程中,这意味着我们的代码应该依赖于接口或抽象类,而不是具体的实现类,这样可以使代码更灵活、更易于维护和扩展。
假设我们有一个LightBulb
类和一个ElectricPowerSwitch
类,后者依赖于前者:
class LightBulb {
public void turnOn() {
// 实现开灯
}
public void turnOff() {
// 实现关灯
}
}
class ElectricPowerSwitch {
private LightBulb lightBulb;
public ElectricPowerSwitch(LightBulb lightBulb) {
this.lightBulb = lightBulb;
}
public void press() {
// 实现开关灯
}
}
这个例子违反了DIP,因为ElectricPowerSwitch
直接依赖于LightBulb
的具体实现。
为了遵循DIP,我们可以引入一个抽象的接口,使得高层和低层模块都依赖于这个接口:
interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
public void turnOn() {
// 实现开灯
}
public void turnOff() {
// 实现关灯
}
}
class ElectricPowerSwitch {
private Switchable device;
public ElectricPowerSwitch(Switchable device) {
this.device = device;
}
public void press() {
// 实现控制任何Switchable设备
}
}
在这个例子中,通过引入Switchable
接口,ElectricPowerSwitch
不再依赖于LightBulb
的具体实现,而是依赖于抽象。这样,我们可以轻松地用另一个实现了Switchable
接口的类来替换LightBulb
,比如风扇,而不需要修改ElectricPowerSwitch
类的代码。
接口隔离原则(Interface Segregation Principle, ISP)强调不应该强迫客户依赖于它们不使用的接口。换句话说,更倾向于创建专门的接口而不是一个大而全的接口。
接口隔离原则(Interface Segregation Principle, ISP)讲的是不应该强迫客户依赖于它们不用的接口。用一个简单的例子来说,如果有一个多功能打印机,它可以打印、扫描和复印。根据接口隔离原则,我们不应该只有一个接口包含所有这些功能,因为不是每个使用打印机的人都需要扫描和复印的功能。相反,应该为打印、扫描和复印各自提供独立的接口。这样,只需要打印功能的用户就不必实现或依赖于扫描和复印的接口了。简而言之,接口隔离原则就是让接口更小、更专注,避免一个庞大的接口承担太多的职责。
假设我们有一个接口包含了太多的方法,客户类必须实现它们即使不需要所有的方法:
interface Worker {
void work();
void eat();
}
class HumanWorker implements Worker {
public void work() {
// 实现工作
}
public void eat() {
// 实现吃饭
}
}
class RobotWorker implements Worker {
public void work() {
// 实现工作
}
public void eat() {
// 机器人不需要吃饭,但依然需要实现该方法
}
}
遵循接口隔离原则,我们应该将Worker
接口拆分为更小的、更专门的接口:
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class HumanWorker implements Workable, Eatable {
public void work() {
// 实现工作
}
public void eat() {
// 实现吃饭
}
}
class RobotWorker implements Workable {
public void work() {
// 实现工作
}
// 机器人不需要实现Eatable接口
}
这样,每个类只需实现它实际需要的接口,避免了不必要的依赖。
合成/聚合复用原则(Composition/Aggregation Reuse Principle, CARP)强调使用对象的组合或聚合来实现新功能,而不是通过继承关系。这样做可以提高代码的灵活性和复用性。
合成/聚合复用原则就像是搭积木。想象你正在建造一个小屋,你可以选择用预制的部分(比如窗户、门等)来组合成你想要的结构,而不是自己从头开始制造每一个部分。在编程中,这个原则告诉我们应该通过将现有的对象(积木块)组合起来来创建新的功能,而不是通过继承一个大而全的类(从零开始造一个整体)。这样做使得代码更加灵活,因为你可以随时替换或者重新组合这些“积木块”,而不是被固定在一种设计之中。
合成/聚合复用原则鼓励使用对象的组合或聚合来实现功能的复用,而不是通过继承。这是因为继承会导致强耦合关系,使得父类和子类之间的依赖关系过于紧密,这样一来,修改父类可能会影响到所有的子类,增加了代码的复杂性和维护难度。相比之下,组合或聚合能够提供更加灵活的复用机制,对象之间的关系更加松散,修改一个对象不会直接影响到使用它的其他对象,这样有助于降低系统的耦合度,提高代码的可维护性和可扩展性。
通过继承实现复用:
class Vehicle {
void startEngine() {
// 启动引擎的代码
}
}
class Car extends Vehicle {
// Car 类继承了 Vehicle 类,复用了 startEngine 方法
}
这种方式使Car
类与Vehicle
类紧密耦合,限制了灵活性。
使用组合来实现复用:
class Engine {
void start() {
// 启动引擎的代码
}
}
class Car {
private Engine engine;
Car(Engine engine) {
this.engine = engine;
}
void startEngine() {
engine.start();
}
}
在这个例子中,Car
类通过包含Engine
类的实例(组合)来复用启动引擎的功能,而不是继承Vehicle
类。这样Car
类就可以更灵活地复用其他类的功能,同时也降低了类之间的耦合度。
迪米特法则(Law of Demeter, LoD),也称为最少知识原则,是一种软件开发的设计指导原则。它强调,一个对象应该对其他对象有尽可能少的了解,只与直接的朋友通信。直接的朋友指的是成员变量、方法参数或者对象创建的实例。
迪米特法则就像是说,一个人应该尽可能少地知道其他人的私事,只和直接的朋友交流。在编程中,这意味着一个类不应该知道太多其他类的细节,只和直接相关的类交互。这样做可以减少系统中的耦合,使得修改一个部分的时候,不会影响到太多其他部分,保持代码的整洁和可维护性。
class Paper {
public String getContent() {
return "content";
}
}
class Printer {
public void printPaper(Paper paper) {
System.out.println(paper.getContent());
}
}
class User {
void print() {
Printer printer = new Printer();
Paper paper = new Paper();
printer.printPaper(paper); // User 类直接与 Paper 类交互
}
}
在这个例子中,User
类需要知道Paper
类的细节才能打印内容,违反了迪米特法则。
class Paper {
private String content = "content";
public void printContent() {
System.out.println(content);
}
}
class Printer {
public void printPaper(Paper paper) {
paper.printContent();
}
}
class User {
void print() {
Printer printer = new Printer();
Paper paper = new Paper();
printer.printPaper(paper); // User 类不需要直接了解 Paper 类的内容
}
}
在这个改进后的例子中,User
类通过Printer
类来打印文档,而不需要直接与Paper
类交互。User
类仅与Printer
类有直接关系,遵循了迪米特法则。
聚合(Aggregation)表示一种弱“拥有”关系,对象间是整体与部分的关系,但部分可以离开整体而单独存在。
组合(Composition)表示一种强“拥有”关系,对象间也是整体与部分的关系,但部分不能离开整体而单独存在。
继承(Inheritance)表示一种“是”关系,用于表示一种类型是另一种类型的特化。
想象一下:
聚合就像是一支球队和球员的关系。球队由球员组成,但是球员可以离开球队,加入其他球队,球队和球员都可以独立存在。
组合就像是鸟和翅膀的关系。翅膀是鸟的一部分,翅膀不能脱离鸟单独存在。如果鸟不存在了,翅膀也就不存在了。
继承就像是孩子和父母的关系。孩子从父母那里继承了特征(比如眼睛的颜色,头发的类型)。孩子是父母的一个特殊版本,拥有父母的一些特性,同时也可能有自己的一些特性。
// 聚合示例
class Engine { }
class Car {
private Engine engine; // 聚合关系,Car有一个Engine,但Engine可以独立于Car存在
Car(Engine engine) {
this.engine = engine;
}
}
// 组合示例
class Room { }
class House {
private List<Room> rooms = new ArrayList<>(); // 组合关系,Room是House的一部分,不能独立存在
}
// 继承示例
class Vehicle {
void start() { }
}
class Bicycle extends Vehicle { // 继承关系,Bicycle是Vehicle的一种
}
在这些例子中,Car
和Engine
之间的聚合关系允许Engine
脱离Car
独立存在;House
和Room
之间的组合关系表明Room
不能脱离House
独立存在;Bicycle
继承自Vehicle
,表明每个Bicycle
也是一个Vehicle
。
“抽象不应依赖于细节,细节应依赖于抽象”是依赖倒置原则的核心思想。这句话意味着在设计软件时,高层模块(定义应用程序的核心行为)不应该依赖于低层模块(具体实现细节),而是两者都应该依赖于抽象(接口或抽象类)。同样,这些抽象不应该依赖于具体的实现细节,而具体的实现细节应该依赖于抽象。
遵循“抽象不应依赖于细节,细节应依赖于抽象”的原则具有重要意义,因为它促进了代码的灵活性和可维护性。这样做使得代码对改变更加开放:当系统的具体实现需要变化时,不需要对依赖于抽象的高层模块进行大幅度修改。这降低了代码间的耦合度,使得各个部分更容易理解、测试和重用。简而言之,这个原则帮助开发者构建出易于扩展和修改的系统,减少了未来可能的工作量和复杂性。
想象一下,你正在建造一栋房子。根据“抽象不应依赖细节,细节应依赖于抽象”的原则,你不应该从门把手或砖块开始规划整栋房子;相反,你应该从房子的设计图纸开始,这些图纸定义了房子的结构和它应该如何建造。
在编程中,这意味着你的代码(即房子的设计)不应该依赖于具体的实现(即门把手和砖块),而应该依赖于接口或抽象类(即图纸)。这样,如果你决定更换门把手或砖块(即具体的实现),你的代码仍然可以工作,因为它依赖的是抽象,而不是具体的细节。
// 抽象
interface Switchable {
void turnOn();
void turnOff();
}
// 细节
class LightBulb implements Switchable {
public void turnOn() { /* 实现开灯 */ }
public void turnOff() { /* 实现关灯 */ }
}
// 高层模块
class ElectricPowerSwitch {
private Switchable device; // 依赖于抽象,而不是具体细节
public ElectricPowerSwitch(Switchable device) {
this.device = device;
}
public void press() {
/* 使用 device,而不关心它是怎么实现的 */
}
}
在这个例子中,无论Switchable
背后是LightBulb
还是其他任何实现Switchable
的设备,ElectricPowerSwitch
都可以工作,因为它依赖于抽象而不是具体的细节。