这七种设计模式,学不会来打我!

前言

设计模式就像是编程界的“宝藏”,是一些经过验证和总结的最佳实践,可以帮助我们更好地组织代码、解决问题,并提高代码的可读性、可重用性和可维护性。而这些“宝藏”是由一些很厉害的程序员们历经多年的开发实践和经验总结得来的。他们在面对各种问题时,都会思考如何使用最有效的方式来解决,同时也在这个过程中不断总结优秀的编程思想和方法论,从而形成了各种设计模式。

当然,这些设计模式并不是银弹,不能解决所有问题,只有在特定的场景下才能发挥出它们的优势。所以,作为程序员,我们需要根据具体情况选择合适的模式,并将其灵活运用,才能真正发挥设计模式的作用。

工厂模式

假设你是一个巧克力工厂的老板,你需要生产不同口味的巧克力。你可以选择自己亲自动手制作每个口味的巧克力,但这样你既辛苦又效率低下。那么,该怎么办呢?

这时候,你可以雇佣一些巧克力师傅(具体工厂类),让他们专门负责制作不同口味的巧克力(具体产品类)。而你只需要提供原材料(工厂方法)和制作巧克力的流程(抽象父类),就可以让巧克力师傅们按照你的要求制作出口感和口味都完美的巧克力。

这样,当你需要新增一种口味的巧克力时,只需要再雇佣一个会制作这种口味的巧克力师傅,并提供对应的原材料和制作流程即可。这样,你就无需亲自动手,也能够轻松地扩展巧克力的种类了。

总之,工厂模式就像是一个巧克力工厂,它为我们提供了一种方便、高效、可扩展的巧克力生产方式,让我们能够轻松制作出各种口味的美味巧克力。

首先,我们需要定义一个抽象的巧克力类 Chocolate,它有一个制作巧克力的方法 make()。

public abstract class Chocolate {
    public abstract void make();
}

然后,我们定义具体的巧克力类,比如牛奶巧克力(MilkChocolate)和黑巧克力(DarkChocolate),它们继承自 Chocolate 类,并实现了 make() 方法。

public class MilkChocolate extends Chocolate {
    @Override
    public void make() {
        System.out.println("制作牛奶巧克力");
    }
}

public class DarkChocolate extends Chocolate {
    @Override
    public void make() {
        System.out.println("制作黑巧克力");
    }
}

接下来,我们定义一个抽象的巧克力工厂类 ChocolateFactory,它有一个制作巧克力的抽象工厂方法 makeChocolate()。

public abstract class ChocolateFactory {
    public abstract Chocolate makeChocolate();
}

然后,我们可以定义具体的巧克力工厂类,比如牛奶巧克力工厂(MilkChocolateFactory)和黑巧克力工厂(DarkChocolateFactory),它们都继承自 ChocolateFactory 类,并实现了 makeChocolate() 方法。这样,每个具体工厂就可以生产对应种类的巧克力。

public class MilkChocolateFactory extends ChocolateFactory {
    @Override
    public Chocolate makeChocolate() {
        return new MilkChocolate();
    }
}

public class DarkChocolateFactory extends ChocolateFactory {
    @Override
    public Chocolate makeChocolate() {
        return new DarkChocolate();
    }
}

7

最后,我们就可以在客户端使用巧克力工厂来生产不同种类的巧克力了。

public class Client {
    public static void main(String[] args) {
        // 创建牛奶巧克力工厂
        ChocolateFactory milkFactory = new MilkChocolateFactory();
        // 生产牛奶巧克力
        Chocolate milkChocolate = milkFactory.makeChocolate();
        // 制作巧克力
        milkChocolate.make();

        // 创建黑巧克力工厂
        ChocolateFactory darkFactory = new DarkChocolateFactory();
        // 生产黑巧克力
        Chocolate darkChocolate = darkFactory.makeChocolate();
        // 制作巧克力
        darkChocolate.make();
    }
}

以上就是工厂模式的一个简单示例代码,通过抽象父类、具体子类和抽象工厂类、具体工厂类的组合关系,实现了一种高效、可扩展、易于维护的对象创建方式。

这个时候可能有小伙伴问:

然并卵?我怎么没看出来这有啥用啊,而且好像我们日常开发就是这样啊?

💡

没错!

工厂模式是一种常用的软件设计模式,在项目开发中被广泛应用。工厂模式的主要思想是将对象的创建过程封装起来,以使得程序更加灵活和易于维护。

