public abstract class EMessageObject extends Object implements Serializable
EMessage and EField. Message instances are
used to publish notifications, requests, and replies. Field
instances are used to implement user-defined fields within
both messages and other fields.
Messages and field contain public final data members.
If the message or field can be transmitted between eBus
applications, then the data member type must be one of the
following:
boolean Boolean BigInteger BigDecimal byte Byte char Character
Class Date double Double Duration EField EFieldList EMessage
EMessageList EMessageKey enum File float Float InetAddress
InetSocketAddress Instant int Integer LocalDate LocalDateTime
LocalTime long Long MonthDay OffsetDateTime OffsetTime Period
short Short String URI YearMonth ZonedDateTime ZoneId ZoneOffset
Any eBus field can be made into a homogenous array by
appending [] to the field name.
This data type restriction does not apply if the
message or field is marked as local only.
In that case the data members may be any valid Java type.
Also the following De-serialization requirements do not apply.
However, local only message and fields may not appear in a
non-local message or field.
As of eBus release 5.2.0, eBus determines field member
serialization order based on the field's length. The goal
being to maintain correct by alignment. Because serialization
order can no longer be determined up front, EMessage
and EField instances must be re-created using a
technique not based on field ordering. This is no done using
the GoF builder paradigm.
Consider the following eBus notification message:
public final class EquityQuote
extends ENotificationMessage
implements Serializable
{
// Member fields.
public final PriceType priceType; // PriceType is an enum
public final PriceSize bid; // PriceSize is an EField subclass
public final PriceSize ask;
private static final long serialVersionUID = 1L; // Defined for Serializable
// Remaining code will be added by each step.
}
Every non-local EMessage and EField must
define a public static builder method which returns a
builder instance associated with the message or field.
The builder class name is left up to the developer. In this
example the class is simply Builder:
public static Builder builder() {
return (new Builder());
}
It is recommended that this builder method be the only
way to create a EquityQuote.Builder instance.
public static final class Builder
extends ENotificationMessage.Builder<EquityQuote, Builder>
{
// This code filled in by the following steps.
}
The builder class must extend the target message's
base class builder. In this example, since EquityQuote
extends ENotificationMessage, Builder must
extend ENotificationMessage.Builder. If the message
extends request or reply, then the builder would be required
to extend the request or reply builder, respectively.
ENotification.Builder generic class parameters
M and B (which all super class Builder classes
define) are the target message class and the target builder
class, respectively. The first is needed for the superclass
public final M build() method used to create the
target message instance. The second generic parameter is used
by super class setter methods (set Step 5 for more about
setters).
The builder inner class must be public static because
classes outside this package will need to access the class and
static because the builder instance must in no way be
dependent on the target message class containing it.
private PriceType mPriceType;
private PriceSize bid;
private PriceSize ask;
The builder class must have exactly the same number of fields
as the target message with the same data types except
this time these fields are private, non-final.
private because only the encapsulating target class is
allowed to access these fields. Non-final because the
field values will be set post-construction.
A builder should have only one private, no argument
constructor because only EquityQuote builder method
should be allowed to construct this builder:
private Builder() {
// Superclass constructor requires target class.
super (EquityQuote.class);
// Set fields to defaults if necessary.
}
For each public final target message field the builder
must define a setter method whose name
exactly matches the field name and whose sole
parameter has exactly matches the field data type.
The setter return type is the builder class which allows for
setter method chaining:
builder.height(1).weight(2).age(3). This is the reason
for the second class parameter B. If a message field
is defined in the superclass, then the superclass builder is
responsible for defining the setter method. But if the
superclass setter returns the superclass type, then setter
chain breaks. The solution is for superclass setters to return
type B. While this requires a downcast it will work as
long as B was set correctly.
In this example EquityQuote.Builder has the following
setters:
public Builder priceType(final PriceType pt) { ... };
public Builder bid(final PriceSize ps) { ... };
public Builder ask(final PriceSize ps) { ... };
How the setter is defined depends on what argument validation is required. If no validation is done, then the setter can set the field value and return. If validation is done, then an invalid argument should result in a thrown exception.
public Builder bid(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mBid = ps;
return (this);
}
If validation is performed then that validation must be independent of the other data members and focus on the setter argument but itself. This is due to the unknown ordering of the setter method calls. When a setter is called, there is no way of knowing which builder fields are set, if any. If inter-field validation is required, then see Step 6 to learn how a builder can implement this requirement.
Before the target EMessage or EField is
instantiated, builder member fields may be checked for
correctness. Application code has finished calling the setters
and called EMessageObject.Builder.build(). This method
then calls EMessageObject.Builder.validate(List) to
determine if the builder is correctly configured. The
validate method should not throw an exception
but rather adds text to the problems list explaining
the problems found with the builder configuration. Upon return
an empty list means the builder is properly configured and the
target message can now be built. A non-empty problems list
results in a ValidationException being thrown which
contains the problems list.
When overridding validate use a series of if
statements for each check rather than if/else if
chain. The point here is that all builder configuration
problems should be reported rather than just the first one.
This way the developer will be able to correct all the
problems at one time rather than uncovering the errors one
at a time. Finally, the first line in validate should
be super.validate(problems); to allow the superclass
to perform its own validation.
@Override public void validate(final List<String> problems) {
super.validate(problems);
if (mPriceType == null) { problems.add("priceType not set"); }
// One-sided bids are allowed.
// Note: inter-field comparison may now be performed.
if (mBid == null && mAsk == null) { problems.add("bid and ask both not set"); }
// Bid price must be less than ask price. PriceSize implements Comparable.
// Be sure this is a two-sided quote.
if (mBid != null && mAsk != null && mBid.compareTo(mAsk) ≥ 0) {
problems.add("bid ≥ ask");
}
}
The abstract method the builder class is required to override
is buildImpl. This class returns the target class
instance built from the configured fields.
@Override protected EquityQuote buildImpl() {
return (new EquityQuote(this));
}
buildImpl is called after validate shows no
problems with the builder configuration.
The final link connecting the target class and target class builder is the target class constructor:
private EquityQuote(final Builder builder) {
super (builder);
this.priceType = builder.mPriceType;
this.bid = builder.mBid;
this.ask = builder.mAsk;
}
This constructor is private because only
EquityQuote.Builder.buildImpl has access to this
constructor. Also note that since Builder is an
EquityQuote inner class, the constructor can access
Builder member fields directly, no getter methods are
required.
public final class EquityQuote
extends ENotificationMessage
implements Serializable
{
public final PriceType priceType;
public final PriceSize bid;
public final PriceSize ask;
private static final long serialVersionUID = 1L;
private EquityQuote(final Builder builder) {
super (builder);
this.priceType = builder.mPriceType;
this.bid = builder.mBid;
this.ask = builder.mAsk;
}
public static Builder builder() {
return (new Builder());
}
public static final class Builder
extends ENotificationMessage.Builder<EquityQuote, Builder>
{
private PriceType mPriceType;
private PriceSize mBid;
private PriceSize mAsk;
private Builder() {
super (EquityQuote.class);
}
public Builder priceType(final PriceType pt) {
if (pt == null) {
throw (new NullPointerException("pt is null"));
}
mPriceType = pt;
return (this);
}
public Builder bid(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mBid = ps;
return (this);
}
public Builder ask(final PriceSize ps) {
if (ps == null) {
throw (new NullPointerException("ps is null"));
}
mAsk = ps;
return (this);
}
@Override public void validate(final List<String> problems) {
super.validate(problems);
if (mPriceType == null) { problems.add("priceType not set"); }
if (mBid == null && mAsk == null) { problems.add("bid and ask not set"); }
if (mBid != null && mAsk != null && mBid.compareTo(mAsk) ≥ 0) {
problems.add("bid ≥ ask");
}
}
@Override protected EquityQuote buildImpl() {
return (new EquityQuote(this));
}
}
}
The above example uses a final message class. But what
if the EMessage or EField class may be
extended? See EField for an example of how to set up
builders for a non-final class.
| Modifier and Type | Class and Description |
|---|---|
static class |
EMessageObject.Builder<M extends EMessageObject,B extends EMessageObject.Builder<M,?>>
Base class for all
EMessageObject builders. |
| Modifier and Type | Field and Description |
|---|---|
static int |
MAX_FIELDS
A message object may have at most 31 fields.
|
| Modifier | Constructor and Description |
|---|---|
protected |
EMessageObject()
Creates a new message object instance.
|
protected |
EMessageObject(EMessageObject.Builder<?,?> builder) |
public static final int MAX_FIELDS
protected EMessageObject()
throws InvalidMessageException
InvalidMessageException - if the user-defined message or field is invalid.protected EMessageObject(EMessageObject.Builder<?,?> builder)
Copyright © 2019. All rights reserved.