Spring框架中的设计模式(四)

本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。
一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨论单例模式。

适配器

当我们需要在给定场景下(也就是给定接口)想要不改变自身行为而又想做到一些事情的情况下(就是我给电也就是接口了,你来做事也就是各种电器),使用适配器设计模式(这里再说一点,就相当于我们再一个规章制度的环境下,如何去适应并达到我们期待的效果,放在架构设计这里,可以拿一个php系统和一个Java系统来说,假如两者要互相调用对方的功能,我们可以设计一套对外的api来适配)。这意味着在调用此对象之前,我们将更改使用对象而不改变机制。拿一个现实中的例子进行说明,想象一下你想要用电钻来钻一个洞。要钻一个小洞,你会使用小钻头,钻一个大的需要用大钻头。可以看下面的代码:

public class AdapterTest {

  public static void main(String[] args) {
    HoleMaker maker = new HoleMakerImpl();
    maker.makeHole(1);
    maker.makeHole(2);
    maker.makeHole(30);
    maker.makeHole(40);
  }
}

interface HoleMaker {
  public void makeHole(int diameter);
}

interface DrillBit {
  public void makeSmallHole();
  public void makeBigHole();
}

// Two adaptee objects
class BigDrillBit implements DrillBit {

  @Override
  public void makeSmallHole() {
    // do nothing
  }

  @Override
  public void makeBigHole() {
    System.out.println("Big hole is made byt WallBigHoleMaker");
  }
}

class SmallDrillBit implements DrillBit {

  @Override
  public void makeSmallHole() {
    System.out.println("Small hole is made byt WallSmallHoleMaker");
  }

  @Override
  public void makeBigHole() {
    // do nothing
  }
}

// Adapter class
class Drill implements HoleMaker {

  private DrillBit drillBit;

  public Drill(int diameter) {
    drillBit = getMakerByDiameter(diameter);
  }

  @Override
  public void makeHole(int diameter) {
    if (isSmallDiameter(diameter)) {
            drillBit.makeSmallHole();
    } else {
            drillBit.makeBigHole();
    }
  }

  private DrillBit getMakerByDiameter(int diameter) {
    if (isSmallDiameter(diameter)) {
            return new SmallDrillBit();
    }
    return new BigDrillBit();
  }

  private boolean isSmallDiameter(int diameter) {
    return diameter < 10;
  }
}

// Client class
class HoleMakerImpl implements HoleMaker {

  @Override
  public void makeHole(int diameter) {
    HoleMaker maker = new Drill(diameter);
    maker.makeHole(diameter);
  }
}

以上代码的结果如下:

Small hole is made byt SmallDrillBit
Small hole is made byt SmallDrillBit
Big hole is made byt BigDrillBit
Big hole is made byt BigDrillBit

可以看到,hole 是由所匹配的DrillBit对象制成的。如果孔的直径小于10,则使用SmallDrillBit。如果它更大,我们使用BigDrillBit。

思路就是,要打洞,那就要有打洞的工具,这里提供一个电钻接口和钻头。电钻就是用来打洞的,所以,它就一个接口方法即可,接下来定义钻头的接口,无非就是钻头的尺寸标准,然后搞出两个钻头实现类出来,接下来就是把钻头和电钻主机组装起来咯,也就是Drill类,里面有电钻接口+钻头(根据要钻的孔大小来确定用哪个钻头),其实也就是把几个单一的东西组合起来拥有丰富的功能,最后我们进行封装下:HoleMakerImpl,这样只需要根据尺寸就可以打相应的孔了,对外暴露的接口极为简单,无须管内部逻辑是多么复杂

Spring使用适配器设计模式来处理不同servlet容器中的加载时编织(load-time-weaving)。在面向切面编程(AOP)中使用load-time-weaving,一种方式是在类加载期间将AspectJ的方面注入字节码。另一种方式是对类进行编译时注入或对已编译的类进行静态注入。

