设计模式:单例、原型和生成器

news2025/1/16 16:00:49

在这篇文章中,我们将重点介绍其余的创建模式:Singleton,Builder和Prototype。

在我看来,这些模式不如工厂重要。然而,了解它们仍然很有用。我将提供UML描述,简单的java示例(这样即使你不了解java,你也可以理解),并提出来自着名Java框架或API的真实示例。

文章目录

  • 创造模式
  • 单例模式
    • Java 实现
    • 何时需要使用单例?
    • 为什么不使用单例?
    • 使用单个实例而不是单一实例
    • 一些想法
    • 真实示例
  • 原型模式
    • 何时使用原型?
    • Java 实现
    • **真实示例**
  • 生成器模式
    • 它解决的问题
    • 一个简单的 java 示例
    • 正式定义
    • 真实示例
  • 结论

创造模式

创建模式是处理对象初始化并克服构造函数限制的设计模式。四人帮在他们的书“【设计模式:可重用面向对象软件的元素】中描述了其中的五个:

  • 单身人士,
  • 建筑工人,
  • 原型,
  • 抽象工厂,
  • 工厂模式。
    在这里插入图片描述

自本书出版(1994年)以来,已经发明了许多创造模式:

  • 其他类型的工厂(如静态工厂),
  • 池模式,
  • 惰性初始化,
  • 依赖注入,
  • 服务定位器,

在这篇文章中,我们只关注我还没有描述过的GoF的创造模式的其余部分。正如我在引言中所说,它们不如工厂重要,因为你可以没有它们(而工厂是许多应用程序和框架的支柱)。但是,它们很有用,与工厂不同,它们不会使代码更难阅读。

单例模式

这种模式是最著名的。在过去的几十年里,它被过度使用,但自那以后它的受欢迎程度有所下降。我个人避免使用它,因为它使代码更难以进行单元测试并创建紧密耦合。我更喜欢使用处理类的授权实例数的工厂(如Spring容器),我们将讨论这种方法。我认为你应该避免单例模式。事实上,这种模式最重要的用途是能够在面试官问“什么是单例?”时回答他。这种模式非常有争议,仍然有人赞成它。

话虽如此,根据GoF的说法,单例旨在:

“确保一个类只有一个实例,并提供对它的全局访问点”

因此,类是单例有 2 个要求:

  • 拥有唯一实例
  • 可从任何地方访问

有些人只考虑第一个要求(比如几年前的我)。在这种情况下,该类只是一个实例

让我们看一下如何在UML中执行单例

单例模式

在此 UML 图中,单例类有 3 项:

  • 类属性(实例):此属性包含单例类的唯一实例。
  • 一个类公共方法(getInstance()):它提供了获取类Singleton的唯一实例的唯一方法。可以从任何地方调用该方法,因为它是类方法(而不是实例方法)。
  • 私有构造函数(Singleton()):它阻止任何人使用构造函数实例化单例。

在此示例中,需要 Singleton 实例的开发人员将调用 Singleton.getInstance() 类方法。

单例类中的单例实例可以是:

  • 预初始化(这意味着在有人调用 getInstance()之前,它被实例化了)
  • lazy-initialized(这意味着它是在第一次调用getInstance()期间实例化的)

当然,真正的单例还有其他方法和属性来执行其业务逻辑。

Java 实现

这是使用预实例化方法在Java中创建单例的非常简单的方法。

public class SimpleSingleton {
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();
 
    private SimpleSingleton() { }
 
    public static SimpleSingleton getInstance(){
        return INSTANCE;
    }
}

使用这种方式,当类装入器装入类时,仅创建一次单一实例。如果代码中从未使用过该类,则不会实例化该实例(因为 JVM 的类装入器不会加载它),因此会浪费内存。但是,如果该类出现并且您不使用它(例如,如果它仅在非常非常罕见的条件下使用),则单例将无条件初始化。除非您的单例占用大量内存,否则应以这种方式使用。

不过,如果你只需要在真正使用时创建单例(惰性初始化),这里有一种方法可以在多线程环境中执行此操作。这部分有点棘手,因为它涉及线程一致性。

public class TouchySingleton {
    private static volatile TouchySingleton instance;
 
    private TouchySingleton() {
    }
 
    public static TouchySingleton getInstance() {
        if (instance == null) {
            synchronized (TouchySingleton.class) {
                if (instance == null) {
                    instance = new TouchySingleton();
                }
            }
        }
        return instance;
    }
}