在项目中使用工厂模式可以带来以下几个优点:

降低耦合度:通过工厂模式,客户端代码不需要知道具体的产品类,只需要知道它们的抽象接口即可。这样可以降低客户端与具体产品之间的依赖关系,提高系统的可扩展性和可维护性。

提高代码复用性:工厂模式通过对对象创建的过程进行封装,可以让多个客户端共享同一个对象实例。这样可以避免重复创建对象,提高系统的性能,并且可以提高代码的复用性。

简化对象的创建过程:由于工厂模式将对象的创建过程封装起来,客户端代码无需关心对象的创建过程,从而简化了代码的编写过程。

因此,工厂模式已经成为项目开发中的常用设计模式之一,应用广泛。无论是在面向对象编程还是函数式编程中,都可以看到工厂模式的应用。

作者👨

其实我们可以把工厂模式与Java中的封装思想进行联合记忆,与Java封装数据于类中不同的是它将对象的创建过程封装在一个工厂类中。客户端只需通过工厂类来获取所需的对象实例,而无需了解对象的具体创建过程和内部实现。这样就实现了对对象创建过程的封装,同时提高了代码的可读性和可维护性。

因此,封装和工厂模式都是对实现细节进行封装的策略,它们的目的都是提高代码复用性、简化调用者的使用过程,同时避免直接暴露实现细节带来的安全问题。虽然封装和工厂模式的实现机制不同,但它们的目标是一致的,都是为了提高代码质量和开发效率。

单例模式

单例模式就像是一个特别抠门的大老板,它只让你在整个公司里创建一个实例,其他地方都不行。这样做有什么好处呢?首先,它能够保证在整个系统中只有一个特定类型的对象,这样能够避免重复创建对象造成的内存浪费和性能问题。其次,因为单例模式只允许创建一个实例,所以不同的代码段获取到的都是同一个实例,这样能够确保数据的一致性和正确性,方便我们进行资源共享和数据交互。

如果你还不理解,那么可以想象一下自己是一个卖糖葫芦的小贩,而单例就像是你手里的糖葫芦篮子,你始终只有一个篮子,所有的顾客都从这个篮子里拿糖葫芦。这样可以确保所有的顾客都买到了同样的糖葫芦,并且你也用最少的资源去完成了生意。

当然,这只是一个简单的比喻,单例模式的具体实现还需要考虑线程安全等问题,但是相信通过这个比喻,您已经初步了解了单例模式的概念和作用

单例模式可以保证一个类只有唯一的实例,并提供全局访问点。

在单例模式中,通常会将构造函数声明为私有方法,以防止直接创建类的实例。同时,该类还会提供一个静态方法(通常称之为getInstance()),以便调用者能够获取类的唯一实例。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 私有构造函数
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在上面的代码中,Singleton是一个单例类,它包含一个私有构造函数和一个公共静态方法getInstance()。当第一次调用getInstance()时,会创建Singleton类的实例并将其存储在instance变量中,随后的调用都将返回同一个实例。这样,就可以确保Singleton类的所有对象均为同一实例,并且可以通过getInstance()方法进行全局访问。

💡

单例模式的主要作用是确保一个类只有唯一的实例,并提供全局访问点,使得这个实例可以被整个系统中的其他对象所共享和访问。

在某些情况下,如果我们需要确保某个类的实例只有一个,例如配置信息类、日志记录器类等,那么使用单例模式就非常适合。由于单例模式可以保证某个类的实例永远只有一个,因此可以避免不必要的内存分配和资源消耗,提高系统的性能和效率。

另外,使用单例模式还可以提高代码的可维护性和可扩展性。如果在未来需要对该类进行修改或拓展,只需修改该类的代码即可,无需修改系统中大量的代码,减少了维护成本。

总而言之,单例模式在软件系统中具有广泛的应用,可以提高系统的性能、效率、可维护性和可扩展性

作者👨

记忆理解单例模式可以从以下几个方面考虑:

  1. 唯一性:单例模式的主要特点是确保某个类只有唯一的实例。
  2. 全局访问:单例模式提供了全局访问点,使得该实例可以被整个系统中的其他对象所共享和访问。
  3. 私有构造函数:单例模式通常将构造函数声明为私有方法,以防止直接创建类的实例。

