一、引言
你有没有想过,代码就像是一座繁忙的城市,而设计模式则是这座城市的隐形建筑师?它们静静地存在于代码的深处,塑造着架构的每一个细节。代理、工厂、外观、适配器、单例——这些听起来似乎很复杂的词汇,其实就像是城市的桥梁、隧道、广场和建筑物,它们让代码的运行变得顺畅、可靠、有条不紊。在这篇文章中,我将带你穿梭于这座“代码城市”,一同探索这些设计模式如何在不知不觉中改变着我们的开发方式。准备好揭开这些秘密了吗?让我们一起踏上这趟惊奇之旅吧!
二、单例模式
关于单例模式我在这篇博文就不过多解释了,不是很清楚的可以看看我之前的博文《JavaEE中的单例模式:饿汉与懒汉的优雅实现》,在这篇博文中讲的很清楚。我在这里就简单的提一嘴,饿汉模式和懒汉模式的实现,以及如何处理懒汉模式的线程不安全问题。
三、工厂模式
工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,而不是通过直接调用构造函数来实例化对象。在工厂模式中,我们将对象的创建过程封装在一个工厂类中,并通过该工厂类来生成对象。
工厂模式的主要优点是将对象的创建与使用分离,使得代码更容易扩展和维护。工厂模式有几个常见的变种,包括简单工厂模式、工厂方法模式和抽象工厂模式。
3.1、 简单工厂模式
简单工厂模式是一种不属于GoF 23种设计模式的模式,但它是工厂模式的基础。简单工厂模式通过一个专门的工厂类来创建对象,根据不同的参数返回不同的实例。
示例代码:
假设我们有一个产品 Product
,它有两个具体的实现类 ProductA
和 ProductB
。
// 抽象产品类
abstract class Product {
public abstract void use();
}
// 具体产品A
public class ProductA extends Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
// 具体产品B
public class ProductB extends Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
// 工厂类
public class SimpleFactory {
public static Product createProduct(String type) {
if (type.equals("A")) {
return new ProductA();
} else if (type.equals("B")) {
return new ProductB();
} else {
throw new IllegalArgumentException("未知的产品类型:" + type);
}
}
}
使用方法如下:
public class Main {
public static void main(String[] args) {
// 使用工厂创建产品
Product productA = SimpleFactory.createProduct("A");
productA.use(); // 输出:使用产品A
Product productB = SimpleFactory.createProduct("B");
productB.use(); // 输出:使用产品B
}
}
3.2、 工厂方法模式
工厂方法模式将对象的创建过程延迟到子类中。它提供了一个抽象工厂类,并定义了一个抽象方法 createProduct()
,由具体的子类来实现该方法。
示例代码:
// 抽象产品类
abstract class Product {
public abstract void use();
}
// 具体产品A
public class ProductA extends Product {
@Override
public void use() {
System.out.println("使用产品A");
}
}
// 具体产品B
public class ProductB extends Product {
@Override
public void use() {
System.out.println("使用产品B");
}
}
// 抽象工厂类
abstract class Factory {
public abstract Product createProduct();
}
// 具体工厂A
public class FactoryA extends Factory {
@Override
public Product createProduct() {
return new ProductA();
}
}
// 具体工厂B
public class FactoryB extends Factory {
@Override
public Product createProduct() {
return new ProductB();
}
}
使用方式:
public class Main {
public static void main(String[] args) {
Factory factoryA = new FactoryA();
Product productA = factoryA.createProduct();
productA.use(); // 输出:使用产品A
Factory factoryB = new FactoryB();
Product productB = factoryB.createProduct();
productB.use(); // 输出:使用产品B
}
}
3.3、工厂模式的简单总结
- 简单工厂模式:由一个工厂类根据传入的参数来决定创建哪种类型的对象。
- 工厂方法模式:通过定义一个抽象工厂类,将对象的创建延迟到具体的工厂子类中,每个子类负责创建一种特定类型的对象。
通过上面这些代码,简单来说工厂模式就是在工厂类中事先把一些需要用的东西给写好,将对象的创建过程封装起来,然后在程序猿需要的时候直接用就行,不需要关心具体的创建细节。
四、外观模式 / 门面模式
门面模式是一种结构型设计模式,它提供了一个统一的接口,用来访问子系统中的一群接口。门面模式定义了一个高层接口,使得子系统更容易使用。通过门面模式,客户端可以更简单地与复杂的子系统进行交互,隐藏了系统的复杂性,简化了接口。
假设哈,我们有一个家庭影院系统,包括电视、音响、DVD播放器和灯光控制等设备。要开启家庭影院,可能需要分别打开每个设备,并设置好相应的状态,这对于用户来说比较复杂。通过门面模式,我们可以为用户提供一个简单的接口,比如 HomeTheaterFacade
,用户只需调用一个方法,就可以轻松开启整个家庭影院。
子系统代码实现如下:
// 电视
class TV {
public void on() {
System.out.println("电视已打开");
}
public void off() {
System.out.println("电视已关闭");
}
}
// 音响
class SoundSystem {
public void on() {
System.out.println("音响已打开");
}
public void off() {
System.out.println("音响已关闭");
}
public void setVolume(int level) {
System.out.println("音响音量已设置为 " + level);
}
}
// DVD播放器
class DVDPlayer {
public void on() {
System.out.println("DVD播放器已打开");
}
public void off() {
System.out.println("DVD播放器已关闭");
}
public void play(String movie) {
System.out.println("正在播放电影: " + movie);
}
}
// 灯光
class Lights {
public void dim(int level) {
System.out.println("灯光已调暗到 " + level + "%");
}
public void on() {
System.out.println("灯光已打开");
}
}
门面类代码实现如下:
class HomeTheaterFacade {
private TV tv;
private SoundSystem soundSystem;
private DVDPlayer dvdPlayer;
private Lights lights;
public HomeTheaterFacade(TV tv, SoundSystem soundSystem, DVDPlayer dvdPlayer, Lights lights) {
this.tv = tv;
this.soundSystem = soundSystem;
this.dvdPlayer = dvdPlayer;
this.lights = lights;
}
public void watchMovie(String movie) {
System.out.println("准备开始观看电影...");
lights.dim(10); // 调暗灯光
tv.on(); // 打开电视
soundSystem.on(); // 打开音响
soundSystem.setVolume(5); // 设置音量
dvdPlayer.on(); // 打开DVD播放器
dvdPlayer.play(movie); // 播放电影
}
public void endMovie() {
System.out.println("关闭家庭影院...");
lights.on(); // 打开灯光
tv.off(); // 关闭电视
soundSystem.off(); // 关闭音响
dvdPlayer.off(); // 关闭DVD播放器
}
}
创建好这些之后,我们就可以使用这些了,用户代码如下:
public class Main {
public static void main(String[] args) {
// 首先得拥有这些设备
TV tv = new TV();
SoundSystem soundSystem = new SoundSystem();
DVDPlayer dvdPlayer = new DVDPlayer();
Lights lights = new Lights();
// 创建门面对象
HomeTheaterFacade homeTheater = new HomeTheaterFacade(tv, soundSystem, dvdPlayer, lights);
// 观看电影
homeTheater.watchMovie("《喜羊羊与变形金刚》");
// 结束电影
homeTheater.endMovie();
}
}
简单来说,如果不是门面模式,这些设备就像是一个一个的零散的开关,我要一个一个开一个一个关,而这个门面模式相当于是个总开关,一键开启与关闭。
五、适配器模式
这个模式当时真的是困扰了我很久,感觉很难理解,但是只要想通了也就那样,举个例子,假设你现在是中国的插头、充电线、手机,但是你现在到了美国,手机没电,这下完了,因为国外插座的电压和国内的充电器不适配,然后我们就需要一个转接头进行适配,代码实现的话如下:
// 中国充电器接口类
interface ChinaChargers {
void charge();
}
// 美国插座
public class UnitedStatesOutlets {
public void providePower() {
System.out.println("美国插座提供电源");
}
}
// 转换器类
public class Converter implements ChinaChargers{
// 先创建一个美国插座
private UnitedStatesOutlets unitedStatesOutlets;
public Converter(UnitedStatesOutlets unitedStatesOutlets) {
this.unitedStatesOutlets = unitedStatesOutlets;
}
// 开始转换
@Override
public void charge() {
unitedStatesOutlets.providePower();
System.out.println("通过适配器,使用美国插座给中国充电器充电...");
}
}
// 中国手机类,得使用充电器
public class ChinesePhone {
private ChinaChargers charger;
public ChinesePhone(ChinaChargers charger) {
this.charger = charger;
}
public void charge() {
charger.charge();
}
}
// 使用类
public class Main {
public static void main(String[] args) {
// 首先得有个美国插座
UnitedStatesOutlets unitedStatesOutlets = new UnitedStatesOutlets();
// 然后把美国插座丢给适配器适配
Converter converter = new Converter(unitedStatesOutlets);
// 适配好了之后直接使用充电器充电
ChinesePhone chinesePhone = new ChinesePhone(converter);
chinesePhone.charge();
}
}
这个示例代码展示了如何使用适配器模式,通过一个转换器(适配器)将不兼容的中国充电器与美国插座连接,使得充电器能够在不同的电源接口下正常工作。
六、代理模式
这个也很简单,举个例子,有个房东想卖房子,但是他不自己卖,他找了一个中介,这个中介就是代理对象,而这个房东的房子就是目标对象。
// 目标接口:房东
interface Landlord {
void rentHouse();
}
// 目标类:房东实现出租房子的操作
public class RealLandlord implements Landlord {
@Override
public void rentHouse() {
System.out.println("房东: 出租房子...");
}
}
// 代理类:中介
public class Agent implements Landlord {
private RealLandlord landlord;
public Agent(RealLandlord landlord) {
this.landlord = landlord;
}
@Override
public void rentHouse() {
System.out.println("中介: 在出租房子之前做一些准备工作,比如发布广告...");
landlord.rentHouse(); // 调用房东的实际操作
System.out.println("中介: 在出租房子之后处理一些后续工作,比如签合同...");
}
}
// 客户端类:租客
public class Client {
public static void main(String[] args) {
// 创建房东对象
RealLandlord landlord = new RealLandlord();
// 创建中介对象,代理房东的租房操作
Landlord agent = new Agent(landlord);
// 租客通过中介租房
agent.rentHouse();
}
}
这个示例展示了代理模式的一个典型应用场景:房东通过中介来租房。中介作为代理对象,控制了对房东的访问,并且在租房的过程中添加了一些额外的操作,比如广告宣传和合同签订。租客只需要和中介交互,不需要直接接触房东,从而实现了对房东操作的控制和扩展。
使用代理模式最大的优点就是可以控制对目标对象的访问并且在不修改源代码的情况下能增强功能,就比如我们 AOP 中的拦截器啊,统一返回格式这些都是基于代理模式实现的。
七、代理模式 VS 适配器模式
这个也是面试常考题,用简单的大白话来说:
代理模式:
代理模式就好像是电脑上的某个音频软件,比如XX音乐这些就相当于一个代理类,你需要某个音频的时候直接搜,然后这个软件就会帮你找到。并且可以在执行任务的过程中添加额外的操作,比如缓存、权限控制等。
适配器模式:
适配器模式更像是一个万能的音乐播放器,不管什么格式的音频,只需要通过这个播放器,他就能自己进行格式转换然后播放出来。
八、总结
在本篇博文中,我们深入探讨了代理模式、工厂模式、外观模式、适配器模式以及单例模式这五种常见的设计模式。通过详细的分析和实际示例,我们了解到每种模式的独特之处及其在软件开发中的应用场景。代理模式帮助我们在不改变对象接口的情况下提供额外的功能,工厂模式则为我们创建对象提供了一种灵活的方式。外观模式通过简化复杂系统的接口,提升了系统的可用性;适配器模式则让不兼容的接口能够协同工作。单例模式确保了一个类只有一个实例,适用于需要全局唯一对象的场景。通过对这些设计模式的理解与运用,我们能够编写出更具可维护性、扩展性和复用性的代码,为构建高质量的软件系统打下坚实基础。
九、结语
软件开发的世界瞬息万变,但设计模式的价值却始终如一。掌握并灵活应用设计模式不仅能够提升我们的编程能力,还能让我们在复杂的系统设计中游刃有余。就像一位优秀的建筑师拥有多种工具和设计理念,软件开发者同样需要掌握这些设计模式,以应对不同的开发挑战。未来的道路或许充满未知,但只要我们不断学习、不断进步,就一定能在软件开发的旅程中创造出更为辉煌的篇章。相信自己,勇敢迈出每一步,未来必将因我们的努力而更加精彩!