SuperAPI AI Directory

first-class-collection

Coding & development

View repository

Updated 10-16 06:19

About this skill

First Class Collection

Create a dedicated class that wraps a collection and aggregates domain logic.

Core Principles

A class that wraps a collection must not have any fields other than the collection. (ThoughtWorks Anthology, Object Calisthenics Rule 4)

Approach Characteristics Problems
Raw collection List<Order> orders Scattered logic, lack of domain concepts
First Class Orders orders Aggregated responsibility, domain representation, guaranteed immutability

Decision Flow

コレクション型のフィールド/変数
    ↓
ビジネスロジック(集計/フィルタ/バリデーション)が必要か?
    ├─ YES → ファーストクラスコレクション化を検討
    │         ├─ 同じ操作が複数箇所にあるか? → 必須
    │         └─ 1箇所のみ → 将来性を考慮して判断
    └─ NO → 生のコレクションで可

Anti-pattern Detection

If you find the following patterns, consider converting them:

❌ orders.stream().filter(o -> o.getStatus() == PENDING).toList()  // 複数箇所で同じフィルタ
❌ int total = items.stream().mapToInt(Item::getPrice).sum()       // 呼び出し側で集計
❌ if (users.isEmpty()) throw new EmptyUsersException()            // バリデーション散在
❌ for (Order o : orders) { if (o.isOverdue()) ... }               // 外部でループ処理
❌ orders.add(order)                                                // 直接変更可能

Conversion Patterns

The following explanations use Java collections, but since the various types provided are mutable collections, the internal collection cannot be returned as-is, so measures such as making a copy are necessary. However, if immutable collections are available as in Scala, such consideration is unnecessary, so if immutable collections are available, use them preferentially.

1. Basic Structure

// ❌ 生のコレクション
List<Order> orders;

// ✅ ファーストクラスコレクション
public final class Orders {
    private final List<Order> values;

    public Orders(List<Order> values) {
        this.values = List.copyOf(values);  // 不変性保証
    }

    // ドメインロジックをここに集約
}

2. Aggregation of Aggregation Logic

// ❌ 呼び出し側で集計
Money total = Money.ZERO;
for (Order order : orders) {
    total = total.add(order.getAmount());
}

// ✅ コレクションに集約
public Money totalAmount() {
    return values.stream()
        .map(Order::amount)
        .reduce(Money.ZERO, Money::add);
}

3. Internalization of Filtering

// ❌ 外部でフィルタリング
List<Order> pending = orders.stream()
    .filter(o -> o.getStatus() == PENDING)
    .toList();

// ✅ ドメイン用語でメソッド化
public Orders pending() {
    return new Orders(
        values.stream()
            .filter(Order::isPending)
            .toList()
    );
}

4. Aggregation of Validation

// ❌ 外部でバリデーション
if (orders.isEmpty()) {
    throw new IllegalArgumentException("注文がありません");
}

// ✅ 生成時にバリデーション
public Orders(List<Order> values) {
    if (values.isEmpty()) {
        throw new EmptyOrdersException();
    }
    this.values = List.copyOf(values);
}

5. Immutable Add Operations

// ❌ 破壊的変更
orders.add(newOrder);

// ✅ 新しいインスタンスを返す
public Orders add(Order order) {
    List<Order> newList = new ArrayList<>(values);
    newList.add(order);
    return new Orders(newList);
}

Language-Specific Idioms

TypeScript

class Orders {
    private constructor(private readonly values: readonly Order[]) {}

    static of(orders: Order[]): Orders {
        return new Orders([...orders]);
    }

    totalAmount(): Money {
        return this.values.reduce(
            (sum, order) => sum.add(order.amount),
            Money.ZERO
        );
    }
}

Rust

pub struct Orders(Vec<Order>);

impl Orders {
    pub fn new(orders: Vec<Order>) -> Self {
        Self(orders)
    }

    pub fn total_amount(&self) -> Money {
        self.0.iter().map(|o| o.amount()).sum()
    }

    pub fn pending(&self) -> Self {
        Self(self.0.iter().filter(|o| o.is_pending()).cloned().collect())
    }
}

Python

@dataclass(frozen=True)
class Orders:
    _values: tuple[Order, ...]

    @classmethod
    def of(cls, orders: list[Order]) -> "Orders":
        return cls(tuple(orders))

    def total_amount(self) -> Money:
        return sum((o.amount for o in self._values), Money.ZERO)

Go

type Orders struct {
    values []Order
}

func NewOrders(orders []Order) Orders {
    copied := make([]Order, len(orders))
    copy(copied, orders)
    return Orders{values: copied}
}

func (o Orders) TotalAmount() Money {
    total := ZeroMoney()
    for _, order := range o.values {
        total = total.Add(order.Amount())
    }
    return total
}

func (o Orders) Pending() Orders {
    var pending []Order
    for _, order := range o.values {
        if order.IsPending() {
            pending = append(pending, order)
        }
    }
    return NewOrders(pending)
}

Design Guidelines

What Should Be Made Into a First-Class Collection

  • Collections that have a name in the domain (e.g., "order list," "inventory list")
  • Have aggregation, filtering, or validation logic
  • The same operations are performed from multiple places
  • There are constraints related to business rules

What Can Remain a Raw Collection

  • Pure data transfer (lists within DTOs)
  • Framework/library constraints
  • Temporary intermediate data
  • Simple grouping that requires no logic

Review Perspectives

  1. Scattered logic: Are the same collection operations present in multiple places?
  2. Domain concept: Does the collection have a business name?
  3. Immutability: Is it not being modified directly from outside?
  4. Responsibility: Does it follow Tell, Don't Ask?

Related Principles

Principle Relationship
Tell, Don't Ask Tell collections what to do rather than asking them
Single Responsibility Principle Aggregate collection operations in one place
DRY Eliminate duplicated collection operations
Encapsulation Hide the internal list

Detailed Guidelines

For language-specific detailed patterns, iterator implementations, and testing methods, see references/patterns.md .

Related Skills (Recommended for Concurrent Reading)

When using this skill, also refer to the following skills:

  • tell-dont-ask: Fundamental principles of the pattern of commanding a collection
  • law-of-demeter: The principle of preventing direct access to the collection's internals
  • intent-based-dedup: The judgment not to unify collections with the same structure but different intent