我们在日常开发中,确实会经常使用到单例模式。例如配置信息类、日志记录器类等,它们都需要确保只有一个实例存在,并且可以被整个系统所共享和访问。在这些情况下,使用单例模式就非常适合。

对于配置类来说,我们可以通过单例模式实现,将配置文件中的数据读取到内存中,并保存在一个唯一的实例中,在系统中需要访问配置数据时,直接通过该实例进行访问即可。这样可以避免重复读取配置文件,提高系统性能和效率。

综上所述,单例模式是一个非常常用且实用的设计模式,在我们的日常开发中也经常会用到。通过理解其核心思想和应用场景,我们可以更好地掌握和运用该模式。

那么为什么我们经常编写的配置类信息中 并没有体现出单例模式呢?

这是因为这个配置类所返回的实例是由Spring容器管理的,而Spring默认情况下会将它们的作用范围设置为“单例”(singleton),也就是说,在整个应用程序中只会创建一次这些实例,并且可以被所有需要它们的对象所共享和访问。

因此,虽然代码中并没有显式地使用单例模式,但是在Spring容器的管理下,这些实例确实表现出了单例模式的特点。

观察者模式

假设你是某个小区的物业管理员,需要随时知道小区里居民的动态,比如他们搬进、搬出、有什么新的需求等。但是,如果每次都要一个一个去问居民,那工作量也太大了吧!

这时候,你可以使用观察者模式,将自己定义成观察者,居民们定义成被观察者。当有人搬进、搬出、或者有新的需求时,居民们就会发出通知,这些通知就相当于观察者模式中的事件。

而观察者(也就是你)则会及时接收到这些事件,并进行相应的处理,比如派人去帮忙搬家,安排维修人员修理问题等等。

这样一来,你就不用一个一个去问居民的情况了,只需要等待居民们主动通知你就好了,工作效率也能提高不少呢!

好的,下面是一个简单的 Java 代码演示观察者模式:

首先,我们需要定义两个角色:被观察者和观察者。被观察者需要维护一个观察者列表,当自己状态发生改变时,需要通知所有观察者。观察者则需要实现一个接收通知的方法。

import java.util.ArrayList;
import java.util.List;

interface Subject { // 被观察者接口
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

class ConcreteSubject implements Subject { // 具体被观察者
    private List<Observer> observers = new ArrayList<>();
    private String state;

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(this);
        }
    }

    public void setState(String state) {
        this.state = state;
        notifyObservers();
    }

    public String getState() {
        return state;
    }
}

interface Observer { // 观察者接口
    void update(Subject subject);
}

class ConcreteObserver implements Observer { // 具体观察者
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    public void update(Subject subject) {
        System.out.println(name + " 收到了消息: " + ((ConcreteSubject)subject).getState());
    }
}

在被观察者中,我们定义了 attachdetachnotifyObservers 三个方法,分别用于添加观察者、移除观察者以及通知观察者。当被观察者状态发生改变时,会调用 notifyObservers 方法,通知所有观察者。

然后,在观察者中,我们定义了一个 update 方法,用于接收被观察者的通知,并做出相应的处理。

最后,我们可以定义具体的被观察者和观察者类,来实现我们需要的功能。

public class ObserverPatternDemo {
    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        ConcreteObserver observer1 = new ConcreteObserver("张三");
        ConcreteObserver observer2 = new ConcreteObserver("李四");
        subject.attach(observer1);
        subject.attach(observer2);
        subject.setState("状态1");
        subject.detach(observer1);
        subject.setState("状态2");
    }
}

在这个例子中,我们定义了一个具体的被观察者 ConcreteSubject 和一个具体的观察者 ConcreteObserver。当状态发生改变时,观察者会接收到通知,并输出相应的提示信息。同时,在这个例子中,我们还演示了如何添加和移除观察者的过程。

💡

在实现观察者模式时,需要注意以下几个问题:

观察者列表的线程安全性:如果有多个线程同时进行添加、删除观察者操作,那么需要考虑同步和线程安全问题。

观察者的执行顺序:当被观察者状态发生改变时,通知观察者的顺序可能会影响结果,请在实现时注意这一点。

如何记住观察者模式呢?可以从它的名字入手,观察者就是观察某个对象的变化并做出相应反应的人或者物体。观察者模式就是让我们将这个场景抽象出来,并用代码实现。你可以想象自己在看电视,而电视机上的遥控器就是一个观察者,当你按下遥控器上的按钮时,电视机就会发生改变,这时遥控器就会接收到通知并作出相应的反应——比如调节音量、切换频道等等。

