The Composable Architecture (TCA, pour faire court) est une bibliothèque permettant de construire des applications de manière cohérente et compréhensible, en tenant compte de la composition, des tests et de l'ergonomie. Elle peut être utilisée avec SwiftUI, UIKit, et encore, et sur toutes les plateformes Apple (iOS, macOS, tvOS et watchOS).
- Qu'est-ce la Composable Architecture?
- Plus d'infos
- Exemples
- Utilisation de base
- bibliothèques supplémentaires
- FAQ
- Exigences
- Installation
- Documentation
- Aide
- Traductions
- Crédits et remerciements
- Autres bibliothèques
Cette bibliothèque fournit quelques outils de base, qui peuvent être utilisés pour construire des applications d'objectifs et de complexité variés. Elle fournit des cas d'utilisation convaincants, que vous pouvez suivre pour résoudre de nombreux problèmes que vous rencontrez quotidiennement lors de la création d'applications, comme par exemple :
-
Gestion d'état
Comment gérer l'état de votre application, à l'aide de types de valeurs simples (structs et enums), et partager l'état entre plusieurs écrans, afin que les mutations dans un écran puissent être immédiatement observées dans un autre. -
Composition
Comment décomposer les grandes fonctionnalités en éléments plus petits, qui eux peuvent être extraits dans leurs propres modules isolés, et être facilement recollés pour finalement former la fonctionnalité. -
Effets de bord
Comment laisser certaines parties de l'application parler au monde extérieur, de la manière la plus testable et claire possible. -
Faire des tests
Comment non seulement tester une fonctionnalité construite dans l'architecture, mais aussi écrire des tests d'intégration pour des fonctionnalités qui ont été composées de nombreux éléments, et écrire des tests de bout à bout pour comprendre comment les effets de bord influencent votre application. Cela vous permet d'obtenir de solides garanties que votre logique opérationnelle s'exécute comme vous l'attendez. -
Ergonomie
Comment accomplir tout cela avec un API simple, avec le moins de concepts et de pièces en mouvement possible.
La Composable Architecture a été conçue au cours de nombreux épisodes sur Point-Free, une série de vidéos consacrée à la programmation fonctionnelle et au langage Swift, animée par Brandon Williams et Stephen Celis.
Vous pouvez regarder tous les épisodes ici, ainsi qu'une visite dédiée, en plusieurs parties, de l'architecture à partir de zéro : partie 1, partie 2, partie 3 et partie 4.
Ce repo contient beaucoup d'exemples, qui démontrent comment résoudre des problèmes ordinaires et complexes, avec la Composable Architecture. Consultez ce répertoire pour les voir tous, y compris :
- Études de cas
- Getting started
- Effects
- Navigation
- Higher-order reducers
- Reusable components
- Location manager
- Motion manager
- Search
- Speech Recognition
- Tic-Tac-Toe
- Todos
- Voice memos
Vous recherchez quelque chose de plus sérieux? Consultez le code source de isowords, un jeu de recherche de mots pour iOS, construit avec SwiftUI et la Composable Architecture.
Pour construire une fonctionnalité à l'aide de la Composable Architecture, vous devez définir certains types et valeurs qui modélisent votre domaine :
- State: Un type qui décrit les données dont votre fonctionnalité a besoin, pour exécuter sa logique et générer son UI.
- Action: Un type qui représente toutes les actions qui peuvent se produire dans votre fonctionnalité, comme les actions de l'utilisateur, les notifications, les sources d'événements, et plus.
- Environment: Un type qui contient toutes les dépendances dont la fonctionnalité a besoin, comme les clients API, les clients analytiques, etc.
- Reducer: Une fonction qui décrit comment faire évoluer l'état actuel de l'application vers l'état suivant, en fonction d'une action donnée. Le reducer est également responsable du renvoi de tout effet qui doit être exécuté, comme les requêtes API, ce qui peut être fait en renvoyant une valeur de type
Effect
. - Store: Le runtime qui pilote réellement votre fonctionnalité. Vous envoyez toutes les actions de l'utilisateur au store afin que ce dernier puisse exécuter le reducer et les effets, et vous pourrez observer les changements d'état dans le store afin de mettre à jour le UI.
Les avantages de cette méthode sont que vous gagnerez instantanément la capacité de tester votre fonctionnalité, et vous serez en mesure de diviser les grandes fonctionnalités complexes en domaines plus petits, qui peuvent être rassemblés ensemble.
Prenons l'exemple d'une interface utilisateur qui affiche un nombre, accompagné de boutons "+" et "-" qui incrémentent et décrémentent ce nombre. Pour rendre les choses intéressantes, supposons qu'il y ait également un bouton qui, lorsqu'il est actionné, lance une requête API pour récupérer un fait aléatoire sur ce nombre, puis affiche ce fait dans une alerte.
L'état de cette fonctionnalité consisterait en un nombre entier pour le compte actuel, ainsi qu'une string optionnelle, qui représente le titre de l'alerte que nous voulons montrer (optionnel car nil
signifie ne pas montrer d'alerte) :
struct AppState: Equatable {
var count = 0
var numberFactAlert: String?
}
Ensuite, nous avons les actions de la fonctionnalité. Il y a les actions évidentes, comme appuyer sur le bouton qui décremente, le bouton qui incrémente, ou le bouton qui présente le fait. Mais il y a aussi des actions un peu moins évidentes, comme l'action de l'utilisateur qui rejette l'alerte, et l'action qui se produit lorsque nous recevons une réponse de la requête API de faits :
enum AppAction: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(Result<String, ApiError>)
}
struct ApiError: Error, Equatable {}
Ensuite, nous modélisons l'environnement des dépendances dont cette fonctionnalité a besoin pour faire son travail. En particulier, pour récupérer un fait numérique, nous devons construire une valeur Effect
qui encapsule la requête au réseau. Cette dépendance est donc une fonction de Int
à Effect<String, ApiError>
, où String
représente la réponse de la requête. De plus, l'effet fera typiquement son travail sur un thread d'arrière-plan (comme c'est le cas avec URLSession
), et donc nous avons besoin d'un moyen de recevoir les valeurs de l'effet sur la main queue. Nous le faisons via un main queue scheduler, une dépendance qu'il est important de contrôler pour pouvoir écrire des tests. Nous devons utiliser un AnyScheduler
afin de pouvoir utiliser un DispatchQueue
en production, et un scheduler de test dans les tests.
struct AppEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
var numberFact: (Int) -> Effect<String, ApiError>
}
Ensuite, nous créons un reducer qui met en œuvre la logique de ce domaine. Il décrit comment changer l'état actuel en l'état suivant, et décrit quels effets doivent être exécutés. Certaines actions n'ont pas besoin d'exécuter des effets, et elles peuvent retourner .none
pour représenter cela:
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .factAlertDismissed:
state.numberFactAlert = nil
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
case .numberFactButtonTapped:
return environment.numberFact(state.count)
.receive(on: environment.mainQueue)
.catchToEffect()
.map(AppAction.numberFactResponse)
case let .numberFactResponse(.success(fact)):
state.numberFactAlert = fact
return .none
case .numberFactResponse(.failure):
state.numberFactAlert = "Could not load a number fact :("
return .none
}
}
Et enfin, nous définissons la view qui affiche la fonctionnalité. Elle retient un Store<AppState, AppAction>
, de sorte qu'elle puisse observer tous les changements d'état et effectuer un nouveau rendu, et nous pouvons envoyer toutes les actions de l'utilisateur au store, pour que l'état change. Nous devons également introduire un struct enveloppant l'alerte de fait pour la rendre Identifiable
, ce que le modifier de view .alert
requiert:
struct AppView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
Button("−") { viewStore.send(.decrementButtonTapped) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
}
.alert(
item: viewStore.binding(
get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
send: .factAlertDismissed
),
content: { Alert(title: Text($0.title)) }
)
}
}
}
struct FactAlert: Identifiable {
var title: String
var id: String { self.title }
}
Il est important de noter que nous avons pu mettre en œuvre l'ensemble de cette fonctionnalité, sans disposer d'un effet réel. C'est important, car cela signifie que les fonctionnalités peuvent être construites de manière isolée, sans construire leurs dépendances, ce qui peut améliorer les durées de compilation.
Il est également simple d'avoir un contrôleur UIKit piloté à partir de ce store. Vous vous abonnez au store dans viewDidLoad
afin de mettre à jour le UI, et de montrer des alertes. Le code est un peu plus long que la version SwiftUI, donc nous l'avons réduit ici:
Cliquez pour agrandir!
class AppViewController: UIViewController {
let viewStore: ViewStore<AppState, AppAction>
var cancellables: Set<AnyCancellable> = []
init(store: Store<AppState, AppAction>) {
self.viewStore = ViewStore(store)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let countLabel = UILabel()
let incrementButton = UIButton()
let decrementButton = UIButton()
let factButton = UIButton()
// Omis : Ajouter des subviews et mettre en place des contraintes...
self.viewStore.publisher
.map { "\($0.count)" }
.assign(to: \.text, on: countLabel)
.store(in: &self.cancellables)
self.viewStore.publisher.numberFactAlert
.sink { [weak self] numberFactAlert in
let alertController = UIAlertController(
title: numberFactAlert, message: nil, preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(
title: "Ok",
style: .default,
handler: { _ in self?.viewStore.send(.factAlertDismissed) }
)
)
self?.present(alertController, animated: true, completion: nil)
}
.store(in: &self.cancellables)
}
@objc private func incrementButtonTapped() {
self.viewStore.send(.incrementButtonTapped)
}
@objc private func decrementButtonTapped() {
self.viewStore.send(.decrementButtonTapped)
}
@objc private func factButtonTapped() {
self.viewStore.send(.numberFactButtonTapped)
}
}
Une fois que nous sommes prêts à afficher cette view, par exemple dans le scene delegate, nous pouvons construire un store. C'est à ce moment que nous devons fournir les dépendances, et pour l'instant nous pouvons simplement utiliser un effet qui renvoie immédiatement une string simulée :
let appView = AppView(
store: Store(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
mainQueue: .main,
numberFact: { number in Effect(value: "\(number) is a good number Brent") }
)
)
)
Et c'est suffisant pour obtenir quelque chose à l'écran avec lequel jouer. Il s'agit certainement de quelques étapes supplémentaires par rapport à la méthode SwiftUI classique, mais il y a quelques avantages. Cela nous donne une manière cohérente d'appliquer les mutations d'état, au lieu de disperser la logique dans certains objets observables, et dans diverses closures d'action des composants du UI. Cela nous donne également une manière concise d'exprimer les effets de bord. Et nous pouvons immédiatement tester cette logique, y compris les effets, sans faire beaucoup de travail supplémentaire.
Pour tester, vous créez d'abord un TestStore
, avec les mêmes informations que pour créer un Store
normal, sauf que cette fois nous pouvons fournir des dépendances adaptées aux tests. En particulier, nous utilisons un scheduler de test au lieu du scheduler direct DispatchQueue.main
parce que cela nous permet de contrôler quand le travail est exécuté, et nous n'avons pas à attendre artificiellement que les queues se rattrapent.
let scheduler = DispatchQueue.test
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
mainQueue: scheduler.eraseToAnyScheduler(),
numberFact: { number in Effect(value: "\(number) is a good number Brent") }
)
)
Une fois que le store de test est créé, nous pouvons l'utiliser pour faire une affirmation sur tout un déroulement d'actions provenant de l'utilisateur. À chaque étape, nous devons prouver que l'état a changé comme nous l'attendons. De plus, si une étape provoque l'exécution d'un effet qui renvoie des données dans le store, nous devons affirmer que ces actions ont été reçues correctement.
Dans le test ci-dessous, l'utilisateur incrémente et décrémente le compteur, puis il demande un fait numérique, et la réponse de cet effet déclenche l'affichage d'une alerte, puis fermer l'alerte la fait disparaître.
// Vérifie qu'appuyer sur les boutons d'incrémentation/décrémentation modifie le compteur.
store.send(.incrementButtonTapped) {
$0.count = 1
}
store.send(.decrementButtonTapped) {
$0.count = 0
}
// Teste qu'appuyer sur le bouton "Fact" nous fait recevoir une réponse de l'effet. Note
// que nous devons avancer le store car nous avons utilisé `.receive(on :)` dans le reducer.
store.send(.numberFactButtonTapped)
scheduler.advance()
store.receive(.numberFactResponse(.success("0 is a good number Brent"))) {
$0.numberFactAlert = "0 is a good number Brent"
}
// Et enfin fermer l'alerte
store.send(.factAlertDismissed) {
$0.numberFactAlert = nil
}
Ce sont les bases de la construction et du testage d'une fonctionnalité dans la Composable Architecture. Il y a beaucoup d'autres choses à découvrir, comme la composition, la modularité, l'adaptabilité et les effets complexes. Le répertoire Examples contient un grand nombre de projets à étudier pour découvrir des utilisations plus avancées.
La Composable Architecture est fournie avec un certain nombre d'outils pour faciliter le débogage.
-
reducer.debug()
étend un reducer avec une débogue-imprimante qui décrit chaque action que le réducteur reçoit et chaque mutation qu'il effectue sur l'état.received action: AppAction.todoCheckboxTapped( index: 0 ) AppState( todos: [ Todo( - isComplete: false, + isComplete: true, description: "Milk", id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90 ), Todo( isComplete: false, description: "Eggs", id: AB3C7921-8262-4412-AA93-9DC5575C1107 ), Todo( isComplete: true, description: "Hand Soap", id: 06E94D88-D726-42EF-BA8B-7B4478179D19 ), ] )
-
reducer.signpost()
instrumente un reducer avec des "signposts" (panneaux indicateurs) afin que vous puissiez comprendre la durée d'exécution des actions, et quand les effets s'exécutent.
L'un des principes les plus importants de la Composable Architecture est que les effets de bord ne sont jamais exécutés directement, mais qu'ils sont emballés dans le type Effect
, renvoyés par les reducers, et que le Store
exécute ensuite l'effet. Ce principe est crucial pour simplifier la façon dont les données circulent dans une application, et pour obtenir une testabilité sur le cycle complet, de l'action de l'utilisateur à l'exécution de l'effet.
Cependant, cela signifie également que de nombreuses bibliothèques et SDK, avec lesquelles vous communiquez quotidiennement, doivent être adaptées pour être un peu plus adaptées au style de la Composable Architecture. C'est pourquoi nous souhaitons faciliter l'utilisation de certains des frameworks les plus populaires d'Apple, en fournissant des bibliothèques enveloppantes qui exposent leurs fonctionnalités d'une manière qui s'harmonise avec notre bibliothèque. Jusqu'à présent, nous offrons:
ComposableCoreLocation
: Un emballage autour deCLLocationManager
, qui le rend facile à utiliser à partir d'un reducer, et facile à écrire des tests pour savoir comment votre logique interagit avec la fonctionnalité deCLLocationManager
.ComposableCoreMotion
: Un emballage autour deCMMotionManager
qui le rend facile à utiliser à partir d'un reducer, et facile à écrire des tests pour savoir comment votre logique interagit avec la fonctionnalité deCMMotionManager
.- D'autres suivront bientôt. Restez à l'affût 😉
Si vous souhaitez contribuer à la création d'une bibliothèque emballage pour un framework que nous n'avons pas encore couvert, n'hésitez pas à ouvrir un "issue" pour exprimer votre intérêt afin que nous puissions discuter de la marche à suivre.
-
Comment la Composable Architecture se compare-t-elle à Elm, Redux et autres ?
Élargir pour voir la réponse
The Composable Architecture (TCA) repose sur les idées popularisées par l'architecture Elm (TEA) et Redux, mais elle a été conçue pour se sentir chez elle dans le langage Swift et sur les plateformes d'Apple.À certains égards, TCA est un peu plus opiniâtre que les autres libraries. Par exemple, Redux n'est pas explicite sur la façon dont on exécute les effets de bord, mais TCA exige que tous les effets de bord soient modélisés dans le type
Effect
et renvoyés par le reducer.Par ailleurs, TCA est un peu plus flexible que les autres bibliothèques. Par exemple, Elm contrôle quels types d'effets peuvent être créés via le type
Cmd
, mais TCA permet une échappatoire vers n'importe quel type d'effet puisqueEffect
se conforme au protocolePublisher
de Combine.Et puis il y a certaines choses auxquelles TCA accorde une grande priorité et qui ne sont pas des points d'intérêt pour Redux, Elm ou la plupart des autres bibliothèques. Par exemple, la composition est un aspect très important de TCA, c'est-à-dire le processus de décomposer de grandes fonctionnalités en unités plus petites qui peuvent ensuite être collées ensemble. Ceci est accompli avec les opérateurs
pullback
etcombine
sur les reducers, et cela aide à gérer les fonctionnalités complexes ainsi que la modularisation pour une base de code mieux isolée et des temps de compilation améliorés. -
Pourquoi
Store
n'est-il pas thread-safe ?
Pourquoisend
n'est-il pas mis en file d'attente ("queued")?
Pourquoisend
n'est-il pas exécuté sur le main thread?Élargir pour voir la réponse
Toutes les interactions avec une instance de
Store
(y compris tous ses scopes derivesViewStore
) doivent être faites sur le même thread. Si le store alimente une view SwiftUI ou UIKit, toutes les interactions doivent être effectuées sur le main thread.Quand une action est envoyée au
Store
, un reducer est exécuté sur l'état actuel, et ce processus ne peut pas être fait à partir de plusieurs threads. Une solution possible est d'utiliser une queue dans l'implémentation desend
, mais cela introduit quelques nouvelles complications:-
Si vous le faites simplement avec
DispatchQueue.main.async
, vous subirez un saut de thread même si vous êtes déjà sur le thread principal. Cela peut conduire à un comportement inattendu dans UIKit et SwiftUI, où il est parfois nécessaire d'effectuer un travail de manière synchrone, comme dans les blocs d'animation. -
Il est possible de créer un scheduler qui effectue son travail immédiatement lorsqu'il est sur le main thread et qui sinon utilise
DispatchQueue.main.async
(e.g. voir leUIScheduler
de CombineScheduler). Cela introduit beaucoup plus de complexité, et ne devrait probablement pas être adopté sans avoir une très bonne raison.
C'est pourquoi nous exigeons que toutes les actions soient envoyées depuis le même thread. Cette exigence est dans le même esprit que la façon dont
URLSession
et d'autres API d'Apple sont conçues. Ces API ont tendance à délivrer leurs résultats sur thread qui leur convient le mieux, et c'est ensuite à vous qu'il revient de les renvoyer vers la main thread si c'est ce dont vous avez besoin. La Composable Architecture vous rend responsable de l'envoi des actions sur main thread. Si vous utilisez un effet qui peut délivrer sa sortie sur un thread non-main, vous devez explicitement exécuter.receive(on :)
afin de le forcer à revenir sur le main thread.Cette approche fait le moins de suppositions possibles sur la façon dont les effets sont créés et transformés, et évite les sauts de threads et le redispatchage inutiles. Elle présente également certains avantages pour les tests. Si vos effets ne sont pas responsables de leur propre séquencement, alors dans les tests, tous les effets s'exécuteront de manière synchrone et immédiate. Vous ne seriez pas en mesure de tester la façon dont les multiples effets en vol se croisent les uns avec les autres et affectent l'état de votre application. Cependant, en laissant le séquencement en dehors du
Store
, nous pouvons tester ces aspects de nos effets si nous le souhaitons, ou nous pouvons les ignorer si nous préférons. Nous avons cette flexibilité.Toutefois, si vous n'êtes toujours pas fan de notre choix, n'ayez crainte! La Composable Architecture est suffisamment souple pour vous permettre d'introduire vous-même cette fonctionnalité si vous le souhaitez. Il est possible de créer un reducer d'ordre supérieur qui peut forcer tous les effets à délivrer leur sortie sur le main thread, indépendamment de l'endroit où l'effet effectue son travail:
extension Reducer { func receive<S: Scheduler>(on scheduler: S) -> Self { Self { state, action, environment in self(&state, action, environment) .receive(on: scheduler) .eraseToEffect() } } }
Vous voudrez probablement toujours quelque chose comme un
UIScheduler
afin de ne pas effectuer inutilement des sauts de threads. -
L'architecture composable dépend du framework Combine, elle nécessite donc des cibles de déploiement minimales de iOS 13, macOS 10.15, Mac Catalyst 13, tvOS 13 et watchOS 6. Si votre application doit prendre en charge des systèmes plus anciens, il existe des fourches pour [ReactiveSwift] (https://github.com/trading-point/reactiveswift-composable-architecture) et [RxSwift] (https://github.com/dannyhertz/rxswift-composable-architecture) que vous pouvez adopter !
Vous pouvez ajouter ComposableArchitecture à un projet Xcode en l'ajoutant en tant que dépendance du package.
- Dans le menu Fichier, sélectionnez Swift Packages > Add Package Dependency....
- Entrez "https://github.com/pointfreeco/swift-composable-architecture" dans le champ de texte de l'URL du package repository.
- En fonction de la structure de votre projet :
- Si vous avez une seule application cible qui a besoin d'accéder à la bibliothèque, ajoutez ComposableArchitecture directement à votre application.
- Si vous souhaitez utiliser cette bibliothèque à partir de plusieurs cibles Xcode, ou en mélangeant des cibles Xcode et des cibles SPM, vous devez créer un framework partagé qui dépend de ComposableArchitecture et ensuite dépendre de ce framework dans toutes vos cibles. Pour un exemple de ceci, consultez l'application de démonstration Tic-Tac-Toe, qui divise de nombreuses fonctionnalités en modules et consomme la bibliothèque statique de cette manière en utilisant le framework TicTacToeCommon.
La dernière documentation relative aux API de la Composable Architecture est disponible [ici] (https://pointfreeco.github.io/swift-composable-architecture/).
Si vous souhaitez discuter à propos de la Composable Architecture ou si vous avez une question sur la manière de l'utiliser pour résoudre un problème particulier, vous pouvez lancer un sujet dans l'onglet discussions de ce repo, ou poser des questions sur son forum Swift.
Les traductions suivantes de ce README ont été fournies par des membres de la communauté:
Si vous souhaitez contribuer à une traduction, veuillez ouvrir un PR avec un lien vers un Gist !
Les personnes suivantes ont donné leur avis sur la bibliothèque à ses débuts et ont contribué à faire de la bibliothèque ce qu'elle est aujourd'hui:
Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, et tous les abonnés de Point-Free 😁.
Nous remercions particulièrement Chris Liscio qui nous a aidé à résoudre de nombreuses excentricités de SwiftUI et à affiner l'API finale.
Et merci à Shai Mishali et au projet CombineCommunity, dont nous avons repris l'implémentation de Publishers.Create
, que nous utilisons dans Effect
pour faire le lien entre les API basées sur les délégués et les callbacks, ce qui facilite grandement l'interface avec les frameworks externes.
La Composable Architecture a été construite sur la base d'idées lancées par d'autres bibliothèques, en particulier Elm et Redux.
Il existe également de nombreuses bibliothèques d'architecture dans la communauté Swift et iOS. Chacune d'entre elles a son propre ensemble de priorités et de compromis qui diffèrent de la Composable Architecture.
-
Et plus encore
Cette bibliothèque est publiée sous la licence MIT. Voir LICENSE pour plus de détails.