Architectural Refactoring Patterns

Six fundamental architectural principles for OpenHCS development.

Six Fundamental Principles

1. ABC Contract Enforcement

Use ABCs to enforce explicit contracts and enable polymorphism.

Examples: StorageBackend, FilenameParser, MetadataHandler, LibraryRegistryBase

2. Explicit Dependency Injection

Dependencies are explicitly provided, never implicitly created.

Examples: create_microscope_handler(filemanager=filemanager), factory functions require all dependencies

3. Indirection Minimization

Eliminate unnecessary layers; prefer direct method calls.

Examples: getattr(self, method_name) instead of dispatch tables, direct enum usage

4. Genericism Enforcement

Create truly generic systems that work with any valid configuration.

Examples: ComponentConfiguration[T], DynamicParserMeta generates methods from any enum

5. Fail-Loud Error Handling

Specific exceptions with context; no silent failures or defensive programming.

Examples: MemoryConversionError, StorageResolutionError, allow_cpu_roundtrip=False by default

6. Consistent Interface Design

Uniform patterns across all subsystems.

Examples: ABC + Factory pattern, consistent method naming, enum-driven behavior

Refactoring Patterns

Pattern 1: ABC Contract Enforcement

Before: Inconsistent interfaces across similar classes

class ImageXpressParser:
    def parse_well(self, filename): pass

class OperaPhenixParser:
    def extract_well(self, filename): pass  # Different method name

After: ABC enforces consistent contracts

class FilenameParser(ABC):
    @abstractmethod
    def parse_well(self, filename): pass

class ImageXpressParser(FilenameParser):
    def parse_well(self, filename): pass  # Contract enforced

class OperaPhenixParser(FilenameParser):
    def parse_well(self, filename): pass  # Must match ABC

Pattern 2: Explicit Dependency Injection

Before: Hidden dependencies and global state

class MicroscopeHandler:
    def __init__(self, microscope_type: str):
        self.filemanager = get_global_filemanager()  # Hidden dependency
        self.parser = create_parser(microscope_type)  # Hidden creation

After: Explicit dependency injection

class MicroscopeHandler:
    def __init__(self, parser: FilenameParser, filemanager: FileManager):
        self.parser = parser
        self.filemanager = filemanager

# Factory function with explicit dependencies
def create_microscope_handler(microscope_type: str,
                            filemanager: FileManager) -> MicroscopeHandler:
    parser = create_parser(microscope_type)
    return MicroscopeHandler(parser, filemanager)

Pattern 3: Indirection Minimization

Before: Unnecessary dispatch tables and routing layers

class ComponentProcessor:
    def __init__(self):
        self.dispatch_table = {
            'DAPI': self._process_dapi,
            'GFP': self._process_gfp,
            'RFP': self._process_rfp
        }

    def process(self, component: str, data):
        handler = self.dispatch_table.get(component)
        if handler:
            return handler(data)

After: Direct method calls with enum-driven behavior

class ComponentProcessor:
    def process(self, component: Component, data):
        method_name = f"_process_{component.value.lower()}"
        method = getattr(self, method_name)
        return method(data)

Pattern 4: Genericism Enforcement

Before: Pseudo-generic code with hardcoded assumptions

class ConfigValidator:
    def validate_pipeline_config(self, config):
        # Hardcoded for PipelineConfig only
        if not config.steps:
            return False
        return True

    def validate_zarr_config(self, config):
        # Separate method for each config type
        if not config.compression:
            return False
        return True

After: Truly generic validation using metaprogramming

class ConfigValidator(Generic[T]):
    def validate(self, config: T) -> ValidationResult:
        errors = []
        for field in fields(config):
            if field.metadata.get('required') and getattr(config, field.name) is None:
                errors.append(f"Required field {field.name} is None")
        return ValidationResult(errors)

Pattern 5: Fail-Loud Error Handling

Before: Defensive programming with silent failures

def process_image(image_path: str):
    try:
        image = load_image(image_path)
        if image is not None:
            return process(image)
    except Exception:
        pass  # Silent failure
    return default_image()  # Fallback masks problems

After: Explicit error handling with specific exceptions

def process_image(image_path: str) -> ProcessedImage:
    try:
        image = load_image(image_path)
    except FileNotFoundError as e:
        raise ImageLoadError(f"Image not found: {image_path}") from e
    except PermissionError as e:
        raise ImageLoadError(f"Permission denied: {image_path}") from e

    if image.ndim != 3:
        raise ImageValidationError(f"Expected 3D image, got {image.ndim}D")

    return process(image)

Pattern 6: Consistent Interface Design

Before: Inconsistent interfaces across subsystems

class StorageBackend:
    def load_file(self, path): pass  # Different method name
    def write_file(self, data, path): pass

class MicroscopeHandler:
    def get_data(self, path): pass  # Different method name
    def put_data(self, data, path): pass

After: Consistent interface pattern

class StorageBackend(ABC):
    @abstractmethod
    def load(self, path): pass  # Consistent naming

    @abstractmethod
    def save(self, data, path): pass

class MicroscopeHandler(ABC):
    @abstractmethod
    def load(self, path): pass  # Same interface pattern

    @abstractmethod
    def save(self, data, path): pass

# Factory pattern used consistently
def create_storage_backend(backend_type: str) -> StorageBackend: pass
def create_microscope_handler(microscope_type: str) -> MicroscopeHandler: pass

Refactoring Methodology

Step 1: Identify Violations

  • ABC violations: Similar classes with inconsistent interfaces

  • Dependency violations: Hidden object creation or global state access

  • Indirection excess: Unnecessary dispatch tables or routing layers

  • Pseudo-genericism: Hardcoded assumptions in “generic” code

  • Defensive programming: hasattr checks, silent fallbacks

  • Interface inconsistency: Different method names for similar operations

Step 2: Apply Patterns

  1. Create ABCs for similar functionality

  2. Extract dependencies to constructor parameters

  3. Remove dispatch layers in favor of direct method calls

  4. Use Generic[T] and metaprogramming for true genericism

  5. Replace defensive code with explicit error handling

  6. Standardize interfaces across subsystems

Step 3: Validate Consistency

  • All subsystems follow ABC + Factory pattern

  • Dependencies are explicitly injected

  • Error handling is fail-loud with specific exceptions

  • Systems work with any valid configuration

  • Interfaces are consistent across similar functionality

Evidence from OpenHCS

Stable Systems (Storage, Microscope): Established foundational patterns Refactored Systems (Memory, Component): Applied same principles to new domains Cross-System Integration: All systems work together due to consistent architecture

Summary

OpenHCS refactoring follows six fundamental principles that create architectural consistency:

  1. ABC Contract Enforcement - Explicit interfaces enable polymorphism

  2. Explicit Dependency Injection - No hidden dependencies or object creation

  3. Indirection Minimization - Direct method calls, fewer layers

  4. Genericism Enforcement - True genericism via metaprogramming

  5. Fail-Loud Error Handling - Specific exceptions, no silent failures

  6. Consistent Interface Design - Uniform patterns across all subsystems

These principles appear consistently across both stable legacy code and newly refactored systems, creating a coherent architectural methodology.