作者👨

在实际项目中,观察者模式运用还是比较多的。举个例子,可能你们公司的 HR 部门需要经常了解员工的情况,包括调动、离职、晋升等等。如果每次都要手动去询问员工的情况,那效率太低了。这时候,HR 部门可以作为观察者,员工们则是被观察者,当员工状态发生改变时,HR 部门就能够及时接收到通知并做出相应的处理。

再举一个例子,在游戏开发中,可能需要实现多个游戏对象之间的协作。比如,当玩家攻击敌人时,敌人需要受到伤害并可能死亡,同时玩家也会获得一定的分数、金币等奖励。如果每次都要手动去实现这些逻辑,代码量很大,而且也不够灵活。这时候,我们可以将玩家和敌人作为被观察者,将计分板、掉落物品等作为观察者,当玩家攻击敌人时,被观察者就会发出通知,所有的观察者就能够及时接收到通知并做出相应的处理,从而简化代码,并提高了可扩展性。

装饰者模式

假设你是一家咖啡店的老板,你想要给顾客提供各种不同口味的咖啡。但是你又不想要过多的商品,因为对于每个口味都建立一个品类实在太繁琐了。

这时候,你可以考虑使用装饰者模式。它能够动态地为对象添加新功能,而无需修改原有代码。这样你就可以只维护几种基本的咖啡类型了,然后通过添加装饰器来实现各种不同的口味。

具体来说,我们可以定义一个基础的咖啡对象,并通过继承和组合的方式,来动态地添加各种配料(比如牛奶、糖、巧克力等)。

首先,我们定义一个 Coffee 接口:

public interface Coffee {
    double getCost();
    String getDescription();
}

其中 getCost() 方法用于获取咖啡的价格,getDescription() 方法用于获取咖啡的描述信息。

然后,我们定义一个基础的咖啡实现类 SimpleCoffee:

public class SimpleCoffee implements Coffee {
    public double getCost() {
        return 1;
    }

    public String getDescription() {
        return "Simple Coffee";
    }
}

接下来,我们定义一个抽象类 CoffeeDecorator,它同时实现了 Coffee 接口:

public abstract class CoffeeDecorator implements Coffee {
    private final Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    public double getCost() {
        return decoratedCoffee.getCost();
    }

    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

注意到这个抽象类里面包含了一个私有成员变量 decoratedCoffee,它的类型是 Coffee。这个变量用于保存被装饰的 Coffee 对象。

然后,我们可以通过继承这个抽象类实现各种具体的装饰器。比如,牛奶、糖和巧克力:

public class Milk extends CoffeeDecorator {
    public Milk(Coffee coffee) {
        super(coffee);
    }

    public double getCost() {
        return super.getCost() + 0.5;
    }

    public String getDescription() {
        return super.getDescription() + ", Milk";
    }
}

public class Sugar extends CoffeeDecorator {
    public Sugar(Coffee coffee) {
        super(coffee);
    }

    public double getCost() {
        return super.getCost() + 0.2;
    }

    public String getDescription() {
        return super.getDescription() + ", Sugar";
    }
}

public class Chocolate extends CoffeeDecorator {
    public Chocolate(Coffee coffee) {
        super(coffee);
    }

    public double getCost() {
        return super.getCost() + 0.3;
    }

