如何克隆旧构建器以创建新的构建器对象?
我有一个我正在我的项目中使用的构建器类。
- 假设我将
metricA
作为基于以下类的构建器。 - 我需要通过克隆
metricA
来制定基于metricA
的新构建器metricB
,以便metricB
包含metricB
中已经存在的所有值。
在MetricHolder
的构造函数中,我正在基于已经设置的字段初始化一些字段(不是直接设置)。
-
clientTypeOrPayId
– 我正在初始化此字段。 如果payId
存在,那么我将设置此值或我将设置clientType
。 -
clientKey
– 我也在同一个构造函数中初始化这个字段。 - 最重要的是,我在
clientPayload
地图中放置了几个必填字段。 我不确定这样做的正确方法是什么。 但我需要在地图中添加is_clientid
和is_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
的范围,因此您不能在构造函数中意外地重用它。