我们可以从关于Spring和JBoss的处理接口这里找到一个很好的例子,它包含在org.springframework.instrument.classloading.jboss包中。我们检索JBossLoadTimeWeaver类负责JBoss容器的编织管理。然而,类加载器对于JBoss 6(使用JBossMCAdapter实例)和JBoss 7/8(使用JBossModulesAdapter实例)是不同的。根据JBoss版本,我们在JBossLoadTimeWeaver构造函数中初始化相应的适配器(与我们示例中的Drill的构造函数完全相同):

public JBossLoadTimeWeaver(ClassLoader classLoader) {
  private final JBossClassLoaderAdapter adapter;

  Assert.notNull(classLoader, "ClassLoader must not be null");
  if (classLoader.getClass().getName().startsWith("org.jboss.modules")) {
    // JBoss AS 7 or WildFly 8
    this.adapter = new JBossModulesAdapter(classLoader);
  }
  else {
    // JBoss AS 6
    this.adapter = new JBossMCAdapter(classLoader);
  }
}

而且,此适配器所创建的实例用于根据运行的servlet容器版本进行编织操作:

@Override
public void addTransformer(ClassFileTransformer transformer) {
  this.adapter.addTransformer(transformer);
}

@Override
public ClassLoader getInstrumentableClassLoader() {
  return this.adapter.getInstrumentableClassLoader();
}

总结:适配器模式,其实就是我们用第一人称的视角去看世界,我想拓展我自己的技能的时候,就实行拿来主义,就好比这里的我是电钻的视角,那么我想拥有钻大孔或者小孔的功能,那就把钻头拿到手组合起来就好。

和装饰模式的区别:装饰模式属于第三人称的视角,也就是上帝视角!我只需要把几个功能性的组件给拿到手,进行组合一下,实现一个更加niubility的功能这里提前说下,这样看下面的内容能好理解些。下面解释装饰模式

装饰

这里描述的第二种设计模式看起来类似于适配器。它是装饰模式。这种设计模式的主要作用是为给定的对象添加补充角色。举个现实的例子,就拿咖啡来讲。通常越黑越苦,你可以添加(装饰)糖和牛奶,使咖啡不那么苦。咖啡在这里被装饰的对象,糖与牛奶是用来装饰的。可以参考下面的例子:

public class DecoratorSample {

  @Test
  public void test() {
    Coffee sugarMilkCoffee=new MilkDecorator(new SugarDecorator(new BlackCoffee()));
    assertEquals(sugarMilkCoffee.getPrice(), 6d, 0d);
  }
}

// decorated
abstract class Coffee{
  protected int candied=0;
  protected double price=2d;
  public abstract int makeMoreCandied();
  public double getPrice(){
    return this.price;
  }
  public void setPrice(double price){
    this.price+=price;
  }
}
class BlackCoffee extends Coffee{
  @Override
  public int makeMoreCandied(){
    return 0;
  }
  @Override
  public double getPrice(){
    return this.price;
  }
}

// abstract decorator
abstract class CoffeeDecorator extends Coffee{
  protected Coffee coffee;
  public CoffeeDecorator(Coffee coffee){
    this.coffee=coffee;
  }
  @Override
  public double getPrice(){
    return this.coffee.getPrice();
  }
  @Override
  public int makeMoreCandied(){
    return this.coffee.makeMoreCandied();
  }
}

// concrete decorators
class MilkDecorator extends CoffeeDecorator{
  public MilkDecorator(Coffee coffee){
    super(coffee);
  }
  @Override
  public double getPrice(){
    return super.getPrice()+1d;
  }
  @Override
  public int makeMoreCandied(){
    return super.makeMoreCandied()+1;
  }
}
class SugarDecorator extends CoffeeDecorator{
  public SugarDecorator(Coffee coffee){
    super(coffee);
  }
  @Override
  public double getPrice(){
    return super.getPrice()+3d;
  }
  @Override
  public int makeMoreCandied(){
    return super.makeMoreCandied()+1;
  }
}

上面这个简单的装饰器的小例子是基于对父方法的调用,从而改变最后的属性(我们这里是指价格和加糖多少)。在Spring中,我们在处理与Spring管理缓存同步事务的相关类中可以 发现装饰器设计模式的例子。这个类是org.springframework.cache.transaction.TransactionAwareCacheDecorator。