    public String getDescription() {
        return super.getDescription() + ", Chocolate";
    }
}

最后,我们可以用以下方式来使用装饰器模式:

Coffee coffee = new SimpleCoffee();
coffee = new Milk(coffee);
coffee = new Sugar(coffee);
coffee = new Chocolate(coffee);
System.out.println("Coffee Description: " + coffee.getDescription());
System.out.println("Coffee Cost: " + coffee.getCost());

在这个例子中,我们首先创建了一个基础的咖啡对象 SimpleCoffee,然后通过添加 Milk、Sugar 和 Chocolate 装饰器,来生成一个具有多种口味的咖啡对象。通过调用咖啡对象的 getDescription() 和 getCost() 方法,我们可以获取到咖啡的描述和价格。

💡

对象的初始状态:被装饰的对象在创建时应该处于一个完整的、可用的状态。否则,在添加装饰器时可能会出现意想不到的问题。

装饰器与被装饰对象的接口一致:装饰器必须实现与被装饰对象相同的接口,这样才能保证装饰器可以替换原始对象,并且客户端代码不需要做任何改变。

装饰器之间的顺序:如果有多个装饰器,其添加顺序可能会影响结果。一般来说,先添加的装饰器会先起作用,后添加的装饰器会依次叠加。因此,在设计时要仔细考虑装饰器的顺序。

避免过度装饰:如果添加过多的装饰器,可能会导致代码变得混乱和难以维护。因此,在设计时要注意避免对对象进行过度装饰。

作者👨

关于装饰者模式的应用场景,它的应用范围非常广泛,特别是在需要动态地增加或删除对象功能的情况下。例如:

Java 中的 IO 类库:Java 的 IO 类库就大量使用了装饰者模式。比如,我们可以通过添加缓冲、压缩、加密等装饰器来实现不同的文件操作功能。

GUI 编程中的组件:在 GUI 编程中,经常需要动态地扩展或改变组件的显示和行为。这时候就可以使用装饰者模式来实现。

Web 开发中的过滤器:Web 应用程序中,经常需要过滤、验证、转换输入或输出数据。这时候就可以使用装饰者模式来实现。

日志记录:在日志记录中,可以使用装饰者模式来增加或删除记录方式,比如记录到文件、控制台或数据库等。

因此,装饰者模式在日常编程中应用非常广泛,是一个很实用的设计模式。

策略模式

想象一下,你是个餐厅老板,菜单上有各种各样的菜式。但是你发现有些客人可能对辣椒比较敏感,有些客人可能喜欢口味偏甜的菜式,而有些客人则喜欢咸味的菜式。

这时候,如果你为每个客人都准备一份特别的菜单,那工作量肯定会很大,也不太实际。所以,你可以使用策略模式。

策略模式就像是一个菜单中的不同选项,你可以根据客人的口味选择不同的策略来提供菜式。例如,如果客人喜欢辣味,你可以选择辣椒酱作为调料;如果客人喜欢甜味,你可以加入糖或蜜糖,如果客人喜欢咸味,你可以多加一点盐等等。

在程序设计中,策略模式就是定义一系列算法,将它们分别封装起来,并且使它们可以相互替换。这样,在运行时,你可以根据需要选择不同的算法。就像你可以根据客人的口味选择不同的调料一样。

首先,我们定义一个接口 Strategy,用于声明支持的所有算法:

public interface Strategy {
    int calculate(int a, int b);
}

接下来,我们定义具体的算法类,实现 Strategy 接口:

public class AddStrategy implements Strategy {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
}

public class SubStrategy implements Strategy {
    @Override
    public int calculate(int a, int b) {
        return a - b;
    }
}

public class MulStrategy implements Strategy {
    @Override
    public int calculate(int a, int b) {
        return a * b;
    }
}

然后,我们定义一个上下文对象 Calculator,它持有一个 Strategy 对象引用,并提供一个 setStrategy() 方法,用于切换算法:

public class Calculator {
    private Strategy strategy;

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public int calculate(int a, int b) {
        return strategy.calculate(a, b);
    }
}

最后,我们可以在客户端代码中使用这些类进行计算:

Calculator calculator = new Calculator();

// 使用加法算法
calculator.setStrategy(new AddStrategy());
int result = calculator.calculate(10, 5);  // 输出 15

// 使用减法算法
calculator.setStrategy(new SubStrategy());
result = calculator.calculate(10, 5);  // 输出 5

// 使用乘法算法
calculator.setStrategy(new MulStrategy());
result = calculator.calculate(10, 5);  // 输出 50

在上面的代码中,我们通过 setStrategy() 方法来切换不同的算法,并且可以动态地修改计算器的行为。这就是策略模式的核心思想。

💡

策略模式是一种行为设计模式,它将一组算法封装在独立的类中,并使它们之间可以相互替换。以下是在实际项目中使用策略模式时需要注意的几点:

将变化隔离:策略模式的一个主要特点是将算法的实现与客户端代码分离,使得算法的实现可以更容易地修改或扩展。因此,在选择一个算法实现时,需要考虑其可能需要被修改或替换的程度。

统一接口:为了达到不同算法之间相互替换的目的,所有算法必须实现相同的接口。这意味着在添加新算法时,需要确保新算法实现了该接口。

策略选择:在运行时动态选择算法的过程中,需要根据具体情况进行权衡和选择。这可能涉及到性能、可靠性、复杂度等方面的因素。因此,在选择算法时,需要综合考虑各种因素,并作出最优的选择。

可读性:由于策略模式可能涉及到多个类的交互,因此代码的可读性尤为重要。为了提高代码的可读性,需要采用清晰的命名约定、注释和代码组织方式。

作者👨

策略模式在实际项目中的应用场景非常广泛,以下是一些常见的使用场景:

算法和规则:对于需要根据不同条件选择不同算法或规则的场景,策略模式可以提供一种简单而灵活的解决方案。

数据格式转换:在将数据从一种格式转换为另一种格式时,可能需要使用不同的转换器。策略模式可以使得这些转换器相互替换,从而提高系统的灵活性和可扩展性。

输入输出处理:当需要处理不同类型的输入或输出时,策略模式可以提供一种统一的接口,并帮助我们选择最适合的输入或输出方式。

订单处理:在订单处理系统中,根据订单状态、支付方式、配送地址等因素选择不同的处理方式可能会非常复杂。策略模式可以使得这些处理方式相互独立,并更容易地修改或扩展。

游戏开发:在游戏开发中,可能需要使用不同的策略来控制游戏中的角色行为、AI 敌人行为、道具效果等方面。策略模式可以使得这些策略相互替换,从而提高游戏的可玩性和可扩展性。

策略模式可以理解为一个策略选择器。

在实际应用中,我们需要根据不同的情况选择不同的算法或规则来达到最优的效果。这个过程类似于在一个策略选择器中选择不同的策略来应对不同的问题。

具体地说,策略模式把一组算法封装在独立的类中,并使它们之间可以相互替换。这样,当需要使用某种算法时,只需要从策略选择器中选择相应的算法即可,而不必关心算法的具体实现细节。

迭代器模式

迭代器模式就像是一个超市里的购物车。你可以通过购物车把你想要买的商品放进去,然后遍历一遍购物车里面的商品,逐个结算。在这个过程中,你不需要关心购物车里面的商品具体是哪些,只需要按照顺序逐个处理即可。而迭代器模式的实现就是让用户无需知道集合内部的结构,就可以逐个处理集合中的元素。

迭代器模式是一种设计模式,它允许我们访问一个容器中的所有元素,而不需要暴露容器的内部实现细节。在Java中,迭代器模式通常使用Iterator 接口来实现。

Iterator接口包含三个主要方法:hasNext()、next() 和remove()。其中,hasNext() 方法用于判断是否还有下一个元素, next() 方法用于获取下一个元素, remove() 方法用于从容器中删除当前元素(可选操作)。

import java.util.ArrayList;
import java.util.Iterator;

public class IteratorPatternDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Orange");

