如何克隆旧构建器以创建新的构建器对象?

我有一个我正在我的项目中使用的构建器类。

  • 假设我将metricA作为基于以下类的构建器。
  • 我需要通过克隆metricA来制定基于metricA的新构建器metricB ,以便metricB包含metricB中已经存在的所有值。

MetricHolder的构造函数中,我正在基于已经设置的字段初始化一些字段(不是直接设置)。

  • clientTypeOrPayId – 我正在初始化此字段。 如果payId存在,那么我将设置此值或我将设置clientType
  • clientKey – 我也在同一个构造函数中初始化这个字段。
  • 最重要的是,我在clientPayload地图中放置了几个必填字段。 我不确定这样做的正确方法是什么。 但我需要在地图中添加is_clientidis_deviceid 。 (一般来说,我添加了更多的字段)。
  • 然后在构造函数的最后一个,我计算延迟差异并将其发送到其他系统。

以下是我的课程:

 public final class MetricHolder { private final String clientId; private final String deviceId; private final String payId; private final String clientType; private final String clientTypeOrPayId; private final Schema schema; private final String schemaId; private final String clientKey; private final Map clientPayload; private final Record record; private final long clientCreateTimestamp; private final long clientSentTimestamp; private MetricHolder(Builder builder) { this.payId = builder.payId; this.siteId = builder.siteId; this.clientType = builder.clientType; this.clientId = builder.clientId; this.deviceId = builder.deviceId; this.schema = builder.schema; this.schemaId = builder.schemaId; // populating all the required fields in the map and make it immutable // not sure whether this is right? builder.clientPayload.put("is_clientid", (clientId == null) ? "false" : "true"); builder.clientPayload.put("is_deviceid", (clientId == null) ? "true" : "false"); this.clientPayload = Collections.unmodifiableMap(builder.clientPayload); this.clientTypeOrPayId = Strings.isNullOrEmpty(payId) ? clientType : payId; this.record = builder.record; this.clientKey = "process:" + System.currentTimeMillis() + ":" + ((clientId == null) ? deviceId : clientId); this.clientCreateTimestamp = builder.clientCreateTimestamp; this.clientSentTimestamp = builder.clientSentTimestamp; // this will be called twice while cloning // what is the right way to do this then? SendData.getInstance().insert(clientTypeOrPayId, System.currentTimeMillis() - clientCreateTimestamp); SendData.getInstance().insert(clientTypeOrPayId, System.currentTimeMillis() - clientSentTimestamp); } public static class Builder { private final Record record; private Schema schema; private String schemaId; private String clientId; private String deviceId; private String payId; private String clientType; private Map clientPayload; private long clientCreateTimestamp; private long clientSentTimestamp; // this is for cloning public Builder(MetricHolder packet) { this.record = packet.record; this.schema = packet.schema; this.schemaId = packet.schemaId; this.clientId = packet.clientId; this.deviceId = packet.deviceId; this.payId = packet.payId; this.clientType = packet.clientType; // make a new map and check whether mandatory fields are present already or not // and if they are present don't add it again. this.clientPayload = new HashMap(); for (Map.Entry entry : packet.clientPayload.entrySet()) { if (!("is_clientid".equals(entry.getKey()) || "is_deviceid".equals(entry.getKey())) { this.clientPayload.put(entry.getKey(), entry.getValue()); } } this.clientCreateTimestamp = packet.clientCreateTimestamp; this.clientSentTimestamp = packet.clientSentTimestamp; } public Builder(Record record) { this.record = record; } public Builder setSchema(Schema schema) { this.schema = schema; return this; } public Builder setSchemaId(String schemaId) { this.schemaId = schemaId; return this; } public Builder setClientId(String clientId) { this.clientId = clientId; return this; } public Builder setDeviceId(String deviceId) { this.deviceId = deviceId; return this; } public Builder setPayId(String payId) { this.payId = payId; return this; } public Builder setClientType(String clientType) { this.clientType = clientType; return this; } public Builder setClientPayload(Map payload) { this.clientPayload = payload; return this; } public Builder setClientCreateTimestamp(long clientCreateTimestamp) { this.clientCreateTimestamp = clientCreateTimestamp; return this; } public Builder setClientSentTimestamp(long clientSentTimestamp) { this.clientSentTimestamp = clientSentTimestamp; return this; } public MetricHolder build() { return new MetricHolder(this); } } // getters } 

题:-

下面是我如何制作metricA builder对象:

 MetricHolder metricA = new MetricHolder.Builder(record).setClientId("123456").setDeviceId("abcdefhg") . setPayId("98765").setClientPayload(payloadMapHolder).setClientCreateTimestamp(createTimestamp) .setClientSentTimestamp(sentTimestamp).build(); 

现在,当我获得所有其他字段时,这就是我稍后在代码中克隆metricA对象的方法,如下所示:

 MetricHolder metricB = new MetricHolder.Builder(metricA).setSchema(schema).setSchemaId("345").build(); 

我现在看到两个问题:

  • 首先, MetricHolder构造函数中的SendData.getInstance()行将被调用两次。 首先是当我通过克隆metricA制作metricB时,我制作metricA 。 但是当我尝试创建metricA builder对象时,我只想调用它一次 ? 我怎样才能做到这一点?
  • 其次,我在MetricHolder构造函数中使用两个必填字段填充clientPayload映射的方式对我来说clientPayload 。 有没有其他更好的方法来做同样的事情?

我想整个问题都在发生,因为我克隆metricA的方式来制作一个metricB构建器对象? 做这个的最好方式是什么? 我希望以正确的方式实现上述两件事。

但是,当我尝试创建metricA builder对象时,我只想调用它一次? 我怎样才能做到这一点?

最直接的方法是在构建器中有一个标志,指示它是由Record创建还是通过克隆创建:

 class Builder { final boolean cloned; Builder(MetricHolder packet) { this.cloned = true; // ... } Builder(Record record) { this.cloned = false; // ... } } 

然后,在MetricHolder的构造函数中:

 if (!builder.cloned) { SendData.getInstance().whatever(); } 

但值得指出的是,调用SendData是在构造函数中做太多工作的一个例子。 您应该仔细考虑是否确实要在构造函数中进行此调用,或者是否可以将其分解为另一种方法。

其次,我在MetricHolder构造函数中使用两个必填字段填充clientPayload映射的方式对我来说不合适。 有没有其他更好的方法来做同样的事情?

你误解了使用Collections.unmodifiableMap的“不可修改”的一点:它只是map参数的一个不可修改的视图 ; 你仍然可以修改底层地图。

这是一个JUnit测试来演示:

 Map original = new HashMap<>(); original.put("hello", "world"); // Obviously false, we just put something into it. assertFalse(original.isEmpty()); Map unmodifiable = Collections.unmodifiableMap(original); // We didn't modify the original, so we don't expect this to have changed. assertFalse(original.isEmpty()); // We expect this to be the same as for the original. assertFalse(unmodifiable.isEmpty()); try { unmodifiable.clear(); fail("Expected this to fail, as it's unmodifiable"); } catch (UnsupportedOperationException expected) {} // Yep, still the same contents. assertFalse(original.isEmpty()); assertFalse(unmodifiable.isEmpty()); // But here's where it gets sticky - no exception is thrown. original.clear(); // Yep, we expect this... assertTrue(original.isEmpty()); // But - uh-oh - the unmodifiable map has changed! assertTrue(unmodifiable.isEmpty()); 

问题是,如果地图上没有其他参考,地图只能是不可修改的:如果你没有对original的引用,那么unmodifiable实际上是不可修改的; 否则,你不能依赖地图永远不会改变。

在您的特定情况下,您只需将clientPayload映射包装在不可修改的集合中。 因此,您将覆盖先前构造的实例的值。

例如:

 MetricHolder.Builder builder = new MetricHolder.Builder(); MetricHolder first = builder.build(); assertEquals("false", first.clientPayload.get("is_clientid")); assertEquals("true", first.clientPayload.get("is_deviceid")); builder.setClientId("").build(); // Hmm, first has changed. assertEquals("true", first.clientPayload.get("is_clientid")); assertEquals("false", first.clientPayload.get("is_deviceid")); 

正确的方法是不包装builder.clientPayload 。 获取地图的副本,修改它,然后用unmodifiableMap包装:

 { Map copyOfClientPayload = new HashMap<>(builder.clientPayload); copyOfClientPayload.put("is_clientid", (clientId == null) ? "false" : "true"); copyOfClientPayload.put("is_deviceid", (clientId == null) ? "true" : "false"); this.clientPayload = Collections.unmodifiableMap(copyOfClientPayload); } 

周围的{}并不是绝对必要的,但它们限制了copyOfClientPayload的范围,因此您不能在构造函数中意外地重用它。