这个类的哪些特性证明它是org.springframework.cache.Cache对象的装饰器?首先,与我们的咖啡示例一样,TransactionAwareCacheDecorator的构造函数接收参数装饰对象(Cache):

private final Cache targetCache;
/**
 * Create a new TransactionAwareCache for the given target Cache.
 * @param targetCache the target Cache to decorate
 */
public TransactionAwareCacheDecorator(Cache targetCache) {
  Assert.notNull(targetCache, "Target Cache must not be null");
  this.targetCache = targetCache;
}

其次,通过这个对象,我们可以得到一个新的行为:为给定的目标缓存创建一个新的TransactionAwareCache。这个我们可以在TransactionAwareCacheDecorator的注释中可以阅读到,其主要目的是提供缓存和Spring事务之间的同步级别。这是通过org.springframework.transaction.support.TransactionSynchronizationManager中的两种缓存方法实现的:put 和 evict(其实最终不还是通过targetCache来实现的么):

@Override
public void put(final Object key, final Object value) {
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
    TransactionSynchronizationManager.registerSynchronization(
      new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
          targetCache.put(key, value);
        }
    });
  }
  else {
    this.targetCache.put(key, value);
  }
}

@Override
public void evict(final Object key) {
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
          TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
              @Override
              public void afterCommit() {
                targetCache.evict(key);
              }
          });
  }
  else {
    this.targetCache.evict(key);
  }
}

这种模式看起来类似于适配器,对吧?但是,它们还是有区别的。我们可以看到,适配器将对象适配到运行时环境,即。如果我们在JBoss 6中运行,我们使用与JBoss 7不同的类加载器。Decorator每次使用相同的主对象(Cache)工作,并且仅向其添加新行为(与本例中的Spring事务同步),另外,可以通过我在解读这个设计模式之前的说法来区分二者。

我们再以springboot的初始化来举个例子的,这块后面会进行仔细的源码分析的,这里就仅仅用设计模式来说下的:

/**
 * Event published as early as conceivably possible as soon as a {@link SpringApplication}
 * has been started - before the {@link Environment} or {@link ApplicationContext} is
 * available, but after the {@link ApplicationListener}s have been registered. The source
 * of the event is the {@link SpringApplication} itself, but beware of using its internal
 * state too much at this early stage since it might be modified later in the lifecycle.
 *
 * @author Dave Syer
 */
@SuppressWarnings("serial")
public class ApplicationStartedEvent extends SpringApplicationEvent {
    /**
     * Create a new {@link ApplicationStartedEvent} instance.
     * @param application the current application
     * @param args the arguments the application is running with
     */
    public ApplicationStartedEvent(SpringApplication application, String[] args) {
        super(application, args);
    }
}

从注释可以看出 ApplicationListener要先行到位的,然后就是started的时候Event published走起,接着就是Environment配置好,ApplicationContext进行初始化完毕,那我们去看ApplicationListener的源码:

/**
 * Listener for the {@link SpringApplication} {@code run} method.
 * {@link SpringApplicationRunListener}s are loaded via the {@link SpringFactoriesLoader}
 * and should declare a public constructor that accepts a {@link SpringApplication}
 * instance and a {@code String[]} of arguments. A new
 * {@link SpringApplicationRunListener} instance will be created for each run.
 *
 * @author Phillip Webb
 * @author Dave Syer
 */
public interface SpringApplicationRunListener {
    /**
     * Called immediately when the run method has first started. Can be used for very
     * early initialization.
     */
    void started();
    /**
     * Called once the environment has been prepared, but before the
     * {@link ApplicationContext} has been created.
     * @param environment the environment
     */
    void environmentPrepared(ConfigurableEnvironment environment);
    /**
     * Called once the {@link ApplicationContext} has been created and prepared, but
     * before sources have been loaded.
     * @param context the application context
     */
    void contextPrepared(ConfigurableApplicationContext context);
    /**
     * Called once the application context has been loaded but before it has been
     * refreshed.
     * @param context the application context
     */
    void contextLoaded(ConfigurableApplicationContext context);
    /**
     * Called immediately before the run method finishes.
     * @param context the application context or null if a failure occurred before the
     * context was created
     * @param exception any run exception or null if run completed successfully.
     */
    void finished(ConfigurableApplicationContext context, Throwable exception);
}