        // 获取迭代器对象
        Iterator<String> iterator = list.iterator();

        // 遍历容器中的元素
        while (iterator.hasNext()) {
            String fruit = iterator.next();
            System.out.println(fruit);
        }
    }
}

在上面的代码中,我们首先创建了一个ArrayList容器,并向其中添加了几个字符串元素。然后,通过调用list的iterator()方法,获得了一个迭代器对象。接着,我们使用while循环遍历了整个容器,并在循环体内通过iterator的next()方法获取了容器中的每一个元素并打印出来。

这就是迭代器模式在Java中的应用。通过使用迭代器,我们可以方便地遍历容器中的元素,并且不需要了解容器的内部实现细节。

关于迭代器对象

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    //...
}

在Java中,List、Set和Map等集合类都实现了 Iterable 接口。这意味着,可以通过调用它们的 iterator() 方法来获取一个迭代器对象。

该方法返回一个内部类 Itr 的实例,而该类则实现了 Iterator 接口。由于 Itr 类是ArrayList的内部类,因此它可以直接访问ArrayList的成员变量。

类似地,在 HashSet 和 HashMap 中都有一个内部类 HashIterator,该类同样实现了 Iterator 接口,用于遍历其中的元素。

因此,当我们调用 list.iterator() 时,实际上是获取到了一个内部类 Itr 的实例,该实例封装了对 ArrayList 内部结构的访问方式,并提供了 hasNext()、next() 和 remove() 等方法,以支持遍历 ArrayList 中的元素。

💡

