跳转至

ADR 001: Choose MVVM Architecture

Status

Accepted

Context

EasyKiConverter is a Qt 6 Quick-based desktop application for converting EasyEDA components to KiCad format. In the early stages of the project, we used a simple MVC (Model-View-Controller) architecture. However, as the project evolved, we encountered the following issues:

  1. High Code Coupling: The Controller layer took on too many responsibilities, including business logic, UI state management, and data transformation, making the code difficult to maintain and test.

  2. Chaotic UI State Management: UI state was scattered between Controller and View, making it difficult to track and synchronize.

  3. Difficult Testing: Due to the coupling of business logic and UI code, unit tests were difficult to write and maintain.

  4. Poor Scalability: Adding new features required modifications in multiple places, easily introducing errors.

  5. Low Code Reusability: Similar business logic was implemented repeatedly in different places.

We needed a better architecture to solve these problems.

Decision

We chose to adopt the MVVM (Model-View-ViewModel) architecture for the following reasons:

Architecture Design

┌─────────────────────────────────────────┐
│              View Layer                  │
│         (QML Components)                 │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│          ViewModel Layer                │
│  ┌──────────────────────────────────┐   │
│  │ ComponentListViewModel          │   │
│  │ ExportSettingsViewModel         │   │
│  │ ExportProgressViewModel         │   │
│  │ ThemeSettingsViewModel          │   │
│  └──────────────────────────────────┘   │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│           Service Layer                  │
│  ┌──────────────────────────────────┐   │
│  │ ComponentService                 │   │
│  │ ExportService                    │   │
│  │ ConfigService                    │   │
│  └──────────────────────────────────┘   │
└──────────────┬──────────────────────────┘
┌──────────────▼──────────────────────────┐
│            Model Layer                   │
│  ┌──────────────────────────────────┐   │
│  │ ComponentData                    │   │
│  │ SymbolData                       │   │
│  │ FootprintData                    │   │
│  │ Model3DData                      │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘

Layer Responsibilities

  1. Model Layer
  2. Responsible for data storage and management
  3. Contains no business logic
  4. Pure data models

  5. Service Layer

  6. Responsible for business logic processing
  7. Provides core functionality
  8. Calls underlying APIs

  9. ViewModel Layer

  10. Manages UI state
  11. Handles user input
  12. Calls Service layer
  13. Data binding and transformation

  14. View Layer

  15. Responsible for UI display and user interaction
  16. Implemented using QML
  17. Communicates with ViewModel through data binding

Reasons for Choice

  1. Clear Separation of Concerns
  2. Model only handles data
  3. ViewModel only handles UI state and business logic calls
  4. View only handles UI display
  5. Service only handles business logic

  6. Better Testability

  7. ViewModels can be tested independently of View
  8. Service layer can be unit tested independently
  9. Model layer is easy to test

  10. Better Maintainability

  11. Clear code organization
  12. Clear responsibilities
  13. Easy to locate and fix issues

  14. Better Scalability

  15. Adding new features only requires adding code in the appropriate layer
  16. Does not affect other layers
  17. Easy to add new ViewModels

  18. Natural Fit with Qt Quick

  19. Qt Quick's data binding mechanism perfectly matches MVVM
  20. QML is very suitable as the View layer
  21. Qt's signal-slot mechanism implements the observer pattern

  22. Better Team Collaboration

  23. Different developers can focus on different layers
  24. UI developers focus on QML
  25. Logic developers focus on C++
  26. Reduces conflicts

Consequences

Positive Consequences

  1. Improved Code Quality
  2. Reduced code coupling
  3. Improved code reusability
  4. Enhanced code readability

  5. Increased Development Efficiency

  6. Faster development of new features
  7. Easier bug fixes
  8. More efficient code reviews

  9. Improved Test Coverage

  10. Unit tests are easier to write
  11. Test coverage increased from 30% to 80%+
  12. More comprehensive integration tests

  13. Reduced Maintenance Costs

  14. Faster problem location
  15. Smaller scope of changes
  16. Safer refactoring

  17. Improved User Experience

  18. Faster UI response
  19. More stable state management
  20. More comprehensive error handling

Negative Consequences

  1. Learning Curve
  2. New developers need to understand MVVM architecture
  3. Requires additional training and documentation

  4. Initial Development Cost

  5. Architecture refactoring takes time
  6. Requires writing more code
  7. Needs to establish new development processes

  8. Risk of Over-engineering

  9. MVVM may be too complex for simple features
  10. Need to balance architecture complexity

Mitigation Measures

  1. Comprehensive Documentation
  2. Provide detailed architecture documentation
  3. Provide development guides
  4. Provide example code

  5. Gradual Migration

  6. Not refactoring all code at once
  7. Migrating modules gradually
  8. Maintaining system stability

  9. Code Reviews

  10. Ensure new code follows MVVM architecture
  11. Regularly review code quality
  12. Timely correction of deviations

Implementation Details

Migration Steps

  1. Phase 1: Service Layer
  2. Extract business logic from MainController
  3. Create Service classes
  4. Test Service layer

  5. Phase 2: ViewModel Layer

  6. Create ViewModel classes
  7. Migrate UI state management
  8. Implement data binding

  9. Phase 3: QML Migration

  10. Refactor QML code
  11. Use data binding
  12. Remove code that directly calls C++

  13. Phase 4: Remove MainController

  14. Delete MainController
  15. Clean up related code
  16. Complete testing

Key Components

  1. ComponentService
  2. Responsible for component data retrieval
  3. Responsible for component validation
  4. Calls EasyedaApi

  5. ExportService

  6. Responsible for symbol/footprint/3D export
  7. Manages parallel conversion
  8. Calls Exporter*

  9. ConfigService

  10. Responsible for configuration load/save
  11. Manages theme settings
  12. Calls ConfigManager

  13. ComponentListViewModel

  14. Manages component list state
  15. Handles user input
  16. Calls ComponentService

  17. ExportSettingsViewModel

  18. Manages export settings state
  19. Handles configuration changes
  20. Calls ConfigService

  21. ExportProgressViewModel

  22. Manages export progress state
  23. Displays conversion results
  24. Calls ExportService

  25. ThemeSettingsViewModel

  26. Manages theme settings state
  27. Handles dark/light mode toggle
  28. Calls ConfigService

Alternatives

1. Keep MVC Architecture

Pros: - No refactoring needed - Team is familiar with it

Cons: - Cannot solve existing problems - Maintenance costs continue to increase

2. Use MVP Architecture

Pros: - Clearer separation than MVC - View and Model completely decoupled

Cons: - Presenter may still become complex - Less natural fit with Qt Quick than MVVM

3. Use Redux Pattern

Pros: - Very clear state management - Easy to debug

Cons: - Too complex for Qt Quick - Steep learning curve

Conclusion

After evaluation, we chose MVVM architecture as the architecture pattern for EasyKiConverter. MVVM architecture provides clear separation of concerns, better testability, and maintainability, and perfectly matches Qt Quick's data binding mechanism. Although the initial migration cost is high, in the long run, this will greatly improve development efficiency and code quality.

References