Skip to main content

TOMS - Inside the Group Contact Model: Keeping Shared Contact Data as a Proper Value Object

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.

The Group Contact object does not represent a person. It represents the shared postal and communication information associated with a Group.

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.

Composition allows the Group to have contact information without forcing the Group class itself to become the implementation of contact information.

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.

Entities are normally distinguished by identity. Value objects are normally understood through the values they contain. The reconstructed architecture reflects that distinction here.

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.

Stronger typing is useful only when the stronger type accurately represents the domain. Replacing every string with a specialised object would not automatically make the design better.

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.

Good object-oriented design is not measured by how sophisticated every class is. Sometimes the cleanest class in an architecture is the one that knows exactly ten pieces of information and nothing about the rest of the application.

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.

AI Assistance Disclosure: This article was developed from my original software reconstruction, generated technical documentation and personal professional experience with the assistance of generative AI. AI was used to help organise, expand and refine the text and clarify certain technical distinctions. The reconstruction work, engineering decisions, experience, opinions and conclusions remain my own. Identifying implementation details and internal naming conventions have been omitted.

Comments

Popular posts from this blog

Movies - The Bubble (2022)

  Back to Evolution (2001) .

IT - Fixing Windows Error 1327: Account Restrictions Are Preventing This User from Signing In

Fixing Windows Error 1327: Account Restrictions Are Preventing This User from Signing In Introduction Error 1327, “Account restrictions are preventing this user from signing in,” is a perplexing and disruptive issue that occurs on some Windows 10 and Windows 11 machines. The message typically appears at login or while connecting to remote resources, like shared folders, network drives, or remote desktops. Table of Contents Symptoms of Error 1327 Common Causes Step-by-Step Troubleshooting Advanced Fixes Automation via PowerShell Prevention Tips Further Reading Symptoms of Error 1327 Users experiencing this error may encounter one or more of the following: Login screen fails after credentials are entered. Error message appears when accessing mapped drives or network resources. Remote Desktop Connection (RDP) is rejected with the 1327 message. Group Policy logon restrictions silently block access. Co...

ATA Drive Capacity Limitations

ATA interface versions up through ATA-5 suffered from a drive capacity limitation of about 137GB (billion bytes). Depending on the BIOS used, you can further reduce this limitation to 8.4GB, or even as low as 528MB (million bytes). This is due to limitations in both the BIOS and the ATA interface, which when combined create even further limitations. To understand these limits, you have to look at the BIOS (software) and ATA (hardware) interfaces together. NOTE In addition to the BIOS/ATA limitations discussed in this section, various operating system limitations exist. These are described later in this chapter. The limitations when dealing with ATA drives are those of the ATA interface as well as the BIOS interface used to talk to the drive. A summary of the limitations is shown in Table 7.12. Table 7.12. ATA/IDE Capacity Limitations for Various Sector Addressing Methods Sector Addressing Method Total Sectors Calculation Maximum Total Sectors Maximum Capacity (Byte...