看类注释我们可以知道,需要实现此接口内所定义的这几个方法,ok,来看个实现类:

/**
 * {@link SpringApplicationRunListener} to publish {@link SpringApplicationEvent}s.
 * <p>
 * Uses an internal {@link ApplicationEventMulticaster} for the events that are fired
 * before the context is actually refreshed.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 */
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    private final SpringApplication application;
    private final String[] args;
    private final ApplicationEventMulticaster initialMulticaster;
    public EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        for (ApplicationListener<?> listener : application.getListeners()) {
            this.initialMulticaster.addApplicationListener(listener);
        }
    }
    @Override
    public int getOrder() {
        return 0;
    }
    @Override
    public void started() {
        this.initialMulticaster
                .multicastEvent(new ApplicationStartedEvent(this.application, this.args));
    }
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
                this.application, this.args, environment));
    }
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
    }
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        for (ApplicationListener<?> listener : this.application.getListeners()) {
            if (listener instanceof ApplicationContextAware) {
                ((ApplicationContextAware) listener).setApplicationContext(context);
            }
            context.addApplicationListener(listener);
        }
        this.initialMulticaster.multicastEvent(
                new ApplicationPreparedEvent(this.application, this.args, context));
    }
    @Override
    public void finished(ConfigurableApplicationContext context, Throwable exception) {
        // Listeners have been registered to the application context so we should
        // use it at this point
        context.publishEvent(getFinishedEvent(context, exception));
    }
    private SpringApplicationEvent getFinishedEvent(
            ConfigurableApplicationContext context, Throwable exception) {
        if (exception != null) {
            return new ApplicationFailedEvent(this.application, this.args, context,
                    exception);
        }
        return new ApplicationReadyEvent(this.application, this.args, context);
    }
}

从上可以看出,EventPublishingRunListener里对接口功能的实现,主要是通过SpringApplicationApplicationEventMulticaster 来实现的,自己不干活,挂个虚名,从上帝模式的角度来看,这不就是应用了装饰模式来实现的么。

更多源码解析请关注后续的本人对Spring框架全面的重点部分解析系列博文

单例

单例,我们最常用的设计模式。正如我们在很多Spring Framework中关于单例和原型bean的文章(网上太多了)中已经看到过的,单例是几个bean作用域中的中的一个。此作用域在每个应用程序上下文中仅创建一个给定bean的实例。与signleton设计模式有所区别的是,Spring将实例的数量限制的作用域在整个应用程序的上下文。而Singleton设计模式在Java应用程序中是将这些实例的数量限制在给定类加载器管理的整个空间中。这意味着我们可以为两个Spring的上下文(同一份配置文件起两个容器,也就是不同端口的容器实例)使用相同的类加载器,并检索两个单例作用域的bean。

在看Spring单例应用之前,让我们来看一个Java的单例例子:

public class SingletonTest {

  @Test
  public void test() {
    President president1 = (President) SingletonsHolder.PRESIDENT.getHoldedObject();
    President president2 = (President) SingletonsHolder.PRESIDENT.getHoldedObject();
    assertTrue("Both references of President should point to the same object", president1 == president2);
    System.out.println("president1 = "+president1+" and president2 = "+president2);
    // sample output
    // president1 = com.waitingforcode.test.President@17414c8 and president2 = com.waitingforcode.test.President@17414c8

  }

}

enum SingletonsHolder {

  PRESIDENT(new President());

  private Object holdedObject;

  private SingletonsHolder(Object o) {
          this.holdedObject = o;
  }

  public Object getHoldedObject() {
          return this.holdedObject;
  }

}

class President {
}

