Comment implémenter une logique de filtrage à l’aide de modèles de conception
Dans ce tutoriel, nous allons apprendre à implémenter le filtre à sélection multiple en utilisant 2 modèles de conception : stratégie et décorateur.
Le tutoriel sera divisé en 2 parties :
- Dans la première partie, nous nous concentrerons sur l’implémentation de la logique de filtrage.
- Dans la seconde partie, nous traiterons de l’UI (à venir).
Commençons!
Imaginez que nous développions une application pour un détaillant en ligne de smartphones et que notre propriétaire de produit ait l’idée de filtrer les smartphones dans un catalogue.
Nous sommes censés créer une solution robuste et évolutive. Cela signifie que si nous devons ajouter une option supplémentaire au filtre, ce ne sera pas un peu compliqué.
En dehors de cela, le filtre doit être un filtre à sélection multiple, en supposant que l’utilisateur peut opter pour plusieurs options simultanément.
Compte tenu de ces exigences, nous commençons à élaborer une solution.
Dans un premier temps, nous commençons par définir un Téléphone fixe maquette:
struct Phone {
let model: String
let price: Int
let screenSize: Double
let processor: String
let memory: Int
let diskSpace: Int
let color: String
}
Deuxièmement, nous devons déterminer un spécification modèle qui sera utilisé pour filtrer une spécification concrète de smartphone :
enum Specification {
case model(String)
case price(Int)
case screenSize(Double)
case processor(String)
case memory(Int)
case diskSpace(Int)
case color(String)
}
Chaque cas d’énumération contient la valeur associée, qui représente le type sous-jacent de la spécification concrète.
Maintenant que les modèles sont tous définis, nous sommes prêts à implémenter la fonctionnalité de filtrage.
Nous commençons par créer une classe de contexte et des classes conformes à un modèle de conception de stratégie et sont responsables du filtrage de chaque spécification de smartphone individuellement.
Stratégie – un modèle de conception comportementale qui définit une famille d’algorithmes similaires et place chacun d’eux dans sa propre classe, après quoi les algorithmes peuvent être échangés au moment de l’exécution.
Source: Modèles de conception : plongez dans les modèles de conception
Au départ, nous devrions esquisser un protocole que chaque filtre en béton sera conforme à :
protocol FilterStrategy {
func filter(phones: [Phone], by specs: [Specification]) -> [Phone]
}
Il ne contiendra qu’une seule méthode, qui a deux paramètres d’entrée :
➊ téléphones — le tableau des téléphones à filtrer.
➋ specs — le tableau de spécifications sur la base duquel le filtrage aura lieu.
Après cela, nous passons à la mise en œuvre d’un classe de contexte de filtre qui est responsable de la gestion des filtres à béton :
final class Filter {
private var strategy: FilterStrategy // 1init(strategy: FilterStrategy) {
self.strategy = strategy
}
func update(strategy: FilterStrategy) { // 2
self.strategy = strategy
}
func applyFilter(to phones: [Phone], withSpecs specs: [Specification]) -> [Phone] {
return strategy.filter(phones: phones, by: specs) // 3
}
}
C’est cette classe qui est capable d’échanger la stratégie à l’exécution.
➊ Il conserve la référence au filtre courant.
➋ Le mettre à jour La fonction, à son tour, est utilisée pour changer la stratégie actuelle en une nouvelle.
➌ Et puis on peut interagir avec le filtre courant via une interface définie dans le protocole.
Après avoir défini le contexte, nous passons enfin à la création des filtres concrets.
Note
Nous n’examinerons qu’un seul filtre concret puisque les détails de mise en œuvre sont les mêmes pour tous les filtres. La liste complète des filtres est disponible ici.
Jetons un coup d’oeil au filtre de prix la mise en oeuvre:
final class PriceFilter: FilterStrategy {
func filter(phones: [Phone], by specs: [Specification]) -> [Phone] {
let priceSpecs = Set(specs.compactMap { (spec) -> Int? in // 1
if case let .price(price) = spec { return price }; return nil // 2
})
return phones.filter { priceSpecs.contains($0.price) } // 3
}
}
➊ Nous utilisons compactMap
afin de ne collecter que les paramètres de prix qui serviront au filtrage ultérieur et de s’assurer qu’on en a. Pour permettre une recherche rapide, nous encapsulons le tableau dans Set.
➋ si cas laisser La syntaxe nous permet de déballer uniquement une valeur associée à un cas d’énumération spécifique. Plus d’informations à ce sujet peuvent être trouvées ici.
➌ Enfin, nous appliquons un filtre à la collection de téléphone et renvoyons le résultat.
Après avoir terminé l’implémentation des filtres, nous passons à la création d’un décorateur de filtres.
Afin de permettre la possibilité d’utiliser plusieurs filtres à la fois, nous devons créer une classe qui s’aligne sur le modèle de conception de décorateur.
Décorateur – un modèle de conception structurelle qui vous permet d’ajouter dynamiquement de nouvelles fonctionnalités aux objets en les enveloppant dans des « wrappers » utiles.
Source: Modèles de conception : plongez dans les modèles de conception
Pour commencer, nous définissons un protocole auquel chaque décorateur se conformera :
protocol PhoneFilter {
func filter(phones: [Phone], by specs: [Specification]) -> [Phone]
}
Il a une méthode et la même signature de méthode que le FilterStrategy
méthode.
Ensuite, nous créons un classe de base qui est conforme à la PhoneFilter
protocole:
class PhoneFilterDecorator: PhoneFilter {
private let phoneFilter: PhoneFilter // 1init(phoneFilter: PhoneFilter) {
self.phoneFilter = phoneFilter
}
func filter(phones: [Phone], by specs: [Specification]) -> [Phone] { // 2
return phoneFilter.filter(phones: phones, by: specs)
}
}
Sa tâche principale est de spécifier l’interface d’emballage pour chaque décorateur concret.
- La classe contient l’objet enveloppé conforme à la
PhoneFilter
protocole à l’intérieur duphoneFilter
constante. - Les
filter
sera remplacée par les sous-classes pour fournir une logique de filtrage personnalisée.
Ensuite, nous créons un PhoneBaseFilter
classe qui jouera le rôle d’un filtre d’espace réservé :
class PhoneBaseFilter: PhoneFilter {
func filter(phones: [Phone], by specs: [Specification]) -> [Phone] {
return phones
}
}
Nous conformons cette classe à la PhoneFilter
protocol, et comme valeur de retour, nous fournissons le même tableau de téléphones qui a été passé à la fonction.
Nous utiliserons le PhoneBaseFilter
class comme filtre par défaut.
De la même manière que pour la stratégie, nous approfondirons les détails d’implémentation uniquement pour une classe de décorateur.
Passons en revue le PhonePriceFilter
classer:
final class PhonePriceFilter: PhoneFilterDecorator { // 1
override func filter(phones: [Phone], by specs: [Specification]) -> [Phone] { // 2
let filter = Filter(strategy: PriceFilter()) // 3
let appliedFilterResult = super.filter(phones: phones, by: specs) // 4
let filteredPhones = filter.applyFilter(to: appliedFilterResult, withSpecs: specs) // 5
return filteredPhones
}
}
➊ Il est hérité du décorateur de base — PhoneFilterDecorator
.
➋ Il remplace la méthode de filtrage.
➌ La logique de filtrage est fournie par la stratégie de filtrage.
➍ Nous définissons le tableau filtré de téléphones sur appliedFilterResult
constante en invoquant la méthode du super filtre.
Cela signifie que si nous passons, disons, le PhoneDiskSpaceFilter
à la PhonePriceFilter
initializer, le tableau initial de téléphones fourni sera d’abord filtré avec les conditions d’espace disque, puis le résultat sera transmis à l’initialisateur PhonePriceFilter
pour un filtrage plus poussé.
➎ Nous utilisons le filtre actuel (PriceFilter) afin de filtrer le résultat du filtre précédent (par exemple DiskSpaceFilter), qui est conservé dans le appliedFilterResult
constante, donc on passe le appliedFilterResult
à la applyFilter
méthode.
C’est tout pour la partie décorateur, passons donc à l’exemple d’utilisation.
On commence par créer un fausses données qui servira au filtrage :
var phones = [
Phone(
model: "iPhone 14",
price: 799,
screenSize: 6.1,
processor: "Apple A15 Bionic",
memory: 6,
diskSpace: 128,
color: "Midnight"
),
Phone(
model: "iPhone 14 Plus",
price: 899,
screenSize: 6.7,
processor: "Apple A15 Bionic",
memory: 6,
diskSpace: 256,
color: "Starlight"
)
...
]
Une fois cela fait, nous sommes maintenant prêts à mettre notre filtre en action.
Créons un éventail de spécifications:
private let specifications: [Specification] = [
.diskSpace(256),
.diskSpace(512),
.color("Starlight"),
.color("Space Black"),
.price(1299)
]
C’est ce tableau qui sera rempli lorsque l’utilisateur sélectionnera les options dans l’interface utilisateur.
Il ne reste plus qu’à créer une chaîne de décorateurs dans l’ordre souhaité.
Dans cet exemple, nous avons créé un chaîne de trois décorateurs:
override func viewDidLoad() {
super.viewDidLoad()let phoneColorFilter = PhoneColorFilter(phoneFilter: PhoneBaseFilter()) // 1
let phoneDiskSpaceFilter = PhoneDiskSpaceFilter(phoneFilter: phoneColorFilter) // 2
let phonePriceFilter = PhonePriceFilter(phoneFilter: phoneDiskSpaceFilter) // 3
dataSource.phones = phonePriceFilter.filter(
phones: dataSource.phones,
by: specifications
)
tableView.reloadData()
}
Remarquez comment chaque décorateur est inséré l’un dans l’autre. C’est la puissance du motif décorateur.
- Le filtre de couleur produira le résultat suivant :
- Ensuite, il sera passé au filtre d’espace disque et le résultat sera :
- En fin de compte, le résultat de l’étape précédente sera transmis au filtre de prix et le résultat final sera :
L’utilisation de modèles de conception nous permet de créer une solution flexible qui facilite l’ajout de nouvelles options de filtrage sans casser celles qui existent déjà.
Merci d’avoir lu! Laissez un applaudissement si vous avez aimé le tutoriel et restez à l’écoute pour la deuxième partie où nous apprendrons comment attacher la logique de filtrage à l’interface utilisateur.
Le code source de ce projet se trouve dans mon dépôt GitHub :