7种结构型设计模式
结构型设计模式是软件设计中的一种模式,用于解决对象之间的组合、接口、继承等结构方面的问题。以下是7种常见的结构型设计模式以及它们的作用:
-
适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另一个接口,从而使得原本不兼容的类能够一起工作。
-
桥接模式(Bridge Pattern):将抽象部分与实现部分分离,使它们可以独立地变化,从而降低它们之间的耦合。
-
组合模式(Composite Pattern):将对象组合成树形结构,以表示“部分-整体”的层次结构。通过组合,客户可以一致地使用单个对象和组合对象。
-
装饰模式(Decorator Pattern):动态地将责任附加到对象上,扩展对象的功能,同时避免了使用子类来扩展功能的问题。
-
外观模式(Facade Pattern):为子系统中的一组接口提供一个统一的接口,从而简化客户端的调用和使用。
-
享元模式(Flyweight Pattern):通过共享对象来减小内存使用,当对象具有大量相似的状态时,可以共享这些状态,从而降低内存开销。
-
代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。代理对象可以起到保护和控制的作用。
适配器模式
适配器模式(Adapter Design Pattern)是一种结构型设计模式,它允许不兼容接口的类能够一起工作。适配器模式通过创建一个适配器来转换一个类的接口,使其能够适配另一个接口,从而实现两个不兼容接口之间的交互。
当我们实际开发中使用第三方库或遗留代码时,适配器模式非常有用。这种情况下,我们可能需要将第三方库或遗留代码的接口适配成我们自己系统中已有的接口,以便与现有代码无缝集成。以下是一个实际的案例,演示适配器模式的应用:
假设我们正在开发一个多媒体播放器应用,我们已经有一个 MediaPlayer
接口定义了播放媒体文件的方法,现在要使用一个第三方音频库 ExternalAudioPlayer
来播放音频文件,但是它的接口与我们的 MediaPlayer
接口不兼容。我们将使用适配器模式将 ExternalAudioPlayer
适配成我们的 MediaPlayer
接口。
我们的 MediaPlayer
接口:
// 我们的 MediaPlayer 接口
interface MediaPlayer {
void play(String filename);
}
第三方音频库 ExternalAudioPlayer
,它提供了不兼容的播放方法:
// 第三方音频库,接口不兼容
class ExternalAudioPlayer {
void playAudio(String filename) {
System.out.println("Playing audio: " + filename);
}
}
我们使用适配器模式创建一个适配器类 AudioPlayerAdapter
,将 ExternalAudioPlayer
适配成我们的 MediaPlayer
接口:
// 适配器类,将 ExternalAudioPlayer 适配成 MediaPlayer 接口
class AudioPlayerAdapter implements MediaPlayer {
private ExternalAudioPlayer externalAudioPlayer;
public AudioPlayerAdapter(ExternalAudioPlayer externalAudioPlayer) {
this.externalAudioPlayer = externalAudioPlayer;
}
@Override
public void play(String filename) {
// 调用 ExternalAudioPlayer 的不兼容方法
externalAudioPlayer.playAudio(filename);
}
}
现在,我们可以使用适配器模式适配 ExternalAudioPlayer
,使其能够使用我们的 MediaPlayer
接口:
public class AdapterDemo {
public static void main(String[] args) {
// 创建 ExternalAudioPlayer 对象
ExternalAudioPlayer externalAudioPlayer = new ExternalAudioPlayer();
// 使用适配器将 ExternalAudioPlayer 适配成我们的 MediaPlayer 接口
MediaPlayer audioPlayerAdapter = new AudioPlayerAdapter(externalAudioPlayer);
// 播放音频文件
audioPlayerAdapter.play("song.mp3");
}
}
在这个实际的案例中,我们使用适配器模式通过 AudioPlayerAdapter
将 ExternalAudioPlayer
适配成了我们的 MediaPlayer
接口。通过适配器,我们实现了两个不兼容接口之间的交互,并使得第三方音频库能够与我们的多媒体播放器应用无缝集成。
适配器模式在实际开发中非常常见,特别是在使用第三方库、遗留代码或需要与外部系统交互时。它使得代码复用更加灵活,同时保持了系统的扩展性和维护性。通过适配器模式,我们可以轻松地将不兼容的接口转换成我们需要的接口,从而实现系统间的无缝对接。
桥接模式
假设我们正在开发一个跨平台文件系统接口,我们希望支持不同的文件系统(例如Windows和Linux),并且每个文件系统又可以使用不同的存储介质(例如本地硬盘或网络存储)。这时,我们可以使用桥接模式来将文件系统接口与不同存储介质之间解耦,使得它们可以独立变化。
首先,我们定义一个文件系统接口 FileSystem
:
// 文件系统接口
interface FileSystem {
void writeFile(String fileName, String content);
String readFile(String fileName);
}
我们有不同的文件系统实现类,例如WindowsFileSystem和LinuxFileSystem:
// Windows 文件系统实现类
class WindowsFileSystem implements FileSystem {
@Override
public void writeFile(String fileName, String content) {
System.out.println("Writing file to Windows file system: " + fileName);
// 实际的写文件逻辑
}
@Override
public String readFile(String fileName) {
System.out.println("Reading file from Windows file system: " + fileName);
// 实际的读文件逻辑
return "File content from Windows";
}
}
// Linux 文件系统实现类
class LinuxFileSystem implements FileSystem {
@Override
public void writeFile(String fileName, String content) {
System.out.println("Writing file to Linux file system: " + fileName);
// 实际的写文件逻辑
}
@Override
public String readFile(String fileName) {
System.out.println("Reading file from Linux file system: " + fileName);
// 实际的读文件逻辑
return "File content from Linux";
}
}
接下来,定义一个抽象类 StorageMedium
来表示不同的存储介质:
// 存储介质抽象类
abstract class StorageMedium {
protected FileSystem fileSystem;
public StorageMedium(FileSystem fileSystem) {
this.fileSystem = fileSystem;
}
public abstract void storeFile(String fileName, String content);
public abstract String retrieveFile(String fileName);
}
创建不同的存储介质的具体实现类,例如LocalDisk和NetworkStorage:
// 本地硬盘存储介质实现类
class LocalDisk extends StorageMedium {
public LocalDisk(FileSystem fileSystem) {
super(fileSystem);
}
@Override
public void storeFile(String fileName, String content) {
System.out.println("Storing file on local disk: " + fileName);
fileSystem.writeFile(fileName, content);
}
@Override
public String retrieveFile(String fileName) {
System.out.println("Retrieving file from local disk: " + fileName);
return fileSystem.readFile(fileName);
}
}
// 网络存储介质实现类
class NetworkStorage extends StorageMedium {
public NetworkStorage(FileSystem fileSystem) {
super(fileSystem);
}
@Override
public void storeFile(String fileName, String content) {
System.out.println("Storing file on network storage: " + fileName);
fileSystem.writeFile(fileName, content);
}
@Override
public String retrieveFile(String fileName) {
System.out.println("Retrieving file from network storage: " + fileName);
return fileSystem.readFile(fileName);
}
}
现在,我们可以使用桥接模式将文件系统接口和存储介质解耦,并支持不同的文件系统和存储介质的组合。以下是一个示例代码:
public class BridgeDemo {
public static void main(String[] args) {
// 创建 Windows 文件系统
FileSystem windowsFileSystem = new WindowsFileSystem();
// 使用本地硬盘存储文件
StorageMedium localDisk = new LocalDisk(windowsFileSystem);
localDisk.storeFile("file.txt", "File content for local disk");
String content = localDisk.retrieveFile("file.txt");
System.out.println("File content retrieved: " + content);
System.out.println("--------------------------");
// 使用网络存储文件
StorageMedium networkStorage = new NetworkStorage(windowsFileSystem);
networkStorage.storeFile("file.txt", "File content for network storage");
content = networkStorage.retrieveFile("file.txt");
System.out.println("File content retrieved: " + content);
}
}
在上述案例中,我们使用桥接模式的主要原因是为了将文件系统接口和存储介质之间的实现解耦。桥接模式的优势如下:
-
解耦合:桥接模式将抽象部分和实现部分分离,使它们可以独立变化,从而降低了它们之间的耦合性。在我们的案例中,文件系统接口和存储介质是通过
StorageMedium
桥梁连接的,它们可以独立地进行扩展和修改,而不会相互影响。 -
可扩展性:桥接模式允许我们通过添加新的抽象部分或实现部分来扩展系统。在案例中,如果我们想要增加新的文件系统或存储介质,只需要创建新的文件系统实现类或存储介质实现类,并与现有的抽象类进行桥接即可,无需修改原有代码。
-
灵活组合:通过桥接模式,我们可以在运行时动态地组合不同的抽象部分和实现部分,从而实现不同的组合效果。在案例中,我们可以轻松地选择不同的文件系统和存储介质组合,以满足不同的业务需求。
-
代码可读性和可维护性:桥接模式将复杂性分解为两个独立的维度(抽象部分和实现部分),使得代码结构更清晰,易于理解和维护。这有助于提高代码的可读性和可维护性。
总体来说,桥接模式适用于需要将抽象和实现部分解耦的场景,特别是在需要支持多种组合或扩展性较强的情况下。它帮助我们实现灵活、可扩展和易维护的系统设计,从而提高了代码的质量和可维护性。在实际开发中,桥接模式常常用于处理多维度的变化,使得代码更加健壮和灵活。
组合模式
下面,我将通过两个案例来体现组合模式的特性
我们正在开发一个文件系统的类库,其中包含文件和目录两种类型。文件是叶子节点,而目录是复合节点,可以包含其他文件和目录。我们希望能够以树形结构表示整个文件系统,并能够对文件和目录进行一些操作,例如计算总文件大小、展示文件结构等。
首先,我们定义一个文件系统节点接口 FileSystemNode
,它包含了文件和目录的通用操作方法:
// 文件系统节点接口
interface FileSystemNode {
void display();
long getSize();
}
然后,我们有不同类型的文件系统节点类,包括文件和目录:
// 文件类,实现了文件系统节点接口
class File implements FileSystemNode {
private String name;
private long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public void display() {
System.out.println("File: " + name + ", Size: " + size + " bytes");
}
@Override
public long getSize() {
return size;
}
}
// 目录类,实现了文件系统节点接口
class Directory implements FileSystemNode {
private String name;
private List<FileSystemNode> nodes = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void addNode(FileSystemNode node) {
nodes.add(node);
}
@Override
public void display() {
System.out.println("Directory: " + name);
for (FileSystemNode node : nodes) {
node.display();
}
}
@Override
public long getSize() {
long totalSize = 0;
for (FileSystemNode node : nodes) {
totalSize += node.getSize();
}
return totalSize;
}
}
接下来,我们可以使用组合模式来构建文件系统的树形结构,并对整个文件系统进行操作:
public class CompositeDemo {
public static void main(String[] args) {
// 创建文件
File file1 = new File("file1.txt", 100);
File file2 = new File("file2.txt", 200);
// 创建目录
Directory dir1 = new Directory("dir1");
dir1.addNode(file1);
dir1.addNode(file2);
File file3 = new File("file3.txt", 150);
File file4 = new File("file4.txt", 250);
Directory dir2 = new Directory("dir2");
dir2.addNode(file3);
dir2.addNode(file4);
// 创建更高层级的目录
Directory rootDir = new Directory("root");
rootDir.addNode(dir1);
rootDir.addNode(dir2);
// 显示文件系统结构
System.out.println("File System Structure:");
rootDir.display();
// 计算总文件大小
long totalSize = rootDir.getSize();
System.out.println("\nTotal Size of File System: " + totalSize + " bytes");
}
}
在这个案例中,我们使用组合模式构建了一个文件系统的树形结构,并实现了对整个文件系统的操作。组合模式的特性和优势如下:
-
树形结构表示:组合模式允许我们使用树形结构表示整个文件系统,通过组合节点接口统一管理文件和目录等不同节点类型,使得文件系统结构更直观和易于理解。
-
统一操作接口:组合模式中的节点接口定义了统一的操作方法,使得对整个文件系统的操作更加统一和简洁,无需区分不同节点类型。
-
嵌套递归操作:组合模式支持递归操作,使得我们可以通过简单的操作调用来对整个文件系统进行处理。在示例中,通过递归调用
display()
方法和getSize()
方法,我们可以一次性处理整个文件系统。 -
可扩展性:组合模式允许我们轻松地增加新的节点类型(如新增新的文件类型或目录类型),并通过继承或实现节点接口来实现组合,而无需修改现有的代码。
-
代码复用:组合模式通过统一的节点接口,使得不同节点类的操作方法可以得到复用,减少了代码的冗余,提高了代码的可维护性。
我们再来看一个案例:
我们正在开发一个组织结构管理系统,其中包含公司、部门和员工等层级关系。我们希望能够以树形结构表示整个组织的层级关系,并能够对整个组织进行一些操作,例如计算总薪资、显示组织架构等。这时,我们可以使用组合模式来实现这个功能。
首先,我们定义一个组件接口 Component
,它包含了组织结构中所有节点的通用操作:
// 组件接口
interface Component {
void display();
double getSalary();
}
然后,我们有不同类型的节点类,包括公司、部门和员工等:
// 公司类,实现了组件接口
class Company implements Component {
private String name;
private List<Component> departments = new ArrayList<>();
public Company(String name) {
this.name = name;
}
public void addDepartment(Component department) {
departments.add(department);
}
@Override
public void display() {
System.out.println("Company: " + name);
for (Component department : departments) {
department.display();
}
}
@Override
public double getSalary() {
double totalSalary = 0;
for (Component department : departments) {
totalSalary += department.getSalary();
}
return totalSalary;
}
}
// 部门类,实现了组件接口
class Department implements Component {
private String name;
private List<Component> employees = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void addEmployee(Component employee) {
employees.add(employee);
}
@Override
public void display() {
System.out.println("Department: " + name);
for (Component employee : employees) {
employee.display();
}
}
@Override
public double getSalary() {
double totalSalary = 0;
for (Component employee : employees) {
totalSalary += employee.getSalary();
}
return totalSalary;
}
}
// 员工类,实现了组件接口
class Employee implements Component {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
@Override
public void display() {
System.out.println("Employee: " + name + ", Salary: " + salary);
}
@Override
public double getSalary() {
return salary;
}
}
接下来,我们可以通过组合模式来构建组织结构,并对整个组织进行操作:
public class CompositeDemo {
public static void main(String[] args) {
// 创建公司
Company company = new Company("ABC Corporation");
// 创建部门和员工
Department department1 = new Department("HR Department");
Department department2 = new Department("Finance Department");
Employee employee1 = new Employee("John", 5000);
Employee employee2 = new Employee("Jane", 6000);
Employee employee3 = new Employee("Mike", 5500);
Employee employee4 = new Employee("Alice", 6500);
// 构建组织结构
department1.addEmployee(employee1);
department1.addEmployee(employee2);
department2.addEmployee(employee3);
department2.addEmployee(employee4);
company.addDepartment(department1);
company.addDepartment(department2);
// 显示组织结构
company.display();
// 计算总薪资
double totalSalary = company.getSalary();
System.out.println("Total Salary: " + totalSalary);
}
}
在这个实际的案例中,我们使用组合模式构建了一个组织结构,并实现了对整个组织的一些操作。组合模式的特性和优势如下:
-
组织结构树形表示:组合模式允许我们使用树形结构表示整个组织结构,通过组件接口统一管理公司、部门和员工等不同节点类型,使得组织结构更直观和易于理解。
-
统一操作接口:组合模式中的组件接口定义了统一的操作方法,使得对整个组织的操作更加统一和简洁,无需区分不同节点类型。
-
嵌套递归操作:组合模式支持递归操作,使得我们可以通过简单的操作调用来对整个组织结构进行处理。在示例中,通过递归调用
display()
方法和getSalary()
方法,我们可以一次性处理整个组织。 -
可扩展性:组合模式允许我们轻松地增加新的组件类(如新增新的员工类型),并通过继承或实现组件接口来实现组合,而无需修改现有的代码。
-
代码复用:组合模式通过统一的组件接口,使得不同组件类的操作方法可以得到复用,减少了代码的冗余,提高了代码的可维护性。
总体来说,组合模式适用于需要以树形结构表示整体-部分层次关系,并希望统一处理整体和部分的场景。通过组合模式,我们可以更加灵活地组织和处理复杂的结构,提高代码的可读性和可维护性。在实际开发中,组合模式经常用于处理树形结构的设计和操作。
装饰模式
我们需要开发一个咖啡店点单系统,我们有不同类型的咖啡(如浓缩咖啡、拿铁、摩卡等),而且我们希望能够动态地为咖啡添加各种配料(如牛奶、巧克力、糖浆等)。在这个场景中,我们可以使用装饰模式来实现。
首先,我们定义一个咖啡接口 Coffee
,它包含了咖啡的通用操作方法:
// 咖啡接口
interface Coffee {
double getCost();
String getDescription();
}
然后,我们有不同类型的咖啡类,比如浓缩咖啡、拿铁和摩卡:
// 浓缩咖啡类,实现了咖啡接口
class Espresso implements Coffee {
@Override
public double getCost() {
return 2.0;
}
@Override
public String getDescription() {
return "Espresso";
}
}
// 拿铁咖啡类,实现了咖啡接口
class Latte implements Coffee {
@Override
public double getCost() {
return 3.5;
}
@Override
public String getDescription() {
return "Latte";
}
}
// 摩卡咖啡类,实现了咖啡接口
class Mocha implements Coffee {
@Override
public double getCost() {
return 4.0;
}
@Override
public String getDescription() {
return "Mocha";
}
}
接下来,我们定义装饰者接口 CoffeeDecorator
,它也实现了咖啡接口,并可以在不修改原有咖啡类的基础上,动态地为咖啡添加配料:
// 装饰者接口
interface CoffeeDecorator extends Coffee {
}
然后,我们有不同类型的装饰者类,比如牛奶、巧克力和糖浆:
// 牛奶装饰者类,实现了装饰者接口
class MilkDecorator implements CoffeeDecorator {
private Coffee coffee;
public MilkDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
}
// 巧克力装饰者类,实现了装饰者接口
class ChocolateDecorator implements CoffeeDecorator {
private Coffee coffee;
public ChocolateDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 1.5;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Chocolate";
}
}
// 糖浆装饰者类,实现了装饰者接口
class SyrupDecorator implements CoffeeDecorator {
private Coffee coffee;
public SyrupDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", Syrup";
}
}
现在,我们可以使用装饰模式来动态地为咖啡添加配料,并计算咖啡的总价:
public class DecoratorDemo {
public static void main(String[] args) {
// 点一杯浓缩咖啡
Coffee espresso = new Espresso();
System.out.println("Coffee: " + espresso.getDescription());
System.out.println("Cost: $" + espresso.getCost());
// 加入牛奶和巧克力
CoffeeDecorator latteWithMilkAndChocolate = new ChocolateDecorator(new MilkDecorator(new Latte()));
System.out.println("\nCoffee: " + latteWithMilkAndChocolate.getDescription());
System.out.println("Cost: $" + latteWithMilkAndChocolate.getCost());
// 加入糖浆
CoffeeDecorator mochaWithSyrup = new SyrupDecorator(new Mocha());
System.out.println("\nCoffee: " + mochaWithSyrup.getDescription());
System.out.println("Cost: $" + mochaWithSyrup.getCost());
}
}
在这个案例中,我们使用装饰模式动态地为咖啡添加不同的配料,而不需要修改原有的咖啡类。装饰模式的特性和优势如下:
-
动态扩展:装饰模式允许我们在运行时动态地为对象添加新的行为,而无需修改原有的类。在示例中,我们可以通过组合不同的装饰者类来动态地为咖啡添加不同的配料,而不需要修改咖啡类本身。
-
灵活组合:装饰模式支持将多个装饰者类组合在一起,从而实现不同的组合效果。在示例中,我们可以自由地组合不同的装饰者类,从而得到各种不同口味的咖啡。
-
单一职责原则:装饰模式遵循单一职责原则,每个具体装饰者类只负责为对象添加一个特定的行为,使得代码更加清晰和可维护。
-
避免类爆炸:装饰模式避免了类爆炸问题,即不需要创建大量的类来处理各种不同组合情况。通过装饰模式,我们可以通过组合少量的类来实现各种复杂的行为组合,从而保持代码的简洁性。
总体而言,装饰模式允许我们动态地为对象添加新的行为,提高了代码的灵活性和可扩展性,并且遵循了设计原则,使得代码更加清晰和易于维护。在实际开发中,装饰模式常常用于动态地扩展对象的功能,避免类爆炸问题,以及在框架和库的设计中。
再来看一个案例:
我们正在开发一个图形绘制应用,支持绘制不同类型的图形,如圆形、矩形和椭圆。我们希望能够动态地为这些图形添加不同的样式,比如填充颜色和边框样式。在这个场景中,我们可以使用装饰模式来实现。
首先,我们定义一个图形接口 Shape
,它包含了绘制图形和获取描述的通用操作方法:
// 图形接口
interface Shape {
void draw();
String getDescription();
}
然后,我们有不同类型的图形类,包括圆形、矩形和椭圆:
// 圆形类,实现了图形接口
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
@Override
public String getDescription() {
return "Circle";
}
}
// 矩形类,实现了图形接口
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
@Override
public String getDescription() {
return "Rectangle";
}
}
// 椭圆类,实现了图形接口
class Ellipse implements Shape {
@Override
public void draw() {
System.out.println("Drawing Ellipse");
}
@Override
public String getDescription() {
return "Ellipse";
}
}
接下来,我们定义装饰者接口 ShapeDecorator
,它也实现了图形接口,并可以在不修改原有图形类的基础上,动态地为图形添加样式:
// 装饰者接口
interface ShapeDecorator extends Shape {
}
然后,我们有不同类型的装饰者类,比如填充颜色和边框样式:
// 填充颜色装饰者类,实现了装饰者接口
class FillColorDecorator implements ShapeDecorator {
private Shape shape;
private String color;
public FillColorDecorator(Shape shape, String color) {
this.shape = shape;
this.color = color;
}
@Override
public void draw() {
shape.draw();
System.out.println("Fill Color: " + color);
}
@Override
public String getDescription() {
return shape.getDescription() + " with Fill Color: " + color;
}
}
// 边框样式装饰者类,实现了装饰者接口
class BorderStyleDecorator implements ShapeDecorator {
private Shape shape;
private String borderStyle;
public BorderStyleDecorator(Shape shape, String borderStyle) {
this.shape = shape;
this.borderStyle = borderStyle;
}
@Override
public void draw() {
shape.draw();
System.out.println("Border Style: " + borderStyle);
}
@Override
public String getDescription() {
return shape.getDescription() + " with Border Style: " + borderStyle;
}
}
现在,我们可以使用装饰模式来动态地为图形添加样式,并绘制图形:
public class DecoratorDemo {
public static void main(String[] args) {
// 绘制一个红色的圆形
Shape circle = new Circle();
ShapeDecorator redCircle = new FillColorDecorator(circle, "Red");
redCircle.draw();
System.out.println("Description: " + redCircle.getDescription());
// 绘制一个蓝色的矩形,并添加虚线边框样式
Shape rectangle = new Rectangle();
ShapeDecorator blueDashedRectangle = new BorderStyleDecorator(new FillColorDecorator(rectangle, "Blue"), "Dashed");
blueDashedRectangle.draw();
System.out.println("Description: " + blueDashedRectangle.getDescription());
// 绘制一个绿色的椭圆,并添加实线边框样式
Shape ellipse = new Ellipse();
ShapeDecorator greenSolidEllipse = new BorderStyleDecorator(new FillColorDecorator(ellipse, "Green"), "Solid");
greenSolidEllipse.draw();
System.out.println("Description: " + greenSolidEllipse.getDescription());
}
}
我们通过装饰模式动态地为图形添加样式,而不需要修改原有的图形类。通过组合不同的装饰者类,我们可以为图形添加不同的样式组合。
外观模式
外观模式(Facade Pattern)是一种结构型设计模式,旨在为复杂的子系统提供一个简单的接口,以便客户端可以更方便地使用该子系统。外观模式隐藏了子系统的复杂性,使得客户端与子系统之间的交互变得简单而统一。
让我们通过一个简单的汽车生产厂家的实例来展示外观模式的特性和优势。
假设汽车生产厂家有多个子系统,包括发动机制造、车身制造、涂装和组装等,每个子系统都有复杂的操作和步骤。为了让客户端更方便地订购汽车,我们可以创建一个外观类 CarProductionFacade
,它提供了一个简单的接口,隐藏了子系统的复杂性。
首先,我们定义子系统的接口和实现:
// 发动机制造子系统接口
interface EngineSubsystem {
void createEngine();
}
// 发动机制造子系统实现
class EngineManufacture implements EngineSubsystem {
@Override
public void createEngine() {
System.out.println("Manufacturing Engine");
}
}
// 车身制造子系统接口
interface BodySubsystem {
void createBody();
}
// 车身制造子系统实现
class BodyManufacture implements BodySubsystem {
@Override
public void createBody() {
System.out.println("Manufacturing Body");
}
}
// 涂装子系统接口
interface PaintingSubsystem {
void paint();
}
// 涂装子系统实现
class Painting implements PaintingSubsystem {
@Override
public void paint() {
System.out.println("Painting Car");
}
}
// 组装子系统接口
interface AssemblySubsystem {
void assemble();
}
// 组装子系统实现
class Assembly implements AssemblySubsystem {
@Override
public void assemble() {
System.out.println("Assembling Car");
}
}
接下来,我们创建外观类 CarProductionFacade
,该类提供了一个简单的接口来订购汽车,隐藏了子系统的复杂性:
// 外观类
class CarProductionFacade {
private EngineSubsystem engineSubsystem;
private BodySubsystem bodySubsystem;
private PaintingSubsystem paintingSubsystem;
private AssemblySubsystem assemblySubsystem;
public CarProductionFacade() {
engineSubsystem = new EngineManufacture();
bodySubsystem = new BodyManufacture();
paintingSubsystem = new Painting();
assemblySubsystem = new Assembly();
}
// 简化订购汽车的接口
public void orderCar() {
System.out.println("Ordering Car...");
engineSubsystem.createEngine();
bodySubsystem.createBody();
paintingSubsystem.paint();
assemblySubsystem.assemble();
System.out.println("Car is ready!");
}
}
现在,客户端可以通过 CarProductionFacade
来订购汽车,而不需要关心底层子系统的复杂性:
public class FacadeDemo {
public static void main(String[] args) {
CarProductionFacade carProductionFacade = new CarProductionFacade();
carProductionFacade.orderCar();
}
}
在这个例子中,我们通过外观模式将汽车生产厂家的复杂子系统进行了封装,提供了一个简单的接口 orderCar()
,让客户端能够更方便地订购汽车。
我们来举一个更实际的案例,假设我们正在开发一个多媒体播放器应用,该应用能够播放不同格式的音频文件(比如MP3、WAV等)和视频文件(比如MP4、AVI等)。不同格式的文件需要使用不同的解码器来进行解码和播放。
在这个案例中,我们可以使用外观模式来创建一个多媒体播放器外观类 MediaPlayerFacade
,它隐藏了不同格式的文件解码器的复杂性,提供了一个简单的接口来播放不同格式的多媒体文件。
首先,我们定义音频文件解码器接口和视频文件解码器接口:
// 音频文件解码器接口
interface AudioDecoder {
void decode(String fileName);
}
// 视频文件解码器接口
interface VideoDecoder {
void decode(String fileName);
}
然后,我们有不同格式的音频文件解码器和视频文件解码器实现:
// MP3音频文件解码器实现
class Mp3Decoder implements AudioDecoder {
@Override
public void decode(String fileName) {
System.out.println("Decoding MP3 file: " + fileName);
}
}
// MP4视频文件解码器实现
class Mp4Decoder implements VideoDecoder {
@Override
public void decode(String fileName) {
System.out.println("Decoding MP4 video file: " + fileName);
}
}
接下来,我们创建多媒体播放器外观类 MediaPlayerFacade
,该类提供了一个简单的接口来播放不同格式的多媒体文件:
// 外观类
class MediaPlayerFacade {
private AudioDecoder audioDecoder;
private VideoDecoder videoDecoder;
public MediaPlayerFacade() {
audioDecoder = new Mp3Decoder();
videoDecoder = new Mp4Decoder();
}
// 简化播放音频文件的接口
public void playAudio(String fileName) {
System.out.println("Playing audio file: " + fileName);
audioDecoder.decode(fileName);
}
// 简化播放视频文件的接口
public void playVideo(String fileName) {
System.out.println("Playing video file: " + fileName);
videoDecoder.decode(fileName);
}
}
现在,客户端可以通过 MediaPlayerFacade
来播放不同格式的多媒体文件,而不需要关心底层解码器的复杂性:
public class FacadeDemo {
public static void main(String[] args) {
MediaPlayerFacade mediaPlayer = new MediaPlayerFacade();
// 播放MP3音频文件
mediaPlayer.playAudio("song.mp3");
// 播放MP4视频文件
mediaPlayer.playVideo("movie.mp4");
}
}
在这个案例中,我们通过外观模式将多媒体播放器的复杂子系统进行了封装,提供了一个简单的接口 playAudio()
和 playVideo()
,让客户端能够更方便地播放不同格式的多媒体文件。
外观模式的优势在此案例中体现如下:
-
简化客户端代码:客户端不需要了解复杂的多媒体播放器结构,只需调用外观类的简单接口即可完成播放操作。
-
隐藏复杂性:外观模式将多媒体播放器的复杂子系统封装起来,对于客户端来说,解码器的复杂性是透明的,只需要调用外观类的方法即可完成播放。
-
松散耦合:外观模式实现了客户端与多媒体播放器之间的松散耦合,客户端不依赖于具体解码器的实现,从而降低了代码的耦合度,提高了系统的灵活性和可维护性。
-
提供高层接口:外观模式为复杂子系统提供了一个高层接口,使得多媒体播放器的功能更容易使用和理解。
总体而言,外观模式通过简化接口和隐藏复杂性,提供了一个更加友好和易用的接口给客户端,同时降低了系统的耦合度,使得系统更加灵活和易于维护。在实际开发中,外观模式常常用于简化复杂子系统的使用,提高代码的可读性和可维护性。
享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来有效地支持大量细粒度的对象。它适用于当一个应用程序中需要创建大量相似对象时,通过共享这些对象的内部状态,可以减少内存占用和提高性能。
让我们通过一个游戏中的角色装备系统来展示享元模式的特性和优势。
假设我们正在开发一个多人在线角色扮演游戏(MMORPG),游戏中有许多玩家,每个玩家都有自己的角色,并且可以选择不同的装备进行装备。在这个游戏中,有很多种装备,比如武器、护甲等,而且玩家之间可能会装备相同类型的装备。
首先,我们定义一个装备接口 Equipment
,它包含了装备的通用操作方法:
// 装备接口
interface Equipment {
void use(Player player);
}
然后,我们有不同类型的装备类,比如武器和护甲:
// 武器类,实现了装备接口
class Weapon implements Equipment {
private String name;
public Weapon(String name) {
this.name = name;
}
@Override
public void use(Player player) {
System.out.println(player.getName() + " is using " + name);
}
}
// 护甲类,实现了装备接口
class Armor implements Equipment {
private String name;
public Armor(String name) {
this.name = name;
}
@Override
public void use(Player player) {
System.out.println(player.getName() + " is wearing " + name);
}
}
接下来,我们定义一个装备工厂类 EquipmentFactory
,该类用于创建并管理共享的装备对象:
import java.util.HashMap;
import java.util.Map;
// 装备工厂类
class EquipmentFactory {
private Map<String, Equipment> equipmentMap;
public EquipmentFactory() {
equipmentMap = new HashMap<>();
}
// 获取装备对象
public Equipment getEquipment(String type, String name) {
String key = type + "_" + name;
if (!equipmentMap.containsKey(key)) {
if ("Weapon".equalsIgnoreCase(type)) {
equipmentMap.put(key, new Weapon(name));
} else if ("Armor".equalsIgnoreCase(type)) {
equipmentMap.put(key, new Armor(name));
} else {
throw new IllegalArgumentException("Invalid equipment type: " + type);
}
}
return equipmentMap.get(key);
}
}
最后,我们定义一个玩家类 Player
,它包含了玩家的名称和装备的方法:
// 玩家类
class Player {
private String name;
private Equipment weapon;
private Equipment armor;
public Player(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 装备武器
public void equipWeapon(String weaponName) {
EquipmentFactory equipmentFactory = new EquipmentFactory();
weapon = equipmentFactory.getEquipment("Weapon", weaponName);
weapon.use(this);
}
// 装备护甲
public void equipArmor(String armorName) {
EquipmentFactory equipmentFactory = new EquipmentFactory();
armor = equipmentFactory.getEquipment("Armor", armorName);
armor.use(this);
}
}
现在,我们可以通过 Player
类来装备不同的武器和护甲。请注意,当多个玩家装备相同的装备时,实际上它们共享了相同的装备对象,因为在装备工厂类中进行了对象共享。
public class FlyweightDemo {
public static void main(String[] args) {
Player player1 = new Player("Alice");
Player player2 = new Player("Bob");
// 玩家Alice装备武器
player1.equipWeapon("Sword");
player1.equipArmor("Leather Armor");
// 玩家Bob装备武器
player2.equipWeapon("Sword");
player2.equipArmor("Chain Mail");
}
}
在这个案例中,享元模式的特性和优势在以下方面体现:
-
节省内存:通过共享相同类型的装备对象,避免了创建大量重复的对象,节省了内存空间。当多个玩家装备相同的武器或护甲时,它们实际上共享了相同的装备对象。
-
提高性能:由于共享了相同的装备对象,减少了对象的创建和销毁操作,从而提高了性能。
-
简化复杂性:通过装备工厂类管理共享的装备对象,隐藏了对象的创建细节,简化了客户端代码。
-
分离内部状态和外部状态:享元模式将对象的内部状态(例如装备名称)和外部状态(例如玩家名称)分离,使得可以共享内部状态而不需要共享整个对象。
Java的源码中有使用享元模式的例子。一个典型的例子就是Java中的字符串常量池(String Pool)。
在Java中,字符串是不可变的,当创建一个字符串时,如果字符串常量池中已经存在相同内容的字符串,则直接返回常量池中的字符串对象,而不会创建新的对象。这就是享元模式的一种应用。
让我们通过一个简单的例子来演示Java中的字符串常量池,以及如何利用享元模式来节省内存。
public class FlyweightDemo {
public static void main(String[] args) {
// 创建两个相同内容的字符串
String str1 = "Hello";
String str2 = "Hello";
// 判断两个字符串是否为同一个对象
System.out.println("str1 == str2: " + (str1 == str2)); // 输出:str1 == str2: true
// 创建两个不同内容的字符串
String str3 = new String("Hello");
String str4 = new String("Hello");
// 判断两个字符串是否为同一个对象
System.out.println("str3 == str4: " + (str3 == str4)); // 输出:str3 == str4: false
// 判断两个字符串内容是否相等
System.out.println("str1.equals(str3): " + str1.equals(str3)); // 输出:str1.equals(str3): true
}
}
如果对享元模式的概念还比较模糊,可以看下面这个案例,非常简洁明了。
假设我们正在开发一个简单的文字处理程序,需要处理大量的字符文本,我们希望能够高效地管理这些字符对象,避免创建大量重复的字符对象,从而节省内存和提高性能。
首先,我们定义一个 Character
接口,表示字符对象:
// 字符接口
interface Character {
void display();
}
然后,我们创建两个具体的字符类,分别表示不同的字符:
// 'A' 字符类
class CharacterA implements Character {
@Override
public void display() {
System.out.print("A");
}
}
// 'B' 字符类
class CharacterB implements Character {
@Override
public void display() {
System.out.print("B");
}
}
接下来,我们实现享元工厂类 CharacterFactory
,用于创建和管理字符对象。在工厂类中,我们使用一个 HashMap
来缓存已经创建的字符对象,并在需要时返回缓存中的对象。
import java.util.HashMap;
import java.util.Map;
// 享元工厂类
class CharacterFactory {
private Map<CharacterType, Character> characterCache = new HashMap<>();
// 根据字符类型获取字符对象
public Character getCharacter(CharacterType type) {
Character character = characterCache.get(type);
if (character == null) {
switch (type) {
case A:
character = new CharacterA();
break;
case B:
character = new CharacterB();
break;
// 在实际应用中,可能会有更多的字符类型
}
characterCache.put(type, character);
}
return character;
}
}
我们定义一个枚举 CharacterType
来表示不同的字符类型:
enum CharacterType {
A, B
// 在实际应用中,可能会有更多的字符类型
}
现在,我们可以在主程序中使用享元模式来处理大量的字符文本,共享已经创建过的字符对象,从而节省内存和提高性能:
public class FlyweightDemo {
public static void main(String[] args) {
String text = "ABBABAA";
CharacterFactory characterFactory = new CharacterFactory();
for (char c : text.toCharArray()) {
Character character = characterFactory.getCharacter(CharacterType.valueOf(String.valueOf(c)));
character.display();
}
}
}
在上面的例子中,我们将字符串 "ABBABAA" 中的字符依次通过享元工厂类 CharacterFactory
获取对应的字符对象,并调用 display()
方法进行展示。由于字符对象已经缓存,相同的字符只会创建一次对象并进行复用,从而避免了大量的重复创建,节省了内存空间。
代理模式
让我们通过一个实际案例来展示代理模式的特性和优势。假设我们正在开发一个简单的远程文件访问程序,我们希望能够在客户端和服务器之间添加一个代理来进行文件访问,以增加一些额外的功能,比如权限控制和文件缓存。
首先,我们定义一个共同的文件接口 File
:
// 文件接口
interface File {
void read();
}
然后,我们创建一个具体的文件类 RealFile
,实现文件接口,表示真实的文件对象:
// 真实文件类
class RealFile implements File {
private String filename;
public RealFile(String filename) {
this.filename = filename;
loadFromDisk();
}
@Override
public void read() {
System.out.println("Reading file: " + filename);
}
private void loadFromDisk() {
System.out.println("Loading file from disk: " + filename);
}
}
接下来,我们实现代理类 FileProxy
,代理类和真实文件类都实现了文件接口,代理类持有一个真实文件对象的引用,并在需要时创建或使用真实文件对象。
// 文件代理类
class FileProxy implements File {
private RealFile realFile;
private String filename;
public FileProxy(String filename) {
this.filename = filename;
}
@Override
public void read() {
if (realFile == null) {
realFile = new RealFile(filename);
}
realFile.read();
}
}
现在,我们可以在客户端中使用代理模式来访问文件,代理类在必要时会创建真实文件对象,并调用真实文件对象的方法。
public class ProxyDemo {
public static void main(String[] args) {
File file = new FileProxy("example.txt");
// 第一次读取文件,代理类创建真实文件对象并读取文件
file.read();
// 第二次读取文件,代理类直接使用已经创建的真实文件对象进行读取
file.read();
}
}
在上面的例子中,我们通过代理类 FileProxy
来访问文件,并模拟了两次文件访问。第一次读取文件时,代理类会创建一个真实的文件对象 RealFile
,并从磁盘加载文件内容;第二次读取文件时,代理类直接使用已经创建的真实文件对象,从内存中读取文件内容。
代理模式的特性和优势:
- 代理模式允许在访问对象之前或之后添加一些额外的处理逻辑。在我们的例子中,代理类
FileProxy
可以在读取文件之前进行权限检查、文件缓存等操作。 - 代理模式可以隐藏真实对象的实现细节,客户端只需要与代理类进行交互,而不需要直接访问真实对象。这样,代理模式提供了更好的封装和隔离,提高了系统的安全性和稳定性。
- 代理模式可以延迟真实对象的创建,当真实对象的创建和初始化过程较为耗时时,代理模式可以在需要时才进行真实对象的创建,提高了系统的性能和资源利用率。
- 代理模式还支持更复杂的代理结构,例如虚拟代理、远程代理和动态代理等,可以根据具体的需求灵活地扩展和组合代理。
从这个案例来看,代理模式和享元模式似乎非常相似,但代理模式和享元模式实际上有一些关键的区别。
-
目的不同:
- 代理模式的主要目的是控制对对象的访问。代理模式通过引入代理类来封装对象,代理类可以在访问对象之前或之后添加一些额外的处理逻辑,如权限控制、缓存、延迟加载等。代理模式着重于在客户端和真实对象之间加入中间层,提供更多的控制和管理。
- 享元模式的主要目的是尽量共享对象以节省内存。享元模式通过共享内部状态来避免创建大量相似对象,从而节省内存空间。享元模式着重于优化内存和资源的使用,对于一些可复用的对象,通过共享内部状态来避免重复创建,提高性能和资源利用率。
-
使用场景不同:
- 代理模式通常用于在访问对象时增加一些控制层,比如远程代理、虚拟代理、安全代理等,用于控制对真实对象的访问。代理模式适用于需要在访问对象时增加额外功能的情况,以及需要控制对对象的访问权限或频率的情况。
- 享元模式通常用于优化大量相似对象的创建和内存消耗问题。当需要创建大量具有相同或相似状态的对象时,可以使用享元模式来共享内部状态,避免创建过多的对象。享元模式适用于需要频繁创建相似对象的场景,通过共享内部状态来节省内存。
尽管在某些情况下,代理模式和享元模式可能在结构上有一些相似之处,但它们的目的和使用场景是不同的。代理模式关注于控制访问,而享元模式关注于优化内存和资源的使用。理解这些模式的不同特点和适用场景,有助于在设计中选择合适的模式来解决具体的问题。
我们来改进一下案例展示代理模式的特性,特别是在控制访问和增加额外功能方面的应用。
我们希望在文件访问时增加权限控制功能,即只有具有特定权限的用户才能读取敏感文件,同时希望记录每次文件访问的日志。
首先,我们定义文件接口 File
:
// 文件接口
interface File {
void read();
}
然后,我们创建一个具体的文件类 RealFile
,实现文件接口,表示真实的文件对象:
// 真实文件类
class RealFile implements File {
private String filename;
public RealFile(String filename) {
this.filename = filename;
}
@Override
public void read() {
System.out.println("Reading file: " + filename);
}
}
接下来,我们实现代理类 FileProxy
,代理类也实现了文件接口,并在需要时通过代理类来创建真实的文件对象,并在文件访问时增加权限控制和日志记录功能。
// 文件代理类
class FileProxy implements File {
private String filename;
private RealFile realFile;
public FileProxy(String filename, boolean isAdminUser) {
this.filename = filename;
this.isAdminUser = isAdminUser;
}
@Override
public void read() {
if (realFile == null) {
// 增加权限控制功能(假设只有 admin 用户具有读取权限)
if (!isAdminUser()) {
System.out.println("Access denied. You don't have permission to read this file.");
return;
}
// 创建真实文件对象
realFile = new RealFile(filename);
}
// 增加日志记录功能
System.out.println("Logging file access: " + filename);
// 读取文件
realFile.read();
}
public boolean isAdminUser() {
return isAdminUser;
}
}
现在,我们可以在客户端中使用代理模式来访问文件,代理类 FileProxy
可以在需要时创建真实的文件对象,并在文件访问时增加权限控制和日志记录功能:
public class ProxyDemo {
public static void main(String[] args) {
File file = new FileProxy("sensitiveFile.txt", true);
// 用户 admin 尝试读取敏感文件,应该成功
file.read();
// 普通用户尝试读取敏感文件,应该被拒绝访问
File file2 = new FileProxy("sensitiveFile.txt", false);
file2.read();
}
}
我们通过代理类 FileProxy
访问文件,FileProxy
在文件访问时增加了权限控制功能(只有 admin 用户具有读取权限)和日志记录功能。当 admin 用户尝试读取敏感文件时,代理类允许访问,并记录了文件访问的日志;而普通用户尝试读取敏感文件时,代理类拒绝访问,并记录了文件访问的日志。(请注意,在实际应用中,权限的判断应该根据具体的需求和用户系统进行,上述代码中的权限判断只是一个简单的示例。)
通过这种方式,我们实现了代理模式的特性,即控制访问和增加额外功能。代理类在访问真实对象之前加入了权限控制功能,并在访问真实对象之后增加了日志记录功能,实现了对文件访问的控制和管理。这样的代理模式适用于需要在访问对象时增加额外功能、控制访问权限或频率的情况,增强了系统的安全性和可维护性。