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

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.