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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 设计模式:单例、原型和生成器

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


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


@[toc]


# 创造模式


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


- 单身人士,

- 建筑工人,

- 原型,

- 抽象工厂,

- 工厂模式。

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/afd3790f031347bdadb72ad2ed2ccb9c.png)


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


- 其他类型的工厂(如静态工厂),

- 池模式,

- 惰性初始化,

- 依赖注入,

- 服务定位器,

- ...


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



# 单例模式


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


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


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


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


- 拥有唯一实例

- 可从任何地方访问


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


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


[![单例模式](https://ucc.alicdn.com/images/user-upload-01/img_convert/499cb7a47089bbfc2d96c5cafb6faa65.png)](http://coding-geek.com/wp-content/uploads/2015/06/singleton.png)



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


- 类属性(实例):此属性包含单例类的唯一实例。

- 一个类公共方法(getInstance()):它提供了获取类Singleton的唯一实例的唯一方法。可以从任何地方调用该方法,因为它是类方法(而不是实例方法)。

- 私有构造函数(Singleton()):它阻止任何人使用构造函数实例化单例。


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


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


- 预初始化(这意味着在有人调用 getInstance()之前,它被实例化了)

- lazy-initialized(这意味着它是在第一次调用getInstance()期间实例化的)


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



## Java 实现


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


```java

public class SimpleSingleton {

   private static final SimpleSingleton INSTANCE = new SimpleSingleton();

   private SimpleSingleton() { }

   public static SimpleSingleton getInstance(){

       return INSTANCE;

   }

}

```


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


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


```java

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上](https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons)读到了一个很好的答案,给出了它不好的4个原因:


- 使用单例,可以隐藏类之间的依赖关系,而不是通过接口公开它们。这意味着您需要阅读每个方法的代码,以了解一个类是否正在使用另一个类。

- 它们违反了[单一责任原则](https://en.wikipedia.org/wiki/Single_responsibility_principle):它们控制自己的创建和生命周期(使用惰性初始化,单例在创建时选择)。一个类应该只关注它要做什么。如果你有一个管理人员的单例,它应该只管理人员,而不是如何/何时创建。

- 它们本质上会导致代码紧密耦合。这使得伪造或嘲笑它们进行单元测试变得非常困难。

- 它们在应用程序的生存期内携带状态(对于有状态单例)。

 - 它使单元测试变得困难,因为您可能最终会遇到需要订购测试的情况,这是一种无稽之谈。根据定义,每个单元测试都应该彼此独立。

 - 此外,它使代码的可预测性降低。


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



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


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


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


让我们举个例子来理解:


- 您有一个需要唯一数据库连接实例的 PersonBusiness 类。

- PersonBusiness 将具有 DatabaseConnection 属性,而不是使用单例来获取此连接。

- 此属性将由其构造函数在 PersonBusiness 的实例化时注入。当然,您可以注入任何类型的数据库连接:

 - 适用于您的开发环境的 MysqlDatabaseConnection

 - 面向生产环境的 OracleDatabaseConnection

 - 用于单元测试的模拟数据库连接

- 在此阶段,没有任何操作可以阻止数据库连接是唯一的。这就是工厂有用的地方。您将PersonBusiness的创建委托给工厂,该工厂还负责数据库连接的创建:

 - 它选择要创建的连接类型(例如,使用指定连接类型的属性文件)

 - 它确保数据库连接是唯一的。



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


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


```java

////////////////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 上,关于[单例模式的缺点](https://programmers.stackexchange.com/questions/40373/so-singletons-are-bad-then-what)也有一个非常好的答案。



## 真实示例


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


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



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


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



# 原型模式


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


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


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


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


[![原型模式](https://ucc.alicdn.com/images/user-upload-01/img_convert/7bd597f77b6b81242842d2916ba75acc.png)](http://coding-geek.com/wp-content/uploads/2015/06/prototype.png)


在此图中


- 原型是一个定义函数 clone() 的接口

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


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


- 使用 clone() 函数复制第一个实例

- 或者使用(再次)构造函数创建混凝土原型。



## 何时使用原型?


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


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


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


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

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



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


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


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


- 从数据库连接获取数据,

- 从系统(通过系统调用)或文件系统获取数据,

- 从另一台服务器获取数据(使用套接字,Web服务或其他),

- 计算大量数据(例如,如果需要对数据进行排序),

- 做任何需要时间的事情。


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


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



## Java 实现


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


- 我们有一个汽车比较器商务舱。此类包含比较 2 辆车的函数。

- 要实例化 CarComparator,构造函数需要从数据库中加载默认配置来配置汽车比较算法(例如,在油耗上比速度或价格更重要)。

- 此类不能是单一实例,因为配置可以由每个用户修改(因此每个用户都需要自己的实例)。

- 这就是为什么我们只使用昂贵的构造函数创建一次实例。

- 然后,当客户需要 CarComparator 的实例时,他将获得第一个实例的副本。


```java

//////////////////////////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的](https://docs.oracle.com/javase/8/docs/api/java/lang/Cloneable.html)原型接口。此接口定义了一个 clone() 函数,具体原型需要实现该函数。Java API 中的许多 Java 类都实现了此接口,例如来自集合 API 的集合。使用ArrayList,我可以克隆它并获取一个新的ArrayList,其中包含与原始数组相同的数据:


```java

// 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中,我们可以写这样的东西。


```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是一种非常冗长的语言(内部巨魔),但是如果有一种更干净的方式呢?


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


```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 字段是必填字段,其他字段是可选的。


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


```java

//////////////////////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 的生成器模式](https://ucc.alicdn.com/images/user-upload-01/img_convert/5a6bc67754bc268fc56ce2648dabcd1a.png)](http://coding-geek.com/wp-content/uploads/2015/06/builder.png)



这张图真的很抽象,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 变得更加容易:


[![约书亚·布洛赫的建造者模式](https://ucc.alicdn.com/images/user-upload-01/img_convert/910ae5ea7a032bb32f297d5f19223634.png)](http://coding-geek.com/wp-content/uploads/2015/06/builder2.png)


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



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



## 真实示例


Java API中最常见的例子是[StringBuilder](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html)。使用它,您可以创建一个临时字符串,向其追加新字符串,完成后,您可以创建一个真正的String对象(这是不可变的)。


```java

StringBuilder sBuilder = new StringBuilder();

String example = sBuilder.append("this").append(" is").

  append(" an").append(" example").toString();

System.out.println(example);

```





# 结论


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

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
2月前
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
67 0
|
2月前
|
设计模式 缓存 安全
单例设计模式的优缺点
单例设计模式的优缺点
34 0
|
2月前
|
设计模式 XML 存储
关于 ABAP 单例设计模式的一个冷门知识点
关于 ABAP 单例设计模式的一个冷门知识点
23 0
|
2月前
|
设计模式 安全 Java
【设计模式】2、设计模式分类和单例设计模式
【设计模式】2、设计模式分类和单例设计模式
28 0
|
2月前
|
设计模式 Java
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
26、Java 简单实现单例设计模式(饿汉式和懒汉式)
30 2
|
2月前
|
设计模式 安全 Java
在Java中即指单例设计模式
在Java中即指单例设计模式
22 0
|
17天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
4月前
|
设计模式 缓存 安全
Go语言必学的单例设计模式
Go语言必学的单例设计模式
39 0
|
4月前
|
设计模式 存储 Java
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(四)
95 1
|
4月前
|
设计模式 存储 安全
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)
多线程编程设计模式(单例,阻塞队列,定时器,线程池)(三)
39 2