In the previous article about my C++ event-planning reconstruction, I looked at the Group model: a surprisingly compact object that connects a collection of Guests with a shared name, shared contact information and some seating-related behaviour.
One of those three pieces of Group state deserves a closer look of its own.
Rather than storing addresses, telephone numbers and email directly inside the Group, the reconstructed architecture places all of that information into a separate Group Contact object.
It is not a large class. It has no inheritance hierarchy, no graphical behaviour and no complicated external relationships. Its job is much narrower: represent the contact information associated with a Group as one coherent value.
That makes it a useful example of something that can easily be overlooked in object-oriented design. Not every worthwhile class represents a large business entity. Sometimes a small value object provides a cleaner architecture precisely because its responsibility is so tightly defined.
A value object rather than another domain entity
The distinction between the Group and its Contact object is important.
The Group has an independent persistent identity. It participates in relationships with Guests and elsewhere in the event model.
The Contact object does not appear to need that kind of identity.
Instead, it behaves much more like a value. It can be constructed, copied, compared and assigned as a complete collection of contact information.
Conceptually:
Group
│
├── Persistent identity
├── Group name
├── Guest membership
│
└── Group Contact
├── Postal address
└── Communication details
If I copy the Contact information, I am interested in copying its values. I am not trying to create a second identity referring to the same conceptual Contact entity.
This difference between entity and value object is one of the cleaner aspects of the reconstructed design.
Ten fields, divided naturally into two responsibilities
The Group Contact currently contains ten pieces of textual information.
Six belong to the postal address:
- primary address line;
- secondary address line;
- city;
- state, province or region;
- postal code;
- country.
The remaining four are communication channels:
- work telephone number;
- home telephone number;
- mobile telephone number;
- email address.
The complete conceptual structure is therefore straightforward:
Group Contact
│
├── Address
│ ├── Address line 1
│ ├── Address line 2
│ ├── City
│ ├── State / province / region
│ ├── Postal code
│ └── Country
│
└── Communications
├── Work phone
├── Home phone
├── Mobile phone
└── Email
There is nothing particularly exotic about these fields. Their architectural value comes from placing them together behind one well-defined abstraction.
Why not simply put these fields inside the Group?
Technically, I could put another ten strings directly into the Group class.
The program would still compile. The data could still be saved. The user could still edit an address.
But the Group would then need to understand all of the mechanics associated with contact data in addition to membership, naming, formatting, seating compatibility and everything else that belongs at Group level.
The dedicated Contact object creates a useful boundary:
Group │ │ has ▼ Group Contact │ │ owns the details of ▼ Address + communication information
The Group can then work with contact information at the level it actually needs: retrieve it, replace it, or ask higher-level questions such as whether an email address or telephone number is available.
The address is deliberately decomposed
The postal address is not stored as one preformatted block of text.
Instead, the reconstructed model keeps its meaningful components separate.
This matters because the same address may need to be used in very different contexts.
A dialog might present separate input boxes. A printed envelope might require several lines. An export format might require city and postal code in independent fields. Sorting or filtering might operate specifically on country or region.
If the entire address were stored only as:
15 Example Street, Apartment 4, Exampletown, Example Region, AB12 3CD, Exampleland
then every consumer wanting the country or postal code separately would need to reverse-engineer the textual formatting.
Keeping the individual components structured avoids that problem.
Two address lines are intentionally preserved
The model provides both a primary and a secondary address line.
That is a pragmatic choice.
Postal addresses do not always fit neatly into “street” plus “house number”. A second line may contain an apartment, building, department, company division, care-of information or another locally meaningful subdivision.
At this stage of the reconstruction I do not want to attach a narrower semantic interpretation than the source supports. The important fact is that the model deliberately preserves two independent address lines rather than compressing everything into one field.
Location remains structured beyond the street address
City, state or region, postal code and country are also independent values.
This makes the data model relatively neutral about national addressing conventions.
Not every country uses “state” in the same way, and not every postal system follows the same ordering rules. Storing the components independently gives presentation and import/export code more freedom to format them appropriately later.
This also reinforces a principle I want to preserve during modernisation: store semantics, format at the boundary.
The underlying data model should ideally know that something is a country or postal code. The user interface or exporter can then decide where and how that value should be displayed.
Telephone numbers are separated by context
The Group Contact distinguishes three telephone categories: work, home and mobile.
Again, these are not represented as one generic list of telephone numbers.
That allows the application to retain the context of each number.
Telephone contact │ ├── Work ├── Home └── Mobile
The Group model above this value object can then answer the more general question: “Does this Group have any usable telephone number?”
That is a good division of responsibility.
The Contact object stores the specific channels. The Group interprets their usefulness within its domain context.
Email remains a separate first-class channel
Email is represented independently from telephone data and postal addressing.
The Group can use that value when determining whether Group-level email contact is available.
This becomes especially interesting when considered alongside the Guest model from two articles ago.
An individual Guest has personal communication information. The Group has its own shared communication information. The application can therefore distinguish between:
Individual Guest contact
│
├──────────────┐
│ │
▼ ▼
personal phone personal email
Group contact
│
├──────────────┐
│ │
▼ ▼
shared phone shared email
This gives the event model more flexibility than assuming every attendee must have independently populated contact details.
It can be created empty
Unlike the Guest, where empty construction is deliberately restricted, the Group Contact supports default construction.
That makes sense for a value object of this kind.
A Group can exist before any contact information is known. The contact structure can start empty and be filled progressively as information becomes available.
This is an example of why construction policy should follow domain semantics rather than one universal coding rule.
An uninitialised Guest identity may be undesirable. An empty Contact record is perfectly meaningful.
It also supports complete construction
At the opposite end, the object can be constructed with its contact information supplied together.
This is useful when contact data already exists as a complete record, for example when loading an event or importing data.
Conceptually, the object supports both workflows:
New Group
│
└── Empty contact
│
└── values added later
Existing/imported Group
│
└── Complete contact data
│
└── constructed together
There is also explicit copy construction, which fits the class's value-oriented role.
Each field can still be accessed independently
Although the Contact object can be treated as one complete value, callers are not forced to replace the entire object just to change one telephone number.
The reconstructed interface exposes read and update operations for the individual contact fields.
That means the same object supports both coarse-grained and fine-grained manipulation:
Group Contact
│
├── replace / copy complete value
│
└── work with individual values
├── address fields
├── location fields
├── telephone fields
└── email
This is a practical API shape for form-based desktop software, where an editor frequently changes one field at a time.
Equality is meaningful for a value object
The class supports comparison between two Contact objects.
This is exactly the kind of operation I expect from a value object.
For an entity such as a Guest or Group, equality can be complicated. Do two Guests represent the same person because they have identical names? Usually not. Persistent identity becomes important.
Contact information is different.
If two Contact values contain the same relevant data, comparing them by value is useful and natural.
Assignment reinforces the same semantics
The Contact object also supports assigning one complete Contact value to another.
Again, that is unsurprising from a C++ perspective, but architecturally it tells me how the type is intended to behave.
If a Group receives updated contact information, the complete value can be replaced cleanly.
There is no indication that the Contact object needs independent persistence identity, external ownership management or complex lifecycle coordination.
That keeps it lightweight.
The Contact object does not need to understand Groups
Another useful characteristic is the direction of dependency.
The Group knows that it has Contact information.
The Contact object does not need to know which Group happens to contain it.
This keeps the lower-level object reusable and prevents an unnecessary circular relationship:
Group
│
│ contains
▼
Group Contact
Group Contact
│
└── does not need
a back-reference to Group
That is a considerably cleaner relationship than making every composed object aware of its parent.
It contains data, not event-planning policy
The other important boundary is what this object does not do.
There is no Guest membership here. No seating logic. No RSVP state. No affinity processing. No graphical position. No table assignment.
Even higher-level questions such as whether the Group as a whole is contactable remain outside this object.
The Contact object's responsibility is much narrower:
Store contact values
│
├── expose them
├── update them
├── copy them
└── compare them
This is a relatively disciplined responsibility boundary.
What I may reconsider during modernisation
As with every reconstructed class, preserving the original behaviour and approving the architecture are two separate decisions.
There are questions I will eventually need to revisit.
For example, all ten values are currently represented as strings. That is perfectly practical and may remain the right choice, but modern validation requirements could eventually justify stronger handling of email addresses, telephone normalisation, countries or postal information.
I also need to avoid imposing modern abstractions merely because they are fashionable. A telephone number is not an integer. A postal code is not necessarily numeric. International addresses vary enormously. In many cases a string is precisely the safest representation.
The current priority therefore remains the same: recover the behaviour accurately first, understand the design, and modernise only where there is a concrete engineering benefit.
Putting the complete Group Contact together
The reconstructed object can be summarised conceptually as follows:
Group Contact
│
├── postal information
│ ├── primary address
│ ├── secondary address
│ ├── city
│ ├── state / province / region
│ ├── postal code
│ └── country
│
├── communication information
│ ├── work phone
│ ├── home phone
│ ├── mobile phone
│ └── email
│
└── value behaviour
├── create empty
├── create from complete values
├── copy
├── assign
├── compare
├── read individual fields
└── update individual fields
A small class with a very clear reason to exist
This is probably one of the least dramatic classes in the project.
There is no optimisation algorithm hiding inside it. It does not draw anything on screen. It does not manipulate seating arrangements.
And that is precisely why I find it useful architecturally.
Its purpose is easy to state:
represent the shared contact information belonging to a Group.
It owns the values required for that responsibility, provides the operations needed to work with those values, and leaves higher-level event behaviour elsewhere.
After reconstructing several considerably larger classes, it is useful to encounter an object where the responsibility boundary is this clear.
Together with the Guest and Group models, this gives me another piece of the event domain: individual participants have their own contact channels, Groups can have shared contact information, and the wider Event Document coordinates the relationships between those objects.
The architecture is becoming less of a collection of recovered C++ functions and increasingly a model that can be explained in business terms.
Comments
Post a Comment