Table of Contents
Le modèle de constructeur est l'un des modèles de conception les plus pratiques pour gérer la complexité pendant la création d'objets. Dans Swift, où la sécurité de type et la lisibilité sont prisés, le modèle de constructeur offre une façon propre et enchaînée de construire des objets qui nécessitent de nombreuses options de configuration, paramètres optionnels ou interdépendances complexes.
Comprendre le modèle du constructeur
Le modèle de constructeur sépare la construction d'un objet complexe de sa représentation finale. Au lieu de forcer un initialisateur massif avec des dizaines de paramètres — dont beaucoup peuvent être optionnels ou ont des valeurs par défaut — vous créez un objet de constructeur séparé qui recueille la configuration étape par étape. Lorsque toutes les propriétés souhaitées sont définies, vous appelez une méthode pour produire le produit final, souvent immuable.
Ce modèle est particulièrement utile à Swift lorsqu'il s'agit de :
- Composants d'interface utilisateur (vues, cellules, couches) avec de nombreuses options d'apparence
- Configurations de requêtes réseau (en-têtes, paramètres, authentification)
- Objets de modèle de données de base ou de réalité avec des relations optionnelles
- Objets de domaine nécessitant une validation avant la création
Le modèle de constructeur appartient à la famille de création et est souvent comparé aux modèles de la méthode Factory et de l'usine Abstract. Cependant, les constructeurs sont uniques en ce sens qu'ils permettent au même processus de construction de créer des représentations différentes – vous pouvez réutiliser le constructeur pour plusieurs configurations sans changer son interface.
Mise en œuvre du modèle de constructeur dans Swift: Exemple de fondation
Imaginez que vous ayez besoin d'une sous-classe personnalisée avec plusieurs propriétés configurables. Sans constructeur, vous pourriez finir par avoir un long initialisateur ou une méthode de configuration chargée de propriété. Avec le constructeur, le code devient auto-documentant et expressif.
Étape 1: Définir le produit
Le produit est l'objet que vous voulez créer. Dans Swift, il est commun d'utiliser un pour la sémantique de valeur et de le rendre immuable après construction.
struct CustomViewConfig {
let backgroundColor: UIColor
let cornerRadius: CGFloat
let borderWidth: CGFloat
let borderColor: UIColor
let shadowOpacity: Float
let shadowRadius: CGFloat
}
Étape 2: Créer le constructeur
Le constructeur détient des valeurs par défaut pour chaque propriété et fournit des méthodes qui les mettent à jour, en retournant généralement (ou le type de constructeur) pour permettre l'enchaînement de la méthode.
class CustomViewConfigBuilder {
private var backgroundColor: UIColor = .white
private var cornerRadius: CGFloat = 0.0
private var borderWidth: CGFloat = 0.0
private var borderColor: UIColor = .clear
private var shadowOpacity: Float = 0.0
private var shadowRadius: CGFloat = 0.0
@discardableResult
func withBackgroundColor(_ color: UIColor) -> Self {
self.backgroundColor = color
return self
}
@discardableResult
func withCornerRadius(_ radius: CGFloat) -> Self {
self.cornerRadius = radius
return self
}
@discardableResult
func withBorder(width: CGFloat, color: UIColor) -> Self {
self.borderWidth = width
self.borderColor = color
return self
}
@discardableResult
func withShadow(opacity: Float, radius: CGFloat) -> Self {
self.shadowOpacity = opacity
self.shadowRadius = radius
return self
}
func build() -> CustomViewConfig {
// optional validation can go here
return CustomViewConfig(
backgroundColor: backgroundColor,
cornerRadius: cornerRadius,
borderWidth: borderWidth,
borderColor: borderColor,
shadowOpacity: shadowOpacity,
shadowRadius: shadowRadius
)
}
}
Remarquez l'utilisation de – cela permet aux appelants d'ignorer la valeur de retour s'ils n'ont pas besoin d'une chaîne, ce qui peut être utile dans certains contextes.
Étape 3: Utilisez le constructeur
let config = CustomViewConfigBuilder()
.withBackgroundColor(.systemBlue)
.withCornerRadius(12.0)
.withBorder(width: 1.5, color: .darkGray)
.withShadow(opacity: 0.3, radius: 4.0)
.build()
// Apply the config to a view
let myView = UIView()
myView.backgroundColor = config.backgroundColor
myView.layer.cornerRadius = config.cornerRadius
myView.layer.borderWidth = config.borderWidth
myView.layer.borderColor = config.borderColor.cgColor
myView.layer.shadowOpacity = config.shadowOpacity
myView.layer.shadowRadius = config.shadowRadius
Quand utiliser le modèle de constructeur
Le modèle de construction brille dans les scénarios suivants:
- De nombreux paramètres optionnels :[ Si un type a plus de 3-4 options de configuration, un constructeur améliore la lisibilité et réduit les arguments positionnels sensibles aux erreurs.
- Exigences d'immutabilité: Vous voulez que l'objet final soit immuable, mais sa construction nécessite de nombreuses étapes où l'état intermédiaire compte.
- Validation complexe:[ La méthode peut valider toutes les entrées et lancer des erreurs si quelque chose n'est pas valide, empêchant la création d'objets brisés.
- APIs fluides: Vous voulez une interface "flue" ou "chainable" qui se lit comme un langage naturel.
- Croix des préoccupations :[ Lorsque la logique de construction est réutilisée à plusieurs endroits dans une base de code, le constructeur centralise cette logique.
Pour les objets simples avec peu de propriétés, un initialisateur avec des valeurs par défaut est souvent suffisant. Pour les objets qui ne nécessitent pas de configuration au-delà des bases, un constructeur ajoute des frais généraux inutiles.
Variations du modèle du constructeur
Il existe plusieurs variations communes du modèle de construction utilisé dans les projets Swift:
1. Constructeur classique
Comme indiqué ci-dessus, une classe séparée détient l'état et retourne un produit. C'est le plus flexible et permet une logique de validation et de configuration complexe.
2. Constructeur avec structure (type de valeur)
Puisque Swift encourage les types de valeurs, vous pouvez implémenter le constructeur comme une structure. Cependant, la chaîne de méthodes nécessite des méthodes mutantes, ce qui signifie que vous devez marquer des fonctions comme ou retourner une nouvelle copie de la structure. La dernière approche est plus fonctionnelle mais peut être moins efficace pour de nombreuses missions.
struct CustomViewConfigBuilder {
private var backgroundColor: UIColor = .white
// ...
func withBackgroundColor(_ color: UIColor) -> CustomViewConfigBuilder {
var copy = self
copy.backgroundColor = color
return copy
}
func build() -> CustomViewConfig {
return CustomViewConfig(backgroundColor: backgroundColor, ...)
}
}
3. Constructeur de résultats (Swift 5.4+)
Les constructeurs de résultats de Swift (aussi appelés constructeurs de fonctions) fournissent une syntaxe déclarative qui est liée conceptuellement au modèle de constructeur. Bien que ce ne soit pas un remplacement direct, les constructeurs de résultats peuvent être utilisés pour construire des objets complexes dans un style de langage spécifique au domaine (DSL).
Exemple : Constructeur de résultats personnalisés pour la configuration
@resultBuilder
struct ViewConfigBuilder {
static func buildBlock(_ components: ViewConfigComponent...) -> [ViewConfigComponent] {
return components
}
}
protocol ViewConfigComponent {
func apply(to builder: CustomViewConfigBuilder)
}
struct BackgroundColorComponent: ViewConfigComponent {
let color: UIColor
func apply(to builder: CustomViewConfigBuilder) {
builder.withBackgroundColor(color)
}
}
// Usage with @ViewConfigBuilder
let config = ViewConfigBuilder.buildBlock(
BackgroundColorComponent(color: .red),
CornerRadiusComponent(radius: 8)
)
Cette approche est plus avancée et la mieux réservée pour les DSL ou lorsque vous voulez faire respecter un ordre spécifique de configurations.
Cas d'utilisations réelles dans le monde dans le développement iOS
Configuration de la demande réseau
Les bibliothèques de réseautage doivent souvent construire des requêtes avec de nombreux paramètres optionnels : URL, méthode HTTP, en-têtes, corps, paramètres de requête, politique de cache, timeout, etc. Un constructeur simplifie cela.
class APIRequestBuilder {
private var url: URL
private var method: String = "GET"
private var headers: [String: String] = [:]
private var body: Data?
private var queryItems: [URLQueryItem] = []
init(url: URL) {
self.url = url
}
func setMethod(_ method: String) -> Self {
self.method = method
return self
}
func addHeader(key: String, value: String) -> Self {
headers[key] = value
return self
}
func setBody(_ data: Data) -> Self {
self.body = data
return self
}
func addQueryItem(name: String, value: String) -> Self {
queryItems.append(URLQueryItem(name: name, value: value))
return self
}
func build() -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method
request.allHTTPHeaderFields = headers
request.httpBody = body
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.queryItems = queryItems
request.url = components.url
}
return request
}
}
// Usage
let request = APIRequestBuilder(url: URL(string: "https://api.example.com/users")!)
.setMethod("POST")
.addHeader(key: "Content-Type", value: "application/json")
.setBody(try! JSONEncoder().encode(userData))
.build()
Configuration de l'entité de données de base
Les objets gérés par Core Data sont notoirement verbeux à créer. Un constructeur peut encapsuler la recherche et l'assignation de propriété.
class UserEntityBuilder {
private let context: NSManagedObjectContext
private var name: String = ""
private var email: String = ""
private var age: Int = 0
init(context: NSManagedObjectContext) {
self.context = context
}
func withName(_ name: String) -> Self {
self.name = name
return self
}
func withEmail(_ email: String) -> Self {
self.email = email
return self
}
func withAge(_ age: Int) -> Self {
self.age = age
return self
}
func build() -> User {
let user = NSEntityDescription.insertNewObject(forEntityName: "User", into: context) as! User
user.name = name
user.email = email
user.age = Int32(age)
return user
}
}
Gestion des erreurs dans les constructeurs
Parfois, la création d'objet doit échouer si la configuration est invalide. La méthode peut être lancée, ce qui est une manière propre d'appliquer les règles.
struct LoginConfig {
let username: String
let password: String
let serverURL: URL
}
class LoginConfigBuilder {
private var username: String?
private var password: String?
private var serverURL: URL?
func withUsername(_ username: String) -> Self {
self.username = username
return self
}
func withPassword(_ password: String) -> Self {
self.password = password
return self
}
func withServerURL(_ url: URL) -> Self {
self.serverURL = url
return self
}
func build() throws -> LoginConfig {
guard let username = username, !username.isEmpty else {
throw BuilderError.missingUsername
}
guard let password = password, password.count >= 8 else {
throw BuilderError.invalidPassword
}
guard let serverURL = serverURL else {
throw BuilderError.missingServerURL
}
return LoginConfig(username: username, password: password, serverURL: serverURL)
}
}
enum BuilderError: Error {
case missingUsername
case invalidPassword
case missingServerURL
}
// Usage
do {
let config = try LoginConfigBuilder()
.withUsername("jdoe")
.withPassword("secret1234")
.withServerURL(URL(string: "https://auth.example.com")!)
.build()
} catch {
print("Failed to build login config: \(error)")
}
Considérations relatives aux performances
Les constructeurs de Swift sont généralement légers, mais il y a quelques choses à garder à l'esprit :
- Mémorie : Chaque instance de constructeur détient une copie de toutes les propriétés jusqu'à ce que soit appelée. Pour les grandes configurations, envisager d'utiliser un constructeur de structure qui crée une copie uniquement sur la mutation (l'approche fonctionnelle).
- Méthod enchainement:[ Chaque appel retourne la même instance de constructeur (pour les classes) ou une nouvelle copie (pour les structures).Les constructeurs basés sur la classe sont bons; les constructeurs de structure peuvent en causer des copies supplémentaires, mais le compilateur optimise beaucoup de ceux qui sont absents.
- Coût de validation:[ Si la validation dans est coûteuse, envisager de la mettre en cache ou de la reporter, ou fournir une méthode de validation légère qui peut être appelée plus tôt.
- Utiliser en boucles:[ Si vous devez créer de nombreux objets similaires, évitez de recréer le constructeur à chaque fois. Réutiliser un constructeur et réinitialiser son état après chaque .
Comparaison : constructeur vs usine vs initialisateur direct
| Approach | Best For | Downside |
|---|---|---|
| Direct Initializer | Simple objects with few required parameters | Becomes unreadable with many optional parameters |
| Factory Method | Subclass selection or logic-based creation | Does not handle step-by-step configuration |
| Builder Pattern | Complex, configurable, and potentially immutable objects | More boilerplate; not suitable for trivial objects |
Le constructeur est complémentaire aux usines. Vous pourriez avoir une usine qui retourne un constructeur pré-configuré, puis laisser l'appelant le personnaliser plus loin.
Intégration avec SwiftUI et Combiner
Les constructeurs sont un ajustement naturel pour le style déclaratif de SwiftUI. Vous pouvez créer un constructeur qui construit un basé sur la configuration.
struct CardViewConfig {
let title: String
let subtitle: String
let iconName: String
let backgroundColor: Color
let tapAction: () -> Void
}
class CardViewConfigBuilder {
private var title: String = ""
private var subtitle: String = ""
private var iconName: String = "star"
private var backgroundColor: Color = .white
private var tapAction: (() -> Void)? = nil
func withTitle(_ title: String) -> Self {
self.title = title
return self
}
func withSubtitle(_ subtitle: String) -> Self {
self.subtitle = subtitle
return self
}
func withIcon(_ name: String) -> Self {
self.iconName = name
return self
}
func withBackground(_ color: Color) -> Self {
self.backgroundColor = color
return self
}
func withTapAction(_ action: @escaping () -> Void) -> Self {
self.tapAction = action
return self
}
func build() -> CardViewConfig {
return CardViewConfig(
title: title,
subtitle: subtitle,
iconName: iconName,
backgroundColor: backgroundColor,
tapAction: tapAction ?? {}
)
}
}
// Usage in a SwiftUI view
struct ContentView: View {
var body: some View {
let config = CardViewConfigBuilder()
.withTitle("Welcome")
.withSubtitle("Get started with our app")
.withIcon("hand.wave")
.withBackground(.blue.opacity(0.1))
.withTapAction { print("Tapped!") }
.build()
CardView(config: config)
}
}
struct CardView: View {
let config: CardViewConfig
var body: some View {
VStack {
Image(systemName: config.iconName)
.font(.largeTitle)
Text(config.title)
.font(.headline)
Text(config.subtitle)
.font(.subheadline)
}
.padding()
.background(config.backgroundColor)
.cornerRadius(10)
.onTapGesture(perform: config.tapAction)
}
}
Pièges courants et comment les éviter
- Pour éviter de retourner soi-même :[ Assurez-vous que chaque méthode de setter retourne le type de constructeur. Utilisez si vous voulez permettre aux appelants d'ignorer la déclaration.
- État partagé mixte:[ Si votre constructeur est utilisé sur des threads, ajoutez la sécurité des threads (p. ex., utilisez une file d'attente série privée ou une sémantique de copie à l'écriture).
- Sur-ingénierie: N'appliquez pas le modèle de constructeur à chaque objet. Si votre objet n'a que deux ou trois propriétés, un initialisateur simple avec des valeurs par défaut est plus clair et nécessite moins de code.
- ]Les constructeurs qui ne valident pas peuvent produire des objets dans un état invalide. Vérifiez toujours les hypothèses au point de sécurité le plus tôt possible.
- Nommage de méthode non cohérent:[ Utilisez un préfixe cohérent comme ou pour rendre l'API reconnaissable. Certaines équipes préfèrent , , etc.
Ressources extérieures
- Builder Pattern sur Wikipedia
- Documentation d'Apple sur les constructeurs de résultats
- Le modèle de constructeur dans Swift par John Sundell
- Modèle de builder sur le guru de refactoring
Conclusion
En séparant la logique de construction du produit final, vous pouvez créer des API expressives faciles à utiliser et difficiles à utiliser. Que vous configuriez des vues, des demandes de réseau ou des modèles de domaine de construction, le modèle de constructeur vous aide à garder votre code propre et vos objets valides. Commencez par le constructeur classique basé sur la classe, puis explorez les constructeurs de type valeur ou les constructeurs de résultats à mesure que vos besoins augmentent. Avec la pratique, vous trouverez le juste équilibre entre granularité et simplicité, rendant votre code Swift plus lisible et professionnel.