正如我所说,它真的更难阅读(这就是为什么预先实例化的方式更好)。此单例涉及一个锁,以避免 2 个线程同时调用 getInstance() 创建 2 个实例。由于锁的成本很高,因此首先要进行没有锁的测试,然后使用锁进行测试(这是双重检查的锁定),以便在实例已经存在时不使用锁。

另一个特殊性是,实例必须是易失性的,以确保在创建实例时,其状态在不同的处理器内核上是相同的。

何时需要使用单例?

  • 当您只需要一个资源(数据库连接,套接字连接…
  • 避免无状态类的多个实例以避免内存浪费
  • 出于业务原因

您不应该使用单例在不同对象之间共享变量/数据,因为它会产生非常紧密的耦合!

为什么不使用单例?

一开始,我说你不应该使用单例,因为你获得单例的方式。它基于一个类函数,可以在代码中的任何位置调用该函数。我在stackoverflow上读到了一个很好的答案,给出了它不好的4个原因:

  • 使用单例,可以隐藏类之间的依赖关系,而不是通过接口公开它们。这意味着您需要阅读每个方法的代码,以了解一个类是否正在使用另一个类。
  • 它们违反了单一责任原则:它们控制自己的创建和生命周期(使用惰性初始化,单例在创建时选择)。一个类应该只关注它要做什么。如果你有一个管理人员的单例,它应该只管理人员,而不是如何/何时创建。
  • 它们本质上会导致代码紧密耦合。这使得伪造或嘲笑它们进行单元测试变得非常困难。
  • 它们在应用程序的生存期内携带状态(对于有状态单例)。
    • 它使单元测试变得困难,因为您可能最终会遇到需要订购测试的情况,这是一种无稽之谈。根据定义,每个单元测试都应该彼此独立。
    • 此外,它使代码的可预测性降低。

好吧,所以单例很糟糕。但是你应该使用什么呢?

使用单个实例而不是单一实例

单例只是一种特定类型的单个实例,可以使用其类方法到达任何地方。如果删除此第二个要求,则会删除许多问题。但是,如何处理单个实例呢?

一种可能的方法是使用工厂和依赖注入来管理单个实例(这将是未来文章的主题)。

让我们举个例子来理解:

  • 您有一个需要唯一数据库连接实例的 PersonBusiness 类。
  • PersonBusiness 将具有 DatabaseConnection 属性,而不是使用单例来获取此连接。
  • 此属性将由其构造函数在 PersonBusiness 的实例化时注入。当然,您可以注入任何类型的数据库连接:
    • 适用于您的开发环境的 MysqlDatabaseConnection
    • 面向生产环境的 OracleDatabaseConnection
    • 用于单元测试的模拟数据库连接
  • 在此阶段,没有任何操作可以阻止数据库连接是唯一的。这就是工厂有用的地方。您将PersonBusiness的创建委托给工厂,该工厂还负责数据库连接的创建:
    • 它选择要创建的连接类型(例如,使用指定连接类型的属性文件)
    • 它确保数据库连接是唯一的。

如果你不明白我刚才说了什么,看看下一个java示例,然后再次重读这部分,它应该更全面。否则,请随时告诉我。

下面是一个Java中的例子,其中工厂创建了一个MysqlDatabaseConnection,但你可以想象一个更复杂的工厂,它根据属性文件或环境变量决定连接的类型。

An interface that represents a database connection
public interface DatabaseConnection {
    public void executeQuerry(String sql);
}
 
A concrete implementation of this interface
In this example it's for a mysql database connection
public class MysqlDatabaseConnection implements DatabaseConnection {
    public MysqlDatabaseConnection() {
        // some stuff to create the database connection
    }
 
    public void executeQuerry(String sql) {
        // some stuff to execute a SQL query
        // on the database
    }
}
 
Our business class that needs a connection
public class PersonBusiness {
    DatabaseConnection connection;
 
    //dependency injection using the constructor
    // it is a singleton because the factory that
    //creates a PersonBusiness object ensure that
    //UniqueDatabaseConnection has only one instance
    PersonBusiness(DatabaseConnection connection){
        this.connection = connection;
    }
 
        //a method that uses the injected singleton
    public void deletePerson(int id){
 
        connection.executeQuerry("delete person where id="+id);
    }
}
 
A factory that creates business classes
 with a unique MysqlDatabaseConnection
public class Factory {
    private static MysqlDatabaseConnection databaseConnection = new MysqlDatabaseConnection();
 
    public static MysqlDatabaseConnection getUniqueMysqlDatabaseConnection(){
        return databaseConnection;
    }
 
    public static PersonBusiness createPersonBusiness(){
        //we inject a MysqlDataConnection but we could chose
        //another connection that implements the DatabaseConnection
        //this is why this is a loose coupling
        return new PersonBusiness(databaseConnection);
    }
 
}

这不是一个很好的例子,因为PersonBusiness可以有一个实例,因为它没有状态。但你可以想象,有一个ContractBusiness和一个HouseBusiness也需要那个独特的DatabaseConnection。

不过,我希望你看到,使用依赖注入+ 工厂,你最终会在你的业务类中得到一个数据库连接的单个实例,就像你使用了一个单例一样。但这一次,它是一个松散的耦合,这意味着你可以很容易地使用MockDatabaseConnection来测试PersonBusiness类,而不是使用MysqlDatabaseConnection。

此外,很容易知道PersonBusiness正在使用DatabaseConnection。你只需要查看类的属性,而不是类的2000行代码中的一行(好吧,想象一下这个类有很多函数,整体需要2000行代码)。

这种方法被大多数Java框架(Spring,Hibernate…)和Java容器(EJB容器)使用。它不是真正的单例,因为如果需要,您可以多次实例化类,并且无法从任何地方获取实例。但是,如果您仅通过工厂/容器创建实例,则最终会在代码中获得该类的唯一实例。

注意:我认为Spring框架非常令人困惑,因为它的“单例”范围只是一个实例。我花了一些时间才明白,这不是一个真正的GoF的单例。

一些想法

当涉及到全局状态时,单个实例具有与单例相同的缺点。您应该避免使用单个实例在不同类之间共享数据! 我看到的唯一例外是缓存:

  • 想象一下,您有一个交易应用程序,每秒进行数百次调用,它只需要具有最后几分钟的股票价格。您可以使用在交易业务类之间共享的单个实例 (StockPriceManager),每个需要价格的函数都将从缓存中获取它。如果价格已过时,缓存将刷新它。在这种情况下,紧密耦合的缺点值得在性能上获得收益。但是,当你因为这个全局状态而需要理解生产中的错误时,你会哭泣(我去过那里,这并不好笑)。

我告诉您使用单实例方法而不是单例,但有时当您在所有类中都需要此对象时,值得使用真正的单例。例如,当您需要记录以下内容时:

  • 每个类都需要记录,并且此日志类通常是唯一的(因为日志写在同一个文件中)。由于所有类都使用 log 类,因此您知道每个类都与此日志类具有隐式依赖关系。此外,这不是业务需求,因此对日志进行单元测试“不太重要”(我感到羞耻)。

编写单例比使用依赖关系注入编写单个实例更容易。对于快速而肮脏的解决方案,我将使用单例。对于一个长期耐用的解决方案,我将使用单个实例。由于大多数应用程序都基于框架,因此单个实例的实现比从头开始更容易(假设您知道如何使用框架)。

如果您想了解有关单例的更多信息:

  • 在 stackexchange 上,关于单例模式的缺点也有一个非常好的答案。

真实示例

单实例模式使用工厂。如果使用可实例化工厂,则可能需要确保此工厂是唯一的。更广泛地说,当您使用工厂时,您可能希望它是唯一的,以避免2个工厂实例相互混淆。你可以使用“元工厂”来构建唯一的工厂,但你最终会遇到“元工厂”的相同问题。因此,执行此操作的唯一方法是使用单例创建工厂。

旧图形库AWT中的java.awt.Toolkit就是这种情况。此类提供了一个 getDefaultToolkit() 方法,该方法提供唯一的 Toolkit 实例,这是获取实例的唯一方法。使用此工具包(这是一个工厂),您可以创建一个窗口,一个按钮,一个复选框…

但是,您也可以遇到单例以解决其他问题。当您需要用Java监视系统时,必须使用java.lang.Runtime类。我想这个类必须是唯一的,因为它表示进程的全局状态(环境变量)。如果我引用java API:

“每个 Java 应用程序都有一个类 Runtime 实例,该实例允许应用程序与运行应用程序的环境进行交互。当前运行时可以从 getRuntime 方法获取。

原型模式

我在整个春季都使用过原型,但我从来没有需要使用自己的原型。此模式旨在通过复制而不是构造函数来构建对象。以下是GoF给出的定义:

“使用原型实例指定要创建的对象类型,并通过复制此原型来创建新对象。

和GoF一样,我不理解他们的句子(这是因为英语不是我的母语吗?)。如果你像我一样,这里有另一个解释:如果你不想或不能使用类的构造函数,原型模式允许你通过复制已经存在的实例来创建这个类的新实例。

让我们看一下使用UML图的正式定义:

原型模式

在此图中

  • 原型是一个定义函数 clone() 的接口
  • 一个真正的原型必须实现这个接口,并实现 clone() 函数来返回自身的副本。

开发人员必须实例化一次 ConcretePrototype。然后,他将能够通过以下方式创建ConcretePrototype的新实例:

  • 使用 clone() 函数复制第一个实例
  • 或者使用(再次)构造函数创建混凝土原型。

何时使用原型?

根据Gof的说法,应该使用原型:

  • 当系统应该独立于其产品的创建,组合和表示方式

  • 当要实例化的类是在运行时指定的,例如,通过动态加载

  • 以避免构建与产品类层次结构平行的工厂类层次结构

  • 当类的实例可以具有只有几种不同的状态组合之一时。安装相应数量的原型并克隆它们可能比每次都使用适当的状态手动实例化类更方便。

未知类的动态加载是非常罕见的情况,如果需要复制动态加载的实例,则更是如此。

这本书写于1994年。现在,你可以通过使用依赖注入来“避免构建工厂的类层次结构”(同样,我将在以后的文章中介绍这个奇妙的模式)。

在我看来,最常见的情况是创建有状态实例比复制现有实例更昂贵,并且您需要创建大量此对象。例如,如果创建需要:

  • 从数据库连接获取数据,
  • 从系统(通过系统调用)或文件系统获取数据,
  • 从另一台服务器获取数据(使用套接字,Web服务或其他),
  • 计算大量数据(例如,如果需要对数据进行排序),
  • 做任何需要时间的事情。

该对象必须是有状态的,因为如果它没有状态,则 Singleton(或单个实例)将完成该操作。

还有另一个用例。如果您有一个可变的实例,并且您希望将其提供给代码的另一部分,出于安全原因,您可能希望提供副本而不是真实实例,因为客户端代码可以修改此实例,并对使用它的代码的其他部分产生影响。

Java 实现

让我们看一个Java中的简单示例:

  • 我们有一个汽车比较器商务舱。此类包含比较 2 辆车的函数。
  • 要实例化 CarComparator,构造函数需要从数据库中加载默认配置来配置汽车比较算法(例如,在油耗上比速度或价格更重要)。
  • 此类不能是单一实例,因为配置可以由每个用户修改(因此每个用户都需要自己的实例)。
  • 这就是为什么我们只使用昂贵的构造函数创建一次实例。
  • 然后,当客户需要 CarComparator 的实例时,他将获得第一个实例的副本。
//The Prototype interface
public interface Prototype {
    Prototype duplicate();
}
 
//The class we want to duplicate
public class CarComparator implements Prototype{
    private int priceWeigth;
    private int speedWeigth;
    private int fuelConsumptionWeigth;
 
    //a constructor that makes costly calls to a database
    //to get the default weigths
    public CarComparator(DatabaseConnection connect){
        //I let you imagine the costly calls to the database
    }
 
    //A private constructor only use to duplicate the object
    private CarComparator(int priceWeigth,int speedWeigth,int fuelConsumptionWeigth){
        this.priceWeigth=priceWeigth;
        this.speedWeigth=speedWeigth;
        this.fuelConsumptionWeigth=fuelConsumptionWeigth;
    }
 
    //The prototype method
    @Override
    public Prototype duplicate() {
        return new CarComparator(priceWeigth, speedWeigth, fuelConsumptionWeigth);
    }
 
    int compareCars(Car first, Car second){
        //some kickass and top secret algorithm using the weigths
    }
 
    The setters that lets the possibility to modify
     the algorithm behaviour
    public void setPriceWeigth(int priceWeigth) {
        this.priceWeigth = priceWeigth;
    }
 
    public void setSpeedWeigth(int speedWeigth) {
        this.speedWeigth = speedWeigth;
    }
 
    public void setFuelConsumptionWeigth(int fuelConsumptionWeigth) {
        this.fuelConsumptionWeigth = fuelConsumptionWeigth;
    }
 
// A factory that creates a CarComparator instance using
// constructors then it creates the others by duplication.
// When a client ask for a CarComparator
// he gets a duplicate
 
public class CarComparatorFactory {
    CarComparator carComparator;
    public BusinessClass (DatabaseConnection connect) {
        //We create one instance of CarComparator
        carComparator = new CarComparator(connect);
    }
 
    //we duplicate the instance so that
    //the duplicated instances can be modified
    public CarComparator getCarComparator(){
        return carComparator.duplicate();
    }
 
}

如果你看下一部分,你会发现我本可以使用正确的Java接口制作一个更简单的代码,但我希望你理解一个原型。

在此示例中,在启动时,将使用数据库中的默认配置创建原型,并且每个客户端将使用工厂的 getCarComparator() 方法获取此实例的副本。

真实示例

Java API提供了一个名为Cloneable的原型接口。此接口定义了一个 clone() 函数,具体原型需要实现该函数。Java API 中的许多 Java 类都实现了此接口,例如来自集合 API 的集合。使用ArrayList,我可以克隆它并获取一个新的ArrayList,其中包含与原始数组相同的数据:

// Let's initialize a list
// with 10  integers
ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; i++) {
   list.add(i);
}
System.out.println("content of the list "+list);
 
//Let's now duplicate the list using the prototype method
ArrayList<Integer> duplicatedSet = (ArrayList<Integer>) list.clone();
System.out.println("content of the duplicated list "+duplicatedSet);

此代码的结果是:

集合的内容[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
重复集合的内容[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

生成器模式

生成器模式对于分解代码非常有用。根据GoF,这种模式:

“将复杂对象的构造与其表示分开,以便相同的构造过程可以创建不同的表示。

它的目标随着时间的推移而变化,大多数时候它被用来避免创建许多构造函数,这些构造函数仅因参数数量而异。这是一种避免伸缩构造函数反模式的方法。

它解决的问题

让我们看一下此模式解决的问题。想象一个具有5个属性的类人:

  • 年龄
  • 重量
  • 高度
  • 编号
  • 名字

我们希望能够构建一个人知道:

  • 只有这个年龄,
  • 或只有这个年龄和体重,
  • 或只有这个年龄,体重和身高,
  • 或仅此年龄,体重,身高和ID
  • 或仅此年龄,体重,身高,ID和姓名

在java中,我们可以写这样的东西。

public class Person {
    private int age;
    private int weigth;
    private int height;
    private int id;
    private String name;
 
    //Here comes the telescopic constructors
    public Person() {
        //some stuff
    }
 
    public Person(int age) {
        this();//we're using the previous constructor
        this.age = age;
    }
 
    public Person(int age, int weigth) {
        this(age);//we're using the previous constructor
        this.weigth = weigth;
    }
 
    public Person(int age, int weigth, int height) {
        this(age, weigth);//we're using the previous constructor
        this.height= height;
    }   
 
    public Person(int age, int weigth, int height,int id) {
        this(age, weigth, height);//we're using the previous constructor
        this.id = id;
    }   
 
    public Person(int age, int weigth, int height,int id,String name) {
        this(age, weigth, height, id);//we're using the previous constructor
        this.name = name;
    }   
 
}

为了处理这个简单的需求,我们刚刚创建了5个构造函数,其中有很多代码。我知道java是一种非常冗长的语言(内部巨魔),但是如果有一种更干净的方式呢?

此外,使用这种伸缩式方法,代码很难阅读。例如,如果您阅读以下代码,您是否可以轻松理解参数是什么?他们是年龄,身份还是身高?

Person person1 = new Person (45, 45, 160, 1);
 
Person person2 = new Person (45, 170, 150);

下一个问题:假设你现在希望能够创建一个包含你获得的每一个可能的信息部分的人,例如:

  • 一个年龄
  • 重量
  • 年龄和体重
  • 一个年龄和一个ID,
  • 一个年龄,一个体重和一个名字,

使用构造函数方法,您最终将获得 120 个构造函数 (5! = 120)。你还有另一个问题,如何处理使用相同类型的不同构造函数?例如,您如何同时拥有两者:

  • age 和 weigth(为 2 int)的构造函数,以及
  • 年龄和 id 的构造函数(也是 2 int)?

您可以使用静态工厂方法,但它仍然需要 120 个静态工厂方法。

这就是建筑商发挥作用的地方!

此模式的思想是模拟命名的可选参数。这些类型的参数在某些语言(如python)中是本地可用的。

由于UML版本非常复杂(我认为),我们将从一个简单的java示例开始,并以UML正式定义结束。

一个简单的 java 示例

在此示例中,我有一个人,但这次 id 字段是必填字段,其他字段是可选的。

我创建了一个构建器,以便开发人员可以根据需要使用可选字段。

//the person class
/if you look at its constructor
/it requieres a builder
public class Person {
   private final int id;// mandatory
   private int weigth;// optional
   private int height;// optional
   private int age;// optional
   private String name;// optional
 
   public Person(PersonBuilder builder) {
       age = builder.age;
       weigth = builder.weigth;
       height = builder.height;
       id = builder.id;
       name = builder.name;
    }
}
//the builder that
/takes care of
/Person creation
public class PersonBuilder {
    // Required parameters
    final int id;
 
    // Optional parameters - initialized to default values
        int height;
    int age;
    int weigth;
    String name = "";
 
    public PersonBuilder(int id) {
        this.id = id;
    }
 
    public PersonBuilder age(int val) {
        age = val;
        return this;
    }
 
    public PersonBuilder weigth(int val) {
        weigth = val;
        return this;
    }
 
    public PersonBuilder height(int val) {
        height = val;
        return this;
    }
 
    public PersonBuilder name(String val) {
        name = val;
        return this;
    }
 
    public Person build() {
        return new Person(this);
    }
}
 
//Here is how to use the builder in order to build a Person
//You can see how readable is the code
public class SomeClass {
    public void someMethod(int id){
        PersonBuilder pBuilder = new PersonBuilder(id);
        Person robert = pBuilder.name("Robert").age(18).weigth(80).build();
        //some stuff
    }
 
    public void someMethodBis(int id){
        PersonBuilder pBuilder = new PersonBuilder(id);
        Person jennifer = pBuilder.height(170).name("Jennifer").build();
        //some stuff
    }
 
}

在此示例中,我假设类 Person 和 PersonBuilder 位于同一个包中,这允许构建器使用 Person 构造函数,并且包外的类必须使用 PersonBuilder 来创建 Person。

这个PersonBuilder有2种方法,一种用于构建人的一部分,另一种用于创建人。一个人的所有属性只能由同一包中的类修改。我应该使用getter和setter,但我想有一个简短的例子。您看到使用构建器的部分易于阅读,我们知道我们正在创建

  • 一个名叫罗伯特的人,他18岁,体重80岁,
  • 另一个名叫詹妮弗的人,她长170岁。

这种技术的另一个优点是,您仍然可以创建不可变的对象。在我的示例中,如果我不在 Person 类中添加公共 setter,则 Person 实例是不可变的,因为包外部的任何类都不能修改其属性。

正式定义

现在让我们看一下UML:

GoF 的生成器模式

这张图真的很抽象,GoF的构建器有:

  • 一个生成器界面,用于指定用于创建 Product 对象的部件的函数。在我的图中,只有一个方法,buildPart()。
  • 一个 ConcreteBuilder,它通过实现 Builder 接口来构造和组装产品的部件。
  • 控制器:它使用生成器界面构造产品。

根据 GoF,此模式在以下情况下很有用:

• 创建复杂对象的算法应独立于构成对象的部件及其组装方式。
•构造过程必须允许对构造的对象进行不同的表示。

GoF给出的例子是一个TextConverter构建器,它有3个实现要构建:ASCIIText或TeXText或TextWidget。3 个生成器实现(ASCIIConverter、TeXConverter 和 TextWidgetConverter)具有相同的函数,除了 createObject() 函数不同(这就是为什么此函数不在此模式的接口中)。使用此模式,转换文本的代码(控制器)使用生成器界面,因此它可以轻松地从 ASCII 切换到 TeX 或 TextWidget。此外,您可以添加新的转换器,而无需修改其余代码。在某种程度上,这种模式非常接近国家模式。

但这个问题是罕见的。

这种模式的另一个用途是由Java开发人员Joshua Bloch推广的,他领导了许多Java API的构建。他在《*有效的Java》*一书中写道:

“在面对许多构造函数参数时考虑构建器”

大多数情况下,该模式用于此用例。对于此问题,您不需要生成器接口、多个生成器实现或控制器。在我的java示例中,大多数时候你会发现只有一个具体的构建器。

然后,UML 变得更加容易:

约书亚·布洛赫的建造者模式

在此图中,ConcreteBuilder具有多个函数来创建产品的每个部分(但我只是放了一个,buildPart(),因为我懒惰)。这些函数返回 ConcreteBuilder,以便您可以链接函数调用,例如:builder.buildPart1().buildPart7().createObject()。构建器有一个 createObject() 方法,用于在您不需要添加更多部件时创建产品。

总而言之,当您的类具有许多可选参数并且不希望最终得到许多构造函数时,构建器模式是一个不错的选择。虽然这个模式不是为这个问题而设计的,但它大部分时间都用于这个问题(至少在Java中)。

真实示例

Java API中最常见的例子是StringBuilder。使用它,您可以创建一个临时字符串,向其追加新字符串,完成后,您可以创建一个真正的String对象(这是不可变的)。

StringBuilder sBuilder = new StringBuilder();
String example = sBuilder.append("this").append(" is").
   append(" an").append(" example").toString();
System.out.println(example);

结论

您现在应该对创建模式有了更好的了解。如果您需要记住一件事,那就是使用单个实例而不是单例。请记住生成器模式(Joshua Bloch的版本),如果您正在处理可选参数,它可能会很有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1628443.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

stm32 hid自定义接收发送程序开发过程记录

cubleMX配置如下 修改端点描述符一次传输的数据大小 根据cubelMX标准在这里修改 编译错误 直接修改&#xff08;因为没有使用nodef &#xff09;编译通过 修改报告描述符&#xff08;默认的描述符无法传输数据&#xff09; 参考&#xff1a;USB协议详解第10讲&#xff08;USB描…

基于SpringBoot的“在线BLOG网”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“在线BLOG网”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 在线BLOG网结构功能图 管理员登录功能界面 用户信息…

电脑屏幕监控软件有哪些?大型企业都在用的电脑屏幕监控软件

电脑监控软件&#xff0c;这个东西真的很重要&#xff0c;就像保镖一样&#xff0c;保护我们的电脑和数据安全。它能预防坏事发生&#xff0c;还能在事情发生时及时控制&#xff0c;事后还能帮我们找出问题的根源。对于那些经常担心数据泄密的企业来说&#xff0c;电脑屏幕监控…

2024 XYCTF Web 方向 wp 全解

2024 XYCTF Web 方向 全解 文章目录 2024 XYCTF Web 方向 全解Webezhttp考点:信息泄露基础发包 warm up考点:php黑魔法变量覆盖 ezRCE考点:无字母RCE(bashfuck)shell变量构造RCE ezmd5考点:md5文件强相等 ezunserilze考点&#xff1a;引用绕过强相等php原生类读文件 牢牢记住&a…

【加密周报】中美下周有“大事”发生!准备联手引爆比特币大行情?美国大型养老基金和梅隆银行已持有比特币ETF!

自减半之后&#xff0c;比特币便进入了横盘状态&#xff0c;始终在6-6.5万美元价格区间震荡。4月24日&#xff0c;香港证监会官网正式公示虚拟资产现货ETF获批名单&#xff0c;华夏&#xff08;香港&#xff09;、嘉实国际、博时国际旗下相关产品均在其列&#xff0c;并计划将于…

打印给定数组中每一个数字

如何给定1-10的数字 #include<stdio.h> int main() {int arr[] { 1,2,3,4,5,6,7,8,9,10 };// 0 9//[]--下标引用操作符int i;int sz sizeof(arr) / sizeof(arr[0]);//10个数for (i 0; i < sz; i){printf("%d ", arr[i]);}re…

【项目分享】用 Python 写一个桌面倒计日程序!

事情是这样的&#xff0c;我们班主任想委托我做一个程序&#xff0c;能显示还有几天考试。我立即理解了这个意思&#xff0c;接下了这个项目。 话不多说&#xff0c;来看看这个项目吧—— 项目简介 仓库地址&#xff1a;https://gitee.com/yaoqx/desktop-countdown-day 这是 …

排序 “肆” 之归并排序

1. 归并排序 1.1 原理介绍 归并排序的基本原理是将一个未排序的数组分解为较小的子数组&#xff0c;然后递归地对这些子数组进行排序&#xff0c;最后再将排好序的子数组合并成一个有序数组。其核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。 其主要步骤包…

C++链表操作入门

数据结构基础&#xff1a;链表操作入门 数据结构基础&#xff1a;链表操作入门链表的基本概念链表的基本操作输出链表插入节点删除节点查找值 完整的链表操作示例结语 数据结构基础&#xff1a;链表操作入门 在计算机科学中&#xff0c;数据结构是组织和存储数据的方式&#x…

监控上网行为的软件 五款上网行为监控软件推荐

在这个数字化时代&#xff0c;员工上网行为的管理与监控变得越来越重要。市面上涌现出众多监控员工上网的软件&#xff0c;它们各具特色&#xff0c;各有千秋。接下来&#xff0c;就让我们一起探索一下这些神奇的软件吧&#xff01; 1、安企神点击领取免费试用版 这是一款功能…

LMDeploy量化部署LLMVLM实践-笔记五

本次课程由西北工业大学博士生、书生浦源挑战赛冠军队伍队长、第一期书生浦语大模型实战营优秀学员【安泓郡】讲解【OpenCompass 大模型评测实战】课程 课程视频&#xff1a;https://www.bilibili.com/video/BV1tr421x75B/ 课程文档&#xff1a;https://github.com/InternLM/…

12 内核开发-任务调度之tasklet

12 内核开发-任务调度之tasklet 课程简介&#xff1a; Linux内核开发入门是一门旨在帮助学习者从最基本的知识开始学习Linux内核开发的入门课程。该课程旨在为对Linux内核开发感兴趣的初学者提供一个扎实的基础&#xff0c;让他们能够理解和参与到Linux内核的开发过程中。 课…

C++系列-命名空间

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 命名空间 在C/C中&#xff0c;变量&#xff0c;函数和后面要学到的类都是大量存在的&#xff0c;这些变量&#xff0c;函数和类的名称都存在于全局作用域中&#xff0c;可能会导…

【Linux系统化学习】生产者消费者模型(阻塞队列和环形队列)

目录 生产者消费者模型 什么是生产者消费者模型 为什么要使用生产者消费者模型 生产者消费者模型的优点 为什么生产者和生产者要互斥&#xff1f; 为什么消费者和消费者要互斥&#xff1f; 为什么生产者和消费者既是互斥又是同步&#xff1f; 基于BlockingQueue的生产者…

第四百八十一回

文章目录 1. 概念介绍2. 使用方法2.1 固定样式2.2 自定义样式 3. 示例代码4. 内容总结 我们在上一章回中介绍了"GetMaterialApp组件"相关的内容&#xff0c;本章回中将介绍使用get显示SnackBar.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在介…

Pandas 2.2 中文官方教程和指南(十一·一)

原文&#xff1a;pandas.pydata.org/docs/ PyArrow 功能 原文&#xff1a;pandas.pydata.org/docs/user_guide/pyarrow.html pandas 可以利用PyArrow来扩展功能并改善各种 API 的性能。这包括&#xff1a; 与 NumPy 相比&#xff0c;拥有更广泛的数据类型 对所有数据类型支持缺…

matlab新手快速上手3(差分进化算法)

本文用经典差分进化框架模板&#xff0c;对matlab新手友好&#xff0c;快速上手看懂matlab代码&#xff0c;快速应用实践&#xff0c;源代码在文末给出。 差分进化算法定义&#xff1a; 差分进化算法&#xff08;Differential Evolution&#xff0c;简称DE算法&#xff09;是…

基于springboot实现企业oa管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现企业oa管理系统演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了企业OA管理系统的开发全过程。通过分析企业OA管理系统管理的不足&#xff0c;创建了一个计算机管理企业OA管理系统的方案…

SVGDreamer: 文本引导矢量图形合成

现有的 Text-to-SVG 方法还存在两个限制&#xff1a;1.生成的矢量图缺少编辑性&#xff1b;2. 难以生成高质量和多样性的结果。为了解决这些限制&#xff0c;作者提出了一种新的文本引导矢量图形合成方法&#xff1a;SVGDreamer。 论文题目&#xff1a; SVGDreamer: Text Guid…

❤mac使用Idea工具

❤mac使用Idea工具 1、安装 直接跳过&#xff0c;文章有 &#xff08;点击跳转&#xff09; 给自己的mac系统上安装java环境 2、使用 快捷键 Command , 系统首选项 设置Idea连接数据库 打开右侧的database&#xff08;或菜单里&#xff09;连接数据库&#xff0c;根据提…