11 KiB
Hierarchical Refactoring Plan
Overview
This document outlines the step-by-step plan to extract common hierarchical behavior from Categories and Storage Locations into a shared Hierarchical module, based on the superior category implementation approach.
Goals
- Extract shared hierarchical patterns into reusable modules
- Simplify storage locations to match category elegance
- Maintain AprilTag-specific features for storage locations
- Preserve all existing functionality during refactor
- Improve performance and maintainability
Refactoring Strategy
Phase 1: Extract Hierarchical Behavior Module ⏭️ NEXT
Create shared behavior module based on category patterns
Phase 2: Refactor Storage Locations 🔄 FOLLOW-UP
Simplify storage locations to use new shared module
Phase 3: Refactor Categories 🔄 FOLLOW-UP
Update categories to use shared module
Phase 4: Extract LiveView Components 🔄 FOLLOW-UP
Create reusable UI components for hierarchical display
Phase 1: Extract Hierarchical Behavior Module
1.1 Create Core Hierarchical Module
File: lib/components_elixir/inventory/hierarchical.ex
defmodule ComponentsElixir.Inventory.Hierarchical do
@moduledoc """
Shared hierarchical behavior for entities with parent-child relationships.
This module provides common functionality for:
- Path computation (e.g., "Parent > Child > Grandchild")
- Cycle detection and prevention
- Parent/child filtering for UI dropdowns
- Tree traversal utilities
Based on the elegant category implementation approach.
"""
@doc """
Computes full hierarchical path for an entity.
Uses recursive traversal of parent chain.
## Examples
iex> category = %Category{name: "Resistors", parent: %Category{name: "Electronics", parent: nil}}
iex> Hierarchical.full_path(category, &(&1.parent))
"Electronics > Resistors"
"""
def full_path(entity, parent_accessor_fn, separator \\ " > ")
@doc """
Filters entities to remove circular reference options for parent selection.
Prevents an entity from being its own ancestor.
## Examples
iex> categories = [%{id: 1, parent_id: nil}, %{id: 2, parent_id: 1}]
iex> Hierarchical.filter_parent_options(categories, 1, &(&1.id), &(&1.parent_id))
[%{id: 2, parent_id: 1}] # ID 1 filtered out (self-reference)
"""
def filter_parent_options(entities, editing_entity_id, id_accessor_fn, parent_id_accessor_fn)
@doc """
Checks if an entity is a descendant of an ancestor entity.
Used for cycle detection in parent selection.
"""
def is_descendant?(entities, descendant_id, ancestor_id, parent_id_accessor_fn)
@doc """
Gets all root entities (entities with no parent).
"""
def root_entities(entities, parent_id_accessor_fn)
@doc """
Gets all child entities of a specific parent.
"""
def child_entities(entities, parent_id, parent_id_accessor_fn)
@doc """
Generates display name for entity including parent context.
For dropdown displays: "Parent > Child"
"""
def display_name(entity, parent_accessor_fn, separator \\ " > ")
end
1.2 Schema Behaviour Definition
File: lib/components_elixir/inventory/hierarchical_schema.ex
defmodule ComponentsElixir.Inventory.HierarchicalSchema do
@moduledoc """
Behaviour for schemas that implement hierarchical relationships.
Provides a contract for entities with parent-child relationships.
"""
@callback full_path(struct()) :: String.t()
@callback parent(struct()) :: struct() | nil
@callback children(struct()) :: [struct()]
defmacro __using__(_opts) do
quote do
@behaviour ComponentsElixir.Inventory.HierarchicalSchema
import ComponentsElixir.Inventory.Hierarchical
end
end
end
1.3 Update Category Schema
File: lib/components_elixir/inventory/category.ex
defmodule ComponentsElixir.Inventory.Category do
use ComponentsElixir.Inventory.HierarchicalSchema
# ... existing schema definition
@impl true
def full_path(%Category{} = category) do
ComponentsElixir.Inventory.Hierarchical.full_path(category, &(&1.parent))
end
@impl true
def parent(%Category{parent: parent}), do: parent
@impl true
def children(%Category{children: children}), do: children
end
Phase 2: Refactor Storage Locations
2.1 Simplify Storage Location Schema
Changes to: lib/components_elixir/inventory/storage_location.ex
- Remove virtual fields:
levelandpath - Add category-style path function
- Remove complex cycle validation
- Keep AprilTag-specific features
defmodule ComponentsElixir.Inventory.StorageLocation do
use ComponentsElixir.Inventory.HierarchicalSchema
# ... existing schema without virtual fields
@impl true
def full_path(%StorageLocation{} = location) do
ComponentsElixir.Inventory.Hierarchical.full_path(location, &(&1.parent), " / ")
end
# Keep AprilTag-specific functionality
def apriltag_format(storage_location), do: storage_location.apriltag_id
end
2.2 Simplify Storage Location Changeset
Remove:
validate_no_circular_reference/1function- Virtual field handling
- Complex cycle detection
Keep:
- AprilTag validation
- Basic field validation
2.3 Update Inventory Context
Changes to: lib/components_elixir/inventory.ex
Remove:
compute_hierarchy_fields_batch/1compute_level_for_single/1compute_path_for_single/1- All virtual field computation
Simplify:
list_storage_locations/0- use standard preloadingget_storage_location!/1- remove virtual field computation
Phase 3: Refactor Categories
3.1 Update Categories to Use Shared Module
Changes to: lib/components_elixir/inventory/category.ex
Replace existing full_path/1 with call to shared module.
3.2 Update Categories LiveView
Changes to: lib/components_elixir_web/live/categories_live.ex
Replace custom hierarchy functions with shared module calls:
# Replace custom functions with shared module
defp parent_category_options(categories, editing_category_id \\ nil) do
available_categories =
Hierarchical.filter_parent_options(
categories,
editing_category_id,
&(&1.id),
&(&1.parent_id)
)
|> Enum.map(fn category ->
{Hierarchical.display_name(category, &(&1.parent)), category.id}
end)
[{"No parent (Root category)", nil}] ++ available_categories
end
Phase 4: Extract LiveView Components
4.1 Create Hierarchical LiveView Components
File: lib/components_elixir_web/live/hierarchical_components.ex
defmodule ComponentsElixirWeb.HierarchicalComponents do
use Phoenix.Component
alias ComponentsElixir.Inventory.Hierarchical
@doc """
Renders a hierarchical tree of entities with depth-based styling.
"""
def hierarchy_tree(assigns)
@doc """
Renders an individual item in a hierarchical tree.
"""
def hierarchy_item(assigns)
@doc """
Renders a parent selection dropdown with cycle prevention.
"""
def parent_select(assigns)
end
4.2 Update LiveViews to Use Shared Components
Both CategoriesLive and StorageLocationsLive can use the shared components, with custom slots for entity-specific content (like AprilTag display).
Implementation Timeline
Immediate (Phase 1) - 1-2 hours
- Create
Hierarchicalmodule with core functions - Create
HierarchicalSchemabehaviour - Write comprehensive tests for shared module
Short-term (Phase 2) - 2-3 hours
- Refactor storage location schema to remove virtual fields
- Simplify storage location changeset
- Update inventory context to remove batch computation
- Test storage location functionality
Medium-term (Phase 3) - 1-2 hours
- Update category schema to use shared module
- Update categories LiveView to use shared functions
- Test category functionality
Long-term (Phase 4) - 2-3 hours
- Create shared LiveView components
- Refactor both LiveViews to use shared components
- Add entity-specific customization slots
- Comprehensive integration testing
Benefits After Refactoring
Code Quality
- 50% reduction in hierarchical logic duplication
- Consistent patterns across both entity types
- Easier testing with isolated, pure functions
- Better maintainability with single source of truth
Performance
- Elimination of virtual fields reduces memory usage
- Remove batch computation improves response times
- Standard Ecto patterns optimize database queries
- UI-level cycle prevention reduces validation overhead
Developer Experience
- Shared components speed up new hierarchical entity development
- Consistent API reduces learning curve
- Better documentation with centralized behavior
- Easier debugging with simplified call stacks
Risk Mitigation
Database Migration Safety
- Backup database before running migrations
- Test migrations on development environment first
- Incremental approach - one table at a time
Functionality Preservation
- Comprehensive test coverage before refactoring
- Feature parity testing after each phase
- AprilTag functionality isolation to prevent interference
Rollback Plan
- Git branching strategy for each phase
- Database migration rollbacks prepared
- Quick revert capability if issues discovered
Testing Strategy
Unit Tests
- Test all
Hierarchicalmodule functions - Test schema
full_path/1implementations - Test cycle detection edge cases
Integration Tests
- Test LiveView parent selection dropdowns
- Test hierarchical tree rendering
- Test AprilTag functionality preservation
Performance Tests
- Benchmark path computation performance
- Measure memory usage before/after
- Profile database query patterns
Migration Notes
Database Changes Required
- Remove
is_activecolumn from storage_locations (✅ COMPLETED) - No other database changes needed for this refactor
Deployment Considerations
- Zero downtime: Refactor is code-only (except
is_activeremoval) - Backward compatible: No API changes
- Incremental deployment: Can deploy phase by phase
Success Criteria
Functional
- All existing category functionality preserved
- All existing storage location functionality preserved
- AprilTag features remain storage-location specific
- UI behavior identical to current implementation
Non-Functional
- Code duplication reduced by ≥50%
- Performance maintained or improved
- Test coverage ≥95% for shared modules
- Documentation complete for new modules
Next Steps
- Review this plan with team/stakeholders
- Create feature branch for refactoring work
- Begin Phase 1 - Extract hierarchical behavior module
- Write comprehensive tests before any refactoring
- Execute phases incrementally with testing between each
This refactoring will significantly improve code quality and maintainability while preserving all existing functionality and preparing the codebase for future hierarchical entities.