Clean Code Principles

Guidelines for writing maintainable, well-structured code in the Tom Framework workspace.

---

Table of Contents

---

DRY (Don't Repeat Yourself)

When Duplication is a Code Smell

Extract when: - Same logic appears 3+ times - Copy-paste with minor variations - Bug fixes need to be applied in multiple places - Changes to one copy require changes to others

Acceptable duplication: - Similar but semantically different code (coincidental similarity) - Test setup code that documents specific scenarios - Configuration that may diverge in the future - Boilerplate required by frameworks

Extraction Patterns

// Before: Duplicated validation
void processUser(User user) {
  if (user.name.isEmpty || user.name.length > 100) {
    throw ValidationException('Invalid name');
  }
  // ... process
}

void updateUser(User user) {
  if (user.name.isEmpty || user.name.length > 100) {
    throw ValidationException('Invalid name');
  }
  // ... update
}

// After: Extracted validation
void _validateUserName(String name) {
  if (name.isEmpty || name.length > 100) {
    throw ValidationException('Invalid name');
  }
}

void processUser(User user) {
  _validateUserName(user.name);
  // ... process
}

Rule of Three

1. First occurrence: Write it 2. Second occurrence: Note the duplication 3. Third occurrence: Extract and refactor

---

Abstraction Guidelines

When to Create Abstractions

DO create abstractions when: - Multiple implementations exist or are planned - Testing requires mocking/stubbing - The concept has a clear, stable interface - Different parts of the system need the same capability

DON'T create abstractions when: - Only one implementation exists with no plans for more - The abstraction would be a 1:1 wrapper - It adds complexity without clear benefit - You're guessing at future requirements

Abstraction Decision Tree

Is there more than one implementation?
├── Yes → Create interface/abstract class
└── No → Will there be multiple implementations?
    ├── Definitely → Create abstraction
    ├── Maybe → Wait until second implementation needed
    └── No → Use concrete class

Signs of Premature Abstraction

  • Interface with single implementation (no plans for more)
  • Abstract class that's never extended
  • Factory that creates only one type
  • Dependency injection for classes that never change

Signs of Missing Abstraction

  • Long switch/if-else chains on type
  • Casting to specific types frequently
  • Similar classes with duplicated methods
  • Hard to test due to concrete dependencies

---

Library Extraction

When to Create a New Package

Create a new package when:

1. Distinct domain: The code represents a separate concern (e.g., crypto, networking) 2. Reuse potential: Multiple projects need it 3. Independent lifecycle: Can be versioned/released separately 4. External consumers: May be published to pub.dev

When to Add to Existing Workspace Libraries

LibraryAdd code that...
tom_basics Provides fundamental utilities (string, collections, exceptions, little helpers)
tom_build_baseSupports build tooling (YAML, paths, file operations)
tom_basics_consoleHandles console I/O, colored output, prompts
tom_basics_networkDeals with HTTP, REST, network utilities
tom_cryptoImplements cryptography, hashing, encoding
tom_reflectionUses runtime reflection, mirrors

Decision Matrix

Is the code...
├── Used by build tools only? → tom_build_base
├── A general utility? → tom_basics
├── Console-related? → tom_basics_console
├── Network-related? → tom_basics_network
├── Crypto-related? → tom_crypto
├── Reflection-based? → tom_reflection
├── Domain-specific? → Keep in project or create new package
└── Used by one project only? → Keep in project

Before Creating a New Package

1. Check if functionality exists in workspace libraries 2. Consider if it fits an existing package 3. Evaluate reuse potential (will 2+ projects use it?) 4. Check pub.dev for existing solutions

---

Code Smell Detection

Method-Level Smells

SmellSymptomSolution
Long method>25 linesExtract helper methods
Long parameter list>4 parametersIntroduce parameter object
Feature envy Method uses another class's data extensively Move method to that class
Data clumps Same group of parameters passed together Create a class for them

Class-Level Smells

SmellSymptomSolution
God classDoes too many things, >500 linesSplit into focused classes
Data classOnly getters/setters, no behaviorAdd behavior or use record
Refused bequest Subclass doesn't use inherited methods Use composition instead
Lazy classDoes too littleInline or merge with another

Codebase-Level Smells

SmellSymptomSolution
Shotgun surgery One change requires many file edits Consolidate related code
Divergent change Class changes for multiple unrelated reasons Split by responsibility
Parallel inheritance Adding subclass requires adding another Merge hierarchies
Duplicate codeSame code in multiple placesExtract to shared location

---

Dependency Direction

Allowed Dependencies

┌─────────────────────────────────────────────────────────────┐
│                      Applications                           │
│  (tom_uam_server, tom_sqm_server, tom_assistant)           │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Domain Libraries                         │
│  (tom_core_*, tom_uam_*, tom_sqm_*, tom_provisioning)      │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                   Tooling Libraries                         │
│  (tom_d4rt, tom_reflection, tom_build_base)                │
└─────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│                    Foundation Libraries                     │
│  (tom_basics, tom_crypto)                                  │
└─────────────────────────────────────────────────────────────┘

Rules

1. Dependencies flow downward - Higher layers can depend on lower layers 2. No upward dependencies - Foundation cannot depend on domain 3. No circular dependencies - A cannot depend on B if B depends on A 4. Minimize cross-domain - UAM shouldn't depend on SQM internals

Checking Dependencies

# View dependency tree
buildkit -R :dependencies --deep

# Check for cycles
buildkit -R :dependencies --check-cycles

---

Single Responsibility

Class Responsibility

Each class should have one reason to change:

// Bad: Multiple responsibilities
class UserManager {
  void createUser(User user) { ... }
  void sendWelcomeEmail(User user) { ... }  // Email responsibility
  void logUserCreation(User user) { ... }   // Logging responsibility
  void validateUser(User user) { ... }      // Validation responsibility
}

// Good: Single responsibility
class UserService {
  final UserValidator _validator;
  final UserRepository _repository;
  final UserNotifier _notifier;
  
  Future<void> createUser(User user) async {
    _validator.validate(user);
    await _repository.save(user);
    await _notifier.notifyCreation(user);
  }
}

Method Responsibility

Each method should do one thing:

// Bad: Does multiple things
void processAndSaveUser(User user) {
  // Validate
  if (user.name.isEmpty) throw ...;
  // Transform
  user.name = user.name.trim();
  // Save
  _repository.save(user);
  // Notify
  _eventBus.emit(UserCreated(user));
}

// Good: Single purpose, delegates
void createUser(User user) {
  _validate(user);
  _normalize(user);
  _save(user);
  _notify(user);
}

---

Refactoring Triggers

When to Refactor

Immediate triggers: - Bug discovered due to code structure - Cannot add feature without breaking existing code - Test is impossible to write - Code review feedback

Planned triggers: - Complexity metrics exceeded (see coding_guidelines.md) - Duplication count reaches 3+ - File exceeds 500 lines - Class has 15+ public methods

Refactoring Workflow

1. Ensure tests exist - Don't refactor untested code 2. Make small changes - One refactoring at a time 3. Run tests after each change - Catch breakage early 4. Commit frequently - Enable easy rollback

When NOT to Refactor

  • In the middle of a feature (finish feature first)
  • Without tests (write tests first)
  • Just before release (stability > cleanliness)
  • When "it works" and no changes are needed

---

See Also