如何在构造函数中仅初始化一次变量?

4

我有一个建造者模式,在其中我从客户那里获取一些参数,根据这些参数构建我的建造者类,然后将该建造者类传递给我们的基础库,然后我的库将使用它。

public final class KeyHolder {
  private final String clientId;
  private final String deviceId;
  private final int processId;
  private final Cache<String, List<Response>> userCache;
  private static final long MAXIMUM_CACHE_SIZE = 5000000;
  private static final long EXPIRE_AFTER_WRITE = 120; // this is in seconds

  private KeyHolder(Builder builder) {
    this.clientId = builder.clientId;
    this.deviceId = builder.deviceId;
    this.processId = builder.processId;
    this.maximumCacheSize = builder.maximumCacheSize;
    this.expireAfterWrite = builder.expireAfterWrite;

    // how to execute this line only once
    this.userCache =
        CacheBuilder
            .newBuilder()
            .maximumSize(maximumCacheSize)
            .expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
            .removalListener(
                RemovalListeners.asynchronous(new CustomListener(),
                    Executors.newSingleThreadScheduledExecutor())).build();

  }

  public static class Builder {
    protected final int processId;
    protected String clientId = null;
    protected String deviceId = null;
    protected long maximumCacheSize = MAXIMUM_CACHE_SIZE;
    protected long expireAfterWrite = EXPIRE_AFTER_WRITE;


    public Builder(int processId) {
      this.processId = processId;
    }

    public Builder setClientId(String clientId) {
      this.clientId = clientId;
      return this;
    }

    public Builder setDeviceId(String deviceId) {
      this.deviceId = deviceId;
      return this;
    }

    public Builder setMaximumCacheSize(long size) {
      this.maximumCacheSize = size;
      return this;
    }

    public Builder setExpiryTimeAfterWrite(long duration) {
      this.expireAfterWrite = duration;
      return this;
    }

    public KeyHolder build() {
      return new KeyHolder(this);
    }
  }

 // getters here
}

每次调用我们的库时,他们都会创建一个新的 KeyHolder 构建器类,并将其传递给我们的库。每次调用时,processIdclientIddeviceId 都会更改,但是 maximumCacheSizeexpireAfterWrite 将会保持不变。如上所述,我在这里使用了 guava 缓存,但由于他们每次都创建 KeyHolder 构建器类,我该如何确保以下行仅在我的构造函数中执行一次?

    this.userCache =
        CacheBuilder
            .newBuilder()
            .maximumSize(maximumCacheSize)
            .expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
            .removalListener(
                RemovalListeners.asynchronous(new CustomListener(),
                    Executors.newSingleThreadScheduledExecutor())).build();

由于当前代码,每次调用都会执行,我的库中将获得一个新的Guava缓存,因此之前使用此Guava缓存在我的库中缓存的任何条目都将丢失。

如何仅初始化特定变量一次,并在此后忽略传递给它的任何值?

更新:

public class DataClient implements Client {
    private final ExecutorService executor = Executors.newFixedThreadPool(10);

    // for synchronous call
    @Override
    public List<Response> executeSync(KeyHolder key) {
        Cache<String, List<Response>> userCache = key.getUserCache();
        List<Response> response = userCache.getIfPresent(key.getUUID());
        if (CollectionUtils.isNotEmpty(response)) {
          return response;
        }
        // if not in cache, then normally call the flow and populate the cache
        List<Response> dataResponse = null;
        Future<List<Response>> future = null;
        try {
            future = executeAsync(key);
            dataResponse = future.get(key.getTimeout(), TimeUnit.MILLISECONDS);
            userCache.put(key.getUUID(), dataResponse);
        } catch (TimeoutException ex) {
            // log error and return DataResponse
        } catch (Exception ex) {
            // log error and return DataResponse
        }

        return dataResponse;
    }
}

