6.2 KiB
6.2 KiB
Enum-to-Class Principle: Single Source of Truth
Version: 1.0
Last Updated: 2025-12-06
Status: Active Rule
Core Principle
Enums are TEMPORARY scaffolding. Once an enum is promoted to a class hierarchy, the enum MUST be deleted to maintain a Single Source of Truth.
Rationale
The Problem: Dual Representation
When both an enum AND a class hierarchy exist for the same concept:
- Data sync issues: Enum values and class names can drift apart
- Maintenance burden: Changes must be made in two places
- Developer confusion: Which one should I use?
- Validation conflicts: Enum constraints vs class ranges may diverge
The Solution: Single Source of Truth
- Enums: Use for simple, fixed value constraints (e.g.,
DataTierEnum: TIER_1, TIER_2, TIER_3, TIER_4) - Classes: Use when the concept needs properties, relationships, or rich documentation
- NEVER BOTH: Once promoted to classes, DELETE the enum
When to Promote Enum to Classes
Promote when the concept needs:
| Need | Enum Can Do? | Class Required? |
|---|---|---|
| Fixed value constraint | ✅ Yes | ✅ Yes |
Properties (e.g., role_category, typical_domains) |
❌ No | ✅ Yes |
| Rich description per value | Limited | ✅ Yes |
| Relationships to other entities | ❌ No | ✅ Yes |
| Inheritance hierarchy | ❌ No | ✅ Yes |
| Independent identity (URI) | Limited | ✅ Yes |
Ontology class mapping (class_uri) |
Via meaning |
✅ Native |
Rule of thumb: If you're adding detailed documentation to each enum value, or want to attach properties, it's time to promote to classes.
Promotion Workflow
Step 1: Create Class Hierarchy
# modules/classes/StaffRole.yaml (base class)
StaffRole:
abstract: true
description: Base class for staff role categories
slots:
- role_id
- role_name
- role_category
- typical_domains
# modules/classes/StaffRoles.yaml (subclasses)
Curator:
is_a: StaffRole
description: Museum curator specializing in collection research...
Conservator:
is_a: StaffRole
description: Conservator specializing in preservation...
Step 2: Update Slot Ranges
# BEFORE (enum)
staff_role:
range: StaffRoleTypeEnum
# AFTER (class)
staff_role:
range: StaffRole
Step 3: Update Modular Schema Imports
# REMOVE enum import
# - modules/enums/StaffRoleTypeEnum # DELETED
# ADD class imports
- modules/classes/StaffRole
- modules/classes/StaffRoles
Step 4: Archive the Enum
mkdir -p schemas/.../archive/enums
mv modules/enums/OldEnum.yaml archive/enums/OldEnum.yaml.archived_$(date +%Y%m%d)
Step 5: Document the Change
- Update
archive/enums/README.mdwith migration entry - Add comment in modular schema explaining removal
- Update any documentation referencing the old enum
Example: StaffRoleTypeEnum → StaffRole
Before (2025-12-05):
# StaffRoleTypeEnum.yaml
StaffRoleTypeEnum:
permissible_values:
CURATOR:
description: Museum curator
CONSERVATOR:
description: Conservator
# ... 51 values with limited documentation
After (2025-12-06):
# StaffRole.yaml (abstract base)
StaffRole:
abstract: true
slots:
- role_id
- role_name
- role_category
- typical_domains
- typical_responsibilities
- requires_qualification
# StaffRoles.yaml (51 subclasses)
Curator:
is_a: StaffRole
class_uri: schema:curator
description: |
Museum curator specializing in collection research...
**IMPORTANT - FORMAL TITLE vs DE FACTO WORK**:
This is the OFFICIAL job appellation/title. Actual work may differ.
slot_usage:
role_category:
equals_string: CURATORIAL
typical_domains:
equals_expression: "[Museums, Galleries]"
Why the promotion?
- Need to distinguish FORMAL TITLE from DE FACTO WORK
- Each role has
role_category,common_variants,typical_domains,typical_responsibilities - Roles benefit from inheritance (
Curator is_a StaffRole) - Richer documentation per role
Enums That Should REMAIN Enums
Some enums are appropriate as permanent fixtures:
| Enum | Why Keep as Enum |
|---|---|
DataTierEnum |
Simple 4-value tier (TIER_1 through TIER_4), no properties needed |
DataSourceEnum |
Fixed source types, simple strings |
CountryCodeEnum |
ISO 3166-1 standard, no custom properties |
LanguageCodeEnum |
ISO 639 standard, no custom properties |
Characteristics of "permanent" enums:
- Based on external standards (ISO, etc.)
- Simple values with no need for properties
- Unlikely to require rich per-value documentation
- Used purely for validation/constraint
Anti-Patterns
❌ WRONG: Keep Both Enum and Classes
# modules/enums/StaffRoleTypeEnum.yaml # ← Still exists!
# modules/classes/StaffRole.yaml # ← Also exists!
# Which one is authoritative? CONFUSION!
❌ WRONG: Create Classes but Keep Enum "for backwards compatibility"
# "Let's keep the enum for old code"
# Result: Two sources of truth, guaranteed drift
✅ CORRECT: Delete Enum After Creating Classes
# modules/enums/StaffRoleTypeEnum.yaml # ← ARCHIVED
# modules/classes/StaffRole.yaml # ← Single source of truth
# modules/classes/StaffRoles.yaml # ← All 51 role subclasses
Verification Checklist
After promoting an enum to classes:
- Old enum file moved to
archive/enums/ - Modular schema import removed for enum
- Modular schema import added for new class(es)
- All slot ranges updated from enum to class
- No grep results for old enum name in active schema files
archive/enums/README.mdupdated with migration entry- Comment added in modular schema explaining removal
# Verify enum is fully removed (should return only archive hits)
grep -r "StaffRoleTypeEnum" schemas/20251121/linkml/
See Also
docs/ENUM_CLASS_SINGLE_SOURCE.md- Extended documentationschemas/20251121/linkml/archive/enums/README.md- Archive directory- LinkML documentation on enums: https://linkml.io/linkml/schemas/enums.html
- LinkML documentation on classes: https://linkml.io/linkml/schemas/models.html