这个测试例子证明,只有一个由SingletonsHolder所持有的President实例。在Spring中,我们可以在bean工厂中找到单例应用的影子(例如在org.springframework.beans.factory.config.AbstractFactoryBean中):

/**
 * Expose the singleton instance or create a new prototype instance.
 * @see #createInstance()
 * @see #getEarlySingletonInterfaces()
 */
@Override
public final T getObject() throws Exception {
  if (isSingleton()) {
    return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
  }
  else {
    return createInstance();
  }
}

我们看到,当需求对象被视为单例时,它只被初始化一次,并且在每次使用同一个bean类的实例后返回。我们可以在给定的例子中看到,类似于我们以前看到的President情况。将测试bean定义为:

<bean id="shoppingCart" class="com.waitingforcode.data.ShoppingCart" />

测试用例如下所示:

public class SingletonSpringTest {

  @Test
  public void test() {
    // retreive two different contexts
    ApplicationContext firstContext = new FileSystemXmlApplicationContext("applicationContext-test.xml");
    ApplicationContext secondContext = new FileSystemXmlApplicationContext("applicationContext-test.xml");

    // prove that both contexts are loaded by the same class loader
    assertTrue("Class loaders for both contexts should be the same", 
      firstContext.getClassLoader() == secondContext.getClassLoader());
    // compare the objects from different contexts
    ShoppingCart firstShoppingCart = (ShoppingCart) firstContext.getBean("shoppingCart");
    ShoppingCart secondShoppingCart = (ShoppingCart) secondContext.getBean("shoppingCart");
    assertFalse("ShoppingCart instances got from different application context shouldn't be the same", 
      firstShoppingCart == secondShoppingCart);

    // compare the objects from the same context
    ShoppingCart firstShoppingCartBis = (ShoppingCart) firstContext.getBean("shoppingCart");
    assertTrue("ShoppingCart instances got from the same application context should be the same", 
      firstShoppingCart == firstShoppingCartBis);
  }
}

这个测试案例显示了Spring单例模式与纯粹的单例设计模式的主要区别。尽管使用相同的类加载器来加载两个应用程序上下文,但是ShoppingCart的实例是不一样的。但是,当我们比较两次创建并属于相同上下文的实例时,我们认为它们是相等的。

也正因为有了单例,Spring可以控制在每个应用程序上下文中只有一个这样指定的bean的实例可用。因为适配器,Spring可以决定使用由谁来处理JBoss servlet容器中的加载时编织,也可以实现ConfigurableListableBeanFactory的相应实例。第三种设计模式,装饰器,用于向Cache对象添加同步功能,还有Springboot的容器初始化。

其实对于适配器和装饰者确实有太多的相似的地方,一个是运行时选择,一个是加料组合产生新的化学效应,还有从看待事物的角度不同得到不同的行为,适配适配,更注重面向接口的实现,而内部又根据不同情况调用面向一套接口的多套实现的实例的相应方法来实现所要实现的具体功能,装饰者更注重添油加醋,通过组合一些其他对象实例来让自己的功能实现的更加华丽一些(达到1+1>2的这种效果)。一家之言,有更好的理解可以联系我。

原文:Spring框架中的设计模式(四)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,396评论 1 301
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,482评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 94,858评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,131评论 0 179
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,903评论 1 256
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,847评论 1 177
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,454评论 2 273
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,206评论 0 167
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,047评论 6 232
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,563评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,344评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,667评论 1 231
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,264评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,163评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,546评论 3 207
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,630评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,032评论 0 166
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,572评论 2 231
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,668评论 2 232

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,003评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,079评论 6 345
  • 原文链接:http://blog.csdn.net/zhangerqing http://www.cnblogs....
    孤独杂货铺阅读 1,475评论 0 3
  • 一、设计模式的分类 总体来说设计模式分为三大类: 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者...
    lichengjin阅读 840评论 0 8
  • 风沙漠漠,夜正浓。交河饮马鸣长空,石阵不出鬼见愁。呜呜∽ 耳畔但闻冷风啸,长河日落人渺渺。 身披黄沙,手可摘星辰,...
    曲无由阅读 575评论 0 1