我有点困惑,它已经是“final”了,那么你所说的“忽略传递给它的任何值”是什么意思?你不能重新初始化一个“final”变量。你能详细说明一下吗? - Dioxin
@VinceEmigh "它"(即userCache)是OP代码中的实例变量,而不是静态变量。 - boxed__l
@VinceEmigh 我的使用情况是,每次客户端调用时都会创建一个 KeyHolder 类,并通过传递此类来调用我们的库。现在我的库使用此构建器类,并根据传递的参数执行额外的操作。我已编辑我的问题以更清楚地阐明这一点。现在由于我在这里使用 guava 缓存,我想根据客户端传递的值初始化我的 guava 缓存,但我只想初始化一次。然后我在我的库中使用此 guava 缓存,如上面的代码所示。 - john
我的观点是,在 OP 的代码中,userCache 是基于实例的和 final 的,因此他可以创建该类的多个对象,并且每个对象都有不同的 userCache,它们不是共享的。现在为了纠正这个问题,他可以使用 enum/使变量 static/使用专用类或工厂,这些都是实现细节。在我看来,需要改动最少的是“static”方法,但是否在这种情况下是理想的则有争议。 - boxed__l
@Abhijith,请查看我的答案,希望它能够澄清问题。问题更深层次,使用static会加剧问题。 KeyHolder没有理由与响应缓存耦合在一起。它仅被耦合在一起,以便人们可以通过KeyHolder指定缓存设置,这是不合逻辑的并且创建了紧密的耦合。 - Dioxin
显示剩余2条评论
3个回答

2
如果您只想设置缓存一次,为什么每个KeuHolder对象都要尝试构建它?事实上,即使KeyHolder#Builder也公开了帮助构建缓存的方法,这些方法只有一次有用。
这是非常可疑的。如果第一个KeyHolder没有指定缓存详细信息怎么办?我的意思是,它不是被强制执行的(您没有正确使用构建器模式,在结尾处会更多地讨论)。
解决此问题的第一步是确保在开始创建KeyHolder对象之前设置缓存。您可以通过创建静态工厂并使userCache静态来实现此目的:
class KeyHolder {
    private static Map<String, List<Response>> userCache;

    public static KeyHolder.Builder newBuilder(int id) {
        if(userCache == null) {
            userCache = ...;
        }

        return new Builder(id);
    }
}

但是正如你从我的评论中可能读到的那样,这只是解决问题的权宜之计。每次我们想创建一个新的KeyHolder时,都会检查用户缓存,这是不必要的。

相反,您应该完全将缓存与KeyHolder解耦。它为什么需要知道缓存呢?

您的缓存应该放在DataClient中:

class DataClient {
    private Map<String, List<Response>> userCache;

    public List<Response> executeSync(KeyHolder key) {
        List<Response> response = userCache.getIfPresent(key.getUUID());
        //...
    }
}

您可以通过 DataClient 构造函数接受设置,或者将预先指定的缓存传递到 DataClient 中。
至于您使用建造者模式的方式,请记住我们使用它的原因:Java 缺少可选参数。
这就是为什么建造者很常见的原因:它们允许我们通过方法指定可选数据。
您正在将关键信息(例如缓存设置)作为可选参数(建造者方法)进行指定。如果不需要该信息,则应只使用建造者方法,并且缓存信息绝对是必需的。我会质疑 deviceIdclientId 有多么可选,因为唯一需要提供的数据是 productId

在阅读了所有的评论和答案后,我发现缓存只属于DataClient类。现在,如果我采用这种方法,如何在DataClient类中仅初始化缓存一次呢?客户通常会像这样调用我们的库:DataResponse response = DataClientFactory.getInstance().executeSync(key);,然后调用将传递到DataClient类的executeSync方法中。 - john
我需要允许客户使用KeyHolder类传递这两个值maximumCacheSizeexpireAfterWrite,因为这是我的要求。如果他们没有传递,我已经有了这两个参数的默认值,所以我如何从KeyHolder类中使用这两个值,并且只初始化一次DataClient中的缓存呢? - john
这就是为什么单例模式可能会带来麻烦。按照当前的状态,你必须暴露一个 setCache(...) 并实现策略模式,以便可以在非缓存的 executeSync 算法和缓存算法之间进行切换。或者,如果您不需要动态设置,可以通过 IO 加载它们,允许客户端在设置文件中指定它们。从我所看到的最后两个选项是硬编码设置(不好)或使用 DI 来避免需要单例。 - Dioxin
似乎你的上司们试图过于简化接口,这是我能想到的唯一需要这样做的原因。这样做只会带来伤害并使代码混乱。KeyHolder 只应负责保管密钥 (SRP)。但如果这已经不在你的掌控之中,那么你应该坚持使用静态工厂方法的方式,避免创建另一个单例。如果你被允许的话,就将 Builder 设为私有,并强制客户端调用 newKeyHolder 。如果他们不允许,那么你在工作中会面临很大的压力,因为你正在与面向对象编程哲学对抗。 - Dioxin
让我们在聊天中继续这个讨论 - Dioxin
显示剩余2条评论

