Quand MVVM et MVC ne suffisent plus et que les tests deviennent un enfer
Avez-vous déjà écrit une application en Swift où, après six mois, l'ajout d'une fonctionnalité en cassait trois autres, et où vous deviez réécrire entièrement les tests parce que la base de données était passée de Core Data à Realm ? Si oui, vous n'êtes pas seul. Les architectures classiques en couches (MVC, MVVM) fonctionnent très bien au début, mais se transforment en « bouillie » au fur et à mesure que le projet grandit.
Je vais vous montrer l'architecture Hexagonale (également appelée Ports & Adapters) — une approche venue du monde Java et du DDD, mais qui s'adapte parfaitement à Swift. Nous allons l'analyser avec des exemples concrets, sans « User » ou « Manager » abstraits. Vous verrez comment séparer la logique métier de la base de données, de l'interface utilisateur et du réseau, afin de pouvoir les modifier sans toucher au cœur de l'application.
Ce n'est pas une théorie de plus — c'est une pratique que vous pourrez appliquer dans votre projet dès aujourd'hui.
Qu'est-ce que l'architecture Hexagonale ? En termes simples
Imaginez un hexagone (d'où le nom). Au centre se trouve votre logique métier pure (le domaine). Sur les bords se trouvent les ports (interfaces) par lesquels le monde extérieur communique avec le noyau. Les adaptateurs sont les implémentations concrètes : base de données, API, interface utilisateur, système de fichiers.
La règle principale : le noyau ne sait rien du monde extérieur. Ni UIKit, ni URLSession, ni Core Data. Il ne connaît que ses interfaces (ports).
Exemple concret : imaginez une boutique en ligne. Le noyau sait qu'il y a un « Panier » et « Passer commande ». Peu importe d'où viennent les données — d'un websocket, d'un JSON local ou du clavier. Les adaptateurs se connectent aux ports et traduisent les signaux externes en commandes du domaine.
Pourquoi l'utiliser dans des projets Swift ?
- Testabilité — vous testez le domaine sans base de données ni réseau réels. Remplacez simplement les adaptateurs par des mocks.
- Flexibilité — vous voulez passer de Firebase à votre propre serveur ? Changez un adaptateur, pas la moitié de l'application.
- Lisibilité — la logique métier n'est pas dispersée dans les ViewController. Elle se trouve à un seul endroit.
- Indépendance des frameworks — votre noyau n'importe pas UIKit, Combine ou Alamofire.
Structure d'un projet avec l'architecture Hexagonale en Swift
Passons directement au code. Supposons que nous écrivions une application de gestion de tâches (Todolist). Voici à quoi ressemblera la structure des dossiers :
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swiftNotez que Domain n'importe rien d'autre que Foundation. Adapter peut importer CoreData, UIKit, Alamofire — tout ce que vous voulez.
Étape 1 : Entités du domaine et ports
Commençons par le plus important — le modèle du domaine. C'est une struct ou un enum qui décrit l'entité métier. Pas de NSManagedObject, pas de Codable.
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}Définissons maintenant le port pour le repository (port de sortie). C'est un protocole qui décrit comment nous sauvegardons et chargeons les tâches.
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}Et le port d'entrée — ce par quoi le monde extérieur (UI, API) interagit avec le noyau. Il s'agit généralement d'un UseCase.
// Domain/Us