Clean Code Principles
Guidelines for writing maintainable, well-structured code in the Tom Framework workspace.
---
Table of Contents
- DRY (Don't Repeat Yourself)
- Abstraction Guidelines
- Library Extraction
- Code Smell Detection
- Dependency Direction
- Single Responsibility
- Refactoring Triggers
---
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
| Library | Add code that... |
|---|---|
tom_basics |
Provides fundamental utilities (string, collections, exceptions, little helpers) |
tom_build_base | Supports build tooling (YAML, paths, file operations) |
tom_basics_console | Handles console I/O, colored output, prompts |
tom_basics_network | Deals with HTTP, REST, network utilities |
tom_crypto | Implements cryptography, hashing, encoding |
tom_reflection | Uses 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
| Smell | Symptom | Solution |
|---|---|---|
| Long method | >25 lines | Extract helper methods |
| Long parameter list | >4 parameters | Introduce 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
| Smell | Symptom | Solution |
|---|---|---|
| God class | Does too many things, >500 lines | Split into focused classes |
| Data class | Only getters/setters, no behavior | Add behavior or use record |
| Refused bequest | Subclass doesn't use inherited methods | Use composition instead |
| Lazy class | Does too little | Inline or merge with another |
Codebase-Level Smells
| Smell | Symptom | Solution |
|---|---|---|
| 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 code | Same code in multiple places | Extract 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
- Coding Guidelines - Formatting, naming, structure
- Project Refactoring - Full refactoring workflow
- Unit Tests - Testing patterns