Give your collections a purpose and a connection to the real world

TL;DR: Wrap primitive collections into dedicated objects to ensure type safety and encapsulate business logic.

Problems Addressed in this Article 😔

Context 💬

You find yourself passing around generic lists, arrays, or dictionaries as if they were just anemic "bags of data." like DTOs or Data Clumps.

These primitive structures are convenient to iterate.

But they are also anonymous and lack a voice in the business domain.

When you use a raw array to represent a group of specific entities—like ActiveSubscribersPendingInvoices, or ValidationErrors, you are essentially forcing every part of your system to re-learn how to handle that collection, leading to scattered logic and "primitive obsession."

When you reify the collection, you improve the model and create technical implementation into a first-class citizen of your domain model.

This doesn't just provide a home for validation and filtering; it makes the invisible concepts in your business requirements visible in your code.

Steps 👣

  1. Create a new class to represent the specific collection.
  2. Define a private collection property within this class using the appropriate collection type.
  3. Implement a constructor that accepts only elements of the required type.
  4. Add type-hinted methods to add, remove, or retrieve elements.
  5. Move collection-specific logic (like sorting or filtering) from the outside into this new class.

Sample Code 💻

Before 🚨

<?

/** @var User[] $users */
// this is a static declaration used by many IDEs but not the compiler
// Like many comments it is useless, and possible outdated

function notifyUsers(array $users) {
    foreach ($users as $user) {
        // You have no guarantee $user is actually a User object
        // The comment above is 
        // just a hint for the IDE/Static Analysis
        $user->sendNotification();
    }
}

$users = [new User('Anatoli Bugorski'), new Product('Laser')]; 
// This array is anemic and lacks runtime type enforcement
// There's a Product in the collection and will show a fatal error
// unless it can understand #sendNotification() method

notifyUsers($users);

After 👉

<? 

class UserDirectory {
// 1. Create a new class to represent the specific collection
// This is a real world concept reified  
// 2. Define a private property
private array $elements = [];

    // 3. Implement a constructor that accepts only User types
    public function __construct(User ...$users) {
        $this->elements = $users;
    }

    // 4. Add type-hinted methods to add elements
    public function add(User $user): void {
        $this->elements[] = $user;
    }

    // 5. Move collection-specific logic inside
    public function notifyAll(): void {
        foreach ($this->elements as $user) {
            $user->sendNotification();
        }
    }
}

Type 📝

[X] Manual

Safety Considerations 🛡️

This refactoring is very safe.

You create a new structure and gradually migrate references.

Since you add strict type hints in the new class, the compiler engine catches any incompatible data at runtime, preventing silent failures.

Why is the Code Better? ✨

You transform a generic, "dumb" collection into a specialized object that understands its own rules.

You stop repeating validation logic every time you handle the list.

The code becomes self-documenting because the class name explicitly tells you what the collection contains.

How Does it Improve the Bijection? 🗺️

In the real world, a "List of Users" or a "Staff Directory" is a distinct concept with specific behaviors.

An anonymous array is a technical implementation detail, not a real-world entity.

By reifying the collection, you create a one-to-one correspondence between the business concept and your code.

Limitations ⚠️

You might encounter slight performance overhead when dealing with millions of objects compared to raw arrays.

For most business applications, the safety gains far outweigh the millisecond costs and prevents you from being a premature optimizator.

Remember to avoid hollow specialized business collections that don't exist in the real world.

Many languages support typed collections:

In all the above cases, reifying a real business object (if exists in the MAPPER) gives you a good extra abstraction layer.

Level 🔋

[X] Intermediate

https://maximilianocontieri.com/refactoring-012-reify-associative-arrays?embedable=true

https://hackernoon.com/refactoring-013-eliminating-repeated-code-with-dry-principles?embedable=true

Refactoring with AI 🤖

Ask your AI assistant to: "Identify where I am passing arrays of objects and suggest a Typed Collection class for them."

You can also provide the base class and ask: "Find a real business object and generate a boilerplate for a type-safe collection for this entity."

Credits 🙏

Image by Markéta Klimešová on Pixabay

Inspired by the "Collection Object" pattern in clean architecture and the ongoing quest for type safety in dynamic languages.


This article is part of the Refactoring Series.

https://maximilianocontieri.com/how-to-improve-your-code-with-easy-refactorings?embedable=true