0

首先,将其设为静态

private static Cache<String, List<Response>> userCache;

如果它还没有被初始化,那么就进行初始化。

private KeyHolder(Builder builder) {
    ...

    if (userCache == null) {
        userCache = CacheBuilder
            .newBuilder()
            .maximumSize(maximumCacheSize)
            .expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
            .removalListener(
                RemovalListeners.asynchronous(new CustomListener(), Executors.newSingleThreadScheduledExecutor())
            ).build();
    }
}

如果您在运行时不需要缓存进行自定义(通过使用传递的参数),我建议选择类似以下的东西:
// initialize the cache while defining it
// replace maximumCacheSize and expireAfterWrite with constants
private static final Cache... = CacheBuilder.newBuilder()...;

并将其从构造函数中删除。


@HaifengZhang 静态在哪里? - Tyler Sebastian
把它设为final并急切地初始化有什么问题吗? - Lew Bloch
@VinceEmigh userCache 不是 final。空值检查在类第一次实例化时为真,在每次之后为假。不过我确实有一个打字错误,已经纠正了。 - Tyler Sebastian
@LewBloch 没有什么,但我认为他/她想要对初始的userCache有一些控制(构建器使用了一些传递进来的参数)。 - Tyler Sebastian
@TylerSebastian,它不起作用...我试过了。我更新了我的问题,添加了更多细节,说明这个KeyHolder类将如何使用。首先,我在缓存中查找,如果存在,则从缓存中返回。否则,我执行正常流程并填充缓存。但是,不知何故,在第一次填充缓存后,使用您的建议进行第二次调用时,我的相同缓存为空? - john
显示剩余7条评论

0

如果变量为null,您可以将其设置为静态变量,并仅在第一次调用时进行初始化。就像这样:

public final class KeyHolder {
  private final String clientId;
  private final String deviceId;
  private final int processId;

  //this var is now static, so it is shared across all instances
  private static Cache<String, List<Response>> userCache = null;  

  private static final long MAXIMUM_CACHE_SIZE = 5000000;
  private static final long EXPIRE_AFTER_WRITE = 120; // this is in seconds

  private KeyHolder(Builder builder) {
    this.clientId = builder.clientId;
    this.deviceId = builder.deviceId;
    this.processId = builder.processId;
    this.maximumCacheSize = builder.maximumCacheSize;
    this.expireAfterWrite = builder.expireAfterWrite;

    //this will be executed only the first time, when the var is null
    if (userCache == null) {
      userCache =
        CacheBuilder
            .newBuilder()
            .maximumSize(maximumCacheSize)
            .expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
            .removalListener(
                RemovalListeners.asynchronous(new CustomListener(),
                    Executors.newSingleThreadScheduledExecutor())).build();

  }

  //rest of your class below

同意,这将是使用单例的解决方案。只是缺少最后一个括号。 - RAZ_Muh_Taz
@VinceEmigh 不会,因为这个变量是静态的。当对象的第一个实例被创建时,静态变量将为null。此后每次都会有一个值。在这里阅读有关static关键字的工作原理的更多信息:https://dev59.com/8HRC5IYBdhLWcg3wFdBx,但基本上它意味着该变量将在类的所有实例之间共享。 - nhouser9
我知道static的含义,只是没有意识到你使用它。你在构造函数中初始化了一个static引用变量,这是有问题的(它属于类,但对象负责维护其生命周期,而不是类?)。你删除了final以允许这样做。这是一个巨大的代码气味,单例模式可以像这样工作,而不会产生矛盾的哲学。但即使如此,单例模式也只是对实际问题的一种权宜之计,因为单例模式会暴露过多。再次声明,我没有投反对票,只是想为他们为什么这样做提供一些可能性。 - Dioxin
@nhouser9 我已经更新了我的问题,添加了更多关于KeyHolder类将如何使用的细节。首先我会在缓存中查找,如果存在则从缓存中返回。否则,我执行正常流程并填充缓存。但是不知何故,在第一次填充缓存后,使用您的建议进行第二次调用时,我的同样的缓存为空? - john

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接