在使用迭代器模式时,需要注意以下几点:

  1. 确保容器的数据结构稳定:由于迭代器是基于容器实现的,因此在使用迭代器时要确保容器的数据结构稳定。如果容器的结构发生了变化(如添加、删除元素),则可能会导致迭代器失效或不正确。
  2. 不要直接修改容器中的元素:虽然迭代器可以通过remove()方法移除当前位置的元素,但不建议直接修改容器中的元素。如果需要修改容器中的元素,最好先将其复制到一个临时变量中进行修改,避免对迭代器造成影响。
  3. 不要同时使用多个迭代器:由于迭代器是基于容器实现的,因此在使用迭代器时不建议同时使用多个迭代器来遍历同一个容器。这可能会导致某些元素被漏掉或重复遍历同一个元素。
  4. 注意异常处理:在使用迭代器时,需要注意异常处理。例如,在遍历容器时,如果已经到达容器的末尾,则 hasNext() 方法会返回 false,此时如果仍然调用 next() 方法,则会抛出 NoSuchElementException 异常。
  5. 实现自定义迭代器时,应该遵循迭代器接口的规范:如果需要实现自定义迭代器,则应该遵循迭代器接口的规范,包括实现 hasNext()、next() 和 remove() 三个方法,并且要根据具体的容器类型来实现这些方法。

作者👨

迭代器模式的主要使用场景是需要遍历容器(如列表、树等)中的元素,并且不想暴露容器的内部结构给客户端。迭代器模式可以将遍历操作和容器内部结构解耦,从而更加灵活地处理容器中的元素。

在实际应用中,迭代器模式常常与其他设计模式一起使用,例如工厂方法模式、观察者模式等。下面是一些典型的应用场景:

  1. 需要遍历一个容器中的元素,但不想暴露容器的内部实现细节给客户端。
  2. 需要对容器中的元素进行过滤、排序或其他操作。
  3. 需要同时遍历多个容器中的元素,然后将它们合并到一个容器中。

对于理解迭代器模式,我们可以把它想象成一个抽象的“遍历器”,它可以帮助我们逐个访问容器中的元素,而无需了解容器的具体实现方式。从这个角度上来说,迭代器模式可以看作是一种“遍历器”模式,它将遍历操作和容器的实现分离开来,从而更好地组织代码、提高代码复用性和可维护性。

模板方法模式

假设你要制作一份烤鸡翅的食谱,你需要准备好鸡翅、调料和烤箱等工具。但是,你可能不知道应该先涂什么调料、要烤多长时间、要加多少盐等等。这时候,你可以向专业厨师学习制作烤鸡翅的方法。

专业厨师会告诉你,制作烤鸡翅的步骤大致如下:

  1. 先将鸡翅洗净,再用纸巾擦干水分。
  2. 将鸡翅放入碗中,加入适量的盐、胡椒粉、孜然粉等调料,搅拌均匀。
  3. 在烤盘上铺上锡纸,将鸡翅摆放在烤盘上,放入预热好的烤箱中,烤20分钟。
  4. 取出鸡翅,反面翻转,再烤10分钟。

这就是一个简单的制作烤鸡翅的模板方法。每个步骤都是固定的,只是其中的具体细节可能有所不同。例如,不同的人可能会用不同的调料、烤箱温度也可能有所不同等等。

在软件开发中,模板方法模式也是类似的一种设计模式。它定义了一个算法的骨架,而将具体实现留给子类来完成。这样,在编写代码时就可以避免重复代码,并且可以灵活地修改算法的具体实现。

因此,我们可以将模板方法模式理解为一种“食谱”,其中定义了一个固定的算法流程,而将具体实现留给子类来完成。只要保证算法流程的正确性和稳定性,就可以在具体实现上进行灵活的调整和扩展。

abstract class AbstractClass {
    public void templateMethod() {
        // Step 1
        operation1();

        // Step 2
        operation2();

        // Step 3
        operation3();
    }

    protected abstract void operation1();

    protected abstract void operation2();

    protected void operation3() {
        System.out.println("AbstractClass.operation3()");
    }
}

class ConcreteClass extends AbstractClass {
    protected void operation1() {
        System.out.println("ConcreteClass.operation1()");
    }

    protected void operation2() {
        System.out.println("ConcreteClass.operation2()");
    }
}

public class TemplateMethodDemo {
    public static void main(String[] args) {
        AbstractClass obj = new ConcreteClass();
        obj.templateMethod();
    }
}

在上面的代码中,AbstractClass 是抽象类,其中定义了一个 templateMethod() 方法,该方法包含了算法的骨架。具体实现留给子类来完成。

ConcreteClass 则是具体子类,实现了 operation1() 和 operation2() 方法,并且继承了 operation3() 方法的默认实现。

在 main() 方法中,我们创建了一个 ConcreteClass 的对象,并调用其 templateMethod() 方法。由于 ConcreteClass 继承了 AbstractClass,因此可以使用 templateMethod() 方法的实现。

在程序运行时,输出的结果为:

ConcreteClass.operation1()
ConcreteClass.operation2()
AbstractClass.operation3()

这是因为在 templateMethod() 方法中先调用了 operation1() 和 operation2() 方法,而在默认实现中又调用了 operation3() 方法。

通过使用模板方法模式,我们可以将算法的骨架和具体实现分离开来,从而更好地组织代码并提高代码的可复用性。

💡

在使用模板方法模式时,需要注意以下几点:

  1. 确定好模板方法的算法流程:模板方法是整个模板方法模式的核心,因此在使用模板方法模式时,需要先确定好算法流程。通常,算法流程中会包含若干个抽象方法和一个具体方法。
  2. 抽象方法要求子类必须实现:在定义抽象方法时,需要注意这些方法必须由子类来实现,而不能在抽象类中直接实现。如果需要在抽象类中提供默认实现,可以使用具体方法来实现。
  3. 具体方法可有可无:在定义具体方法时,需要注意这些方法是可有可无的,并且不一定需要被子类覆盖。如果某个步骤不需要子类实现,则可以在具体方法中提供默认实现。
  4. 避免过度设计:在使用模板方法模式时,需要避免过度设计,保持算法流程简单明了,不要让算法流程变得过于复杂或难以理解。
  5. 注意抽象类和具体子类之间的耦合:由于抽象类与具体子类紧密相关,因此在使用模板方法模式时,需要注意抽象类和具体子类之间的耦合性。如果抽象类太过于复杂或难以维护,可能会影响具体子类的开发和维护。

总之,在使用模板方法模式时,需要注意确定好算法流程,并在其中合理地使用抽象方法和具体方法。同时,还需要注意抽象类和具体子类之间的耦合性,保持代码的简洁、灵活和易于维护。

作者👨

为了更好地记住模板方法模式,你可以将其看作是一种“模板”或者“骨架”,用于定义算法的流程。类似于烤鸡翅的食谱,模板方法定义了一个固定的步骤,而具体实现留给子类来完成。

在实际工作中,模板方法模式有很多应用场景:

  1. 操作系统的启动流程:操作系统的启动过程需要执行多个步骤,例如加载内核、初始化设备、启动服务等等。这些步骤的顺序和细节可能会因为不同的操作系统而有所不同,但整个启动流程的逻辑是相同的,可以使用模板方法模式来定义。
  2. HTTP请求的处理流程:在Web开发中,HTTP请求的处理流程也需要执行多个步骤,例如解析请求、调用控制器、渲染模板等等。这些步骤的具体实现可能因为不同的框架而有所不同,但处理请求的流程是相同的,可以使用模板方法模式来定义。
  3. 数据库访问的流程:数据库访问通常需要建立连接、发送查询语句、获取结果集等等。这些步骤的细节可能因为不同的数据库而有所不同,但整个访问流程的逻辑是相同的,可以使用模板方法模式来定义。

总的来说,模板方法模式适用于需要使用固定算法流程,但又希望在具体实现上保持灵活和可扩展性的场景。通过使用模板方法模式,我们可以避免重复代码,并且可以更好地组织代码,提高代码的可读性和可维护性。

总结

写在最后 看到这里你应该发现:没错设计模式是一个抽象的东西,因为它不过就是一些资深工作者所总结的经验;提供了一些通用的解决方案来应对各种软件设计问题、本质上是一些抽象的思想和原则,而不是具体的代码实现或技术细节。

相反 我们在实际工作中可能也是这么做的,只不过那个时候我们由于缺乏系统化的知识和理论指导,我们可能无法准确地描述出这些解决方案的本质和优缺点; 而看了本篇文章后你会发现 :噢~原来这个就叫单例模式、原来这个就是工厂模式啊。

所以真正的设计模式的学习其实重要的更多的是包含了一个程序员对代码的思想以及经验,如果你想好好的学习设计模式,最重要的其实是多学习一些知名开源项目、并阅读其中的源码,模仿这些知名程序员的架构。

相信我!这会给你带来收获的~

最后最后~

继续找实习中!!!(万一被大佬相中了呢)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值