Table of Contents
Le applicazioni moderne richiedono sistemi di notifica robusti in grado di fornire messaggi attraverso più canali — e-mail, SMS, notifiche push, Slack, e altro ancora. La sfida principale è la costruzione di un sistema che rimane flessibile e manutenbile come nuovi canali emerge, senza richiedere una riscrittura del codice esistente. Il modello di progettazione di fabbrica astratto offre una soluzione test-tempo fornendo un'interfaccia per la creazione di famiglie di oggetti correlati senza accoppiare il codice client alle implementazioni concrete.
Capire il modello di fabbrica astratto
Il modello di fabbrica astratta è uno dei modelli originali creati dalla banda dei quattro. Fornisce un'interfaccia per creare famiglie di oggetti correlati o dipendenti senza specificare le loro classi di cemento. Il modello è particolarmente utile quando:
- Un sistema deve essere indipendente da come i suoi prodotti sono creati, composti o rappresentati.
- Un sistema dovrebbe essere configurato con una delle famiglie multiple di prodotti.
- Si desidera applicare la coerenza tra i prodotti che appartengono insieme.
- Stai aggiungendo nuove famiglie di prodotti frequentemente, e si desidera evitare di modificare il codice esistente.
Partecipanti al Modello
- AbstractFactory[]: dichiara un'interfaccia per un insieme di metodi di creazione, ognuno che ritorna un prodotto astratto.
- ConcreteFactory[]: implementa i metodi di creazione per produrre prodotti in cemento.
- AbstractProduct[[]]: dichiara un'interfaccia per un tipo di oggetto del prodotto.
- ConcreteProduct[[]]: definisce un oggetto prodotto che implementa l'interfaccia AbstractProduct.
- Client[]: utilizza solo le interfacce dichiarate da AbstractFactory e AbstractProduct.
Nel contesto di un sistema di notifica, [Supponi] ] corrisponde all'interfaccia e Prodotto di cemento corrisponde a , [regolazione, ecc [[FLT]]]
Perché Kotlin per l'implementazione del modello di progettazione?
Kotlin porta diverse caratteristiche linguistiche moderne che rendono l'implementazione del modello di fabbrica astratto più concisa e più sicura di Java:
- Le classi sigillate[[]] rappresentano un insieme chiuso di tipi di notifica, consentendo espressioni esaustive .
- Le classi di dati[]] per ridurre la caldaia nelle implementazioni dei prodotti.
- Dichiari oggetti[]] per creare fabbriche singleton senza codice aggiuntivo.
- Funzioni di ordine superiore[[]] per sostituire le interfacce di fabbrica con le fabbriche di agnello, quando necessario.
- Null safety[] e type inference[]] per ridurre gli errori di runtime.
Queste caratteristiche consentono di scrivere un sistema di fabbrica che non è solo flessibile ma anche sicuro di compilazione, riducendo la necessità di controlli di tipo di riflessione o runtime.
Costruire un sistema di notifica core con la fabbrica astratta
Passiamo attraverso un’implementazione passo-passo a Kotlin. Iniziamo con una versione minimale e poi lo espanderemo per gestire la complessità del mondo reale.
1. Definire l'interfaccia del prodotto astratto
Ogni notifica deve esporre un metodo ].Includiamo anche una proprietà per supportare il routing e il log-in.
interface Notification {
val type: String
fun send(message: String): Result
}
data class Result(val success: Boolean, val error: String? = null)
2. Implement Prodotti in calcestruzzo
Utilizzando la sintassi concisa di Kotlin, creiamo implementazioni concrete per le notifiche via email, SMS e push.Per il realismo, ogni implementazione conterrebbe le chiamate API reali, ma qui li simulamo.
class EmailNotification : Notification {
override val type = "email"
override fun send(message: String): Result {
// Simulate sending email via SMTP or service like SendGrid
println("Sending email: $message")
return Result(true)
}
}
class SmsNotification : Notification {
override val type = "sms"
override fun send(message: String): Result {
// Simulate sending SMS via Twilio or similar
println("Sending SMS: $message")
return Result(true)
}
}
class PushNotification : Notification {
override val type = "push"
override fun send(message: String): Result {
// Simulate sending push via Firebase Cloud Messaging
println("Sending push notification: $message")
return Result(true)
}
}
3. Creare l'interfaccia di fabbrica astratta
In sistemi più complessi, si potrebbero avere più metodi di creazione (ad esempio, , ]), ma per una semplice fabbrica lo teniamo generico.
interface NotificationFactory {
fun createNotification(): Notification
}
4. Implementare le fattorie di calcestruzzo
Ogni fabbrica è responsabile di istantanare esattamente un tipo di prodotto. Utilizzando una dichiarazione rende la fabbrica un singolo, che è spesso sufficiente.
object EmailNotificationFactory : NotificationFactory {
override fun createNotification(): Notification = EmailNotification()
}
object SmsNotificationFactory : NotificationFactory {
override fun createNotification(): Notification = SmsNotification()
}
object PushNotificationFactory : NotificationFactory {
override fun createNotification(): Notification = PushNotification()
}
5. Utilizzare le fabbriche dal codice cliente
Il cliente (ad esempio, un servizio di notifica) accetta un [ e invia un messaggio senza conoscere la classe di prodotto concreta.
fun sendNotification(factory: NotificationFactory, message: String) {
val notification = factory.createNotification()
val result = notification.send(message)
if (!result.success) {
println("Failed to send ${notification.type}: ${result.error}")
}
}
fun main() {
// In a real app, the factory choice would come from configuration or user preferences
sendNotification(EmailNotificationFactory, "Welcome to our service!")
sendNotification(SmsNotificationFactory, "Your code is 1234")
sendNotification(PushNotificationFactory, "New message from Bob")
}
Questa configurazione del nucleo dimostra già la potenza del modello: l’aggiunta di un nuovo canale (ad esempio, Slack) richiede solo una nuova classe di prodotto e una nuova fabbrica, senza modifiche al codice client o alle fabbriche esistenti.
Estendere il sistema con nuovi canali di notifica
Supponiamo di dover aggiungere notifiche Slack. Creiamo e . Il metodo ] interagirebbe con l'API Web di Slack.
class SlackNotification(private val webhookUrl: String) : Notification {
override val type = "slack"
override fun send(message: String): Result {
// POST to webhookUrl
return if (webhookUrl.isNotBlank()) {
println("Sending Slack message: $message")
Result(true)
} else {
Result(false, "Webhook URL not configured")
}
}
}
object SlackNotificationFactory : NotificationFactory {
override fun createNotification(): Notification = SlackNotification(CONFIG_SLACK_WEBHOOK)
}
Il codice cliente rimane intatto, il segno distintivo del principio aperto/caso in azione: il sistema è aperto per l'estensione ma chiuso per la modifica.
Considerazioni avanzate per i sistemi di produzione-Ready
Mentre il modello di base funziona, i sistemi di notifica del mondo reale richiedono più sofisticazione.
Classi sigillate per la selezione di fabbrica esaustiva
Invece di passare una fabbrica astratta, è possibile utilizzare Kotlin []] per rappresentare i tipi di notifica e lasciare che il cliente scelga quale fabbrica usare tramite un espressione. Il compilatore ti costringe a gestire ogni tipo quando si aggiunge un nuovo canale di notifica.
sealed class NotificationType {
object Email : NotificationType()
object SMS : NotificationType()
object Push : NotificationType()
data class Slack(val webhookUrl: String) : NotificationType()
}
fun createNotification(type: NotificationType): Notification = when (type) {
NotificationType.Email -> EmailNotification()
NotificationType.SMS -> SmsNotification()
NotificationType.Push -> PushNotification()
is NotificationType.Slack -> SlackNotification(type.webhookUrl)
}
Questo approccio elimina l'interfaccia di fabbrica separata e utilizza invece una singola funzione che restituisce il prodotto corretto. È più semplice per i sistemi di piccole e medie e ti dà la sicurezza di compilazione.
Inviare una notifica asincrona
La maggior parte dei canali di notifica comporta chiamate di rete. Il ritorno è sincronizzato e non è pratico. Invece, fare il metodo sospendere o restituire una coroutine.
interface Notification {
suspend fun send(message: String): Result
}
class EmailNotification : Notification {
override suspend fun send(message: String): Result {
// Use Http client to send email
return withContext(Dispatchers.IO) {
// actual API call
Result(true)
}
}
}
Il codice client può quindi chiamare da un campo di applicazione coroutine:
suspend fun sendNotification(factory: NotificationFactory, message: String) {
val notification = factory.createNotification()
val result = notification.send(message)
// handle result
}
Integrazione dell'iniezione di dipendenza
Invece di fabbriche in codice duro, utilizzare un framework di iniezione di dipendenza come Koin o Dagger per legare le fabbriche per interfacciarsi [.
// Koin module
val notificationModule = module {
factory<NotificationFactory>("email") { EmailNotificationFactory }
factory<NotificationFactory>("sms") { SmsNotificationFactory }
factory<NotificationFactory>("push") { PushNotificationFactory }
}
Quindi ottenere la fabbrica corretta da Koin a runtime in base a una chiave di configurazione.
Gestione degli errori e Retries
Avvolgere la fabbrica e la creazione di prodotti con la gestione degli errori. È inoltre possibile implementare un meccanismo di riprovazione utilizzando il costruttore Kotlin coroutines .
suspend fun sendWithRetry(factory: NotificationFactory, message: String, maxRetries: Int = 3): Result {
repeat(maxRetries) { attempt ->
try {
val notification = factory.createNotification()
return notification.send(message)
} catch (e: Exception) {
if (attempt == maxRetries - 1) {
return Result(false, e.message)
}
delay(1000L * (attempt + 1)) // exponential backoff
}
}
return Result(false, "Max retries exceeded")
}
Template e Personalizzazione
Le notifiche richiedono spesso la templatura (ad esempio, e-mail HTML, contenuto dinamico di SMS) e la fabbrica può restituire un oggetto di notifica preconfigurato con un motore di modello.
class TemplatedEmailNotification(private val template: String) : Notification {
override val type = "email"
override fun send(message: String): Result {
val body = processTemplate(template, mapOf("content" to message))
// Send email with rendered body
return Result(true)
}
}
La fabbrica potrebbe quindi creare diverse varianti di prodotto in base a parametri aggiuntivi.
Testare il sistema di notifica
Uno dei maggiori vantaggi del modello di fabbrica astratta è la provabilità. È possibile creare facilmente fabbriche di mock che il test di ritorno raddoppia.
class MockNotification : Notification {
var sentMessage: String? = null
override val type = "mock"
override fun send(message: String): Result {
sentMessage = message
return Result(true)
}
}
object MockNotificationFactory : NotificationFactory {
val mock = MockNotification()
override fun createNotification(): Notification = mock
}
// In test:
val factory = MockNotificationFactory
sendNotification(factory, "Test message")
assertEquals("Test message", factory.mock.sentMessage)
È inoltre possibile scrivere test di unità che verificano la fabbrica restituisce il tipo di prodotto corretto, utilizzando Kotlin abbinatori di stile.
Configurazione delle fabbriche a Runtime
In un'architettura microservice, la scelta del canale di notifica può provenire da un file di configurazione, da una variabile di ambiente o da un database.
object NotificationFactoryRegistry {
private val factories = mutableMapOf<String, NotificationFactory>()
fun register(channel: String, factory: NotificationFactory) {
factories[channel] = factory
}
fun getFactory(channel: String): NotificationFactory? = factories[channel]
}
Poi popula il registro all'avvio dell'applicazione da o un'occhiata di database.Questo decouples la decisione di quale fabbrica usare completamente dal codice client.
Confronto con il modello metodo di fabbrica
Il modello di fabbrica astratta è spesso confuso con il modello più semplice del metodo di fabbrica. La differenza chiave è che il metodo di fabbrica si occupa di un singolo prodotto, mentre la fabbrica astratta si occupa di famiglie di prodotti correlati. Nel nostro sistema di notifica, se si dispone di più prodotti correlati per canale (ad esempio, un notificante e un logger specifico a quel canale), si potrebbe utilizzare la fabbrica astratta.
Mettere tutto insieme: un esempio reale-mondo
Immaginate una piattaforma SaaS che permette agli utenti di scegliere le preferenze di notifica: e-mail, SMS, push o Slack. Il sistema legge le preferenze dell'utente da un database, cerca la fabbrica corrispondente (sia iniettato che ottenuto da un registro), e invia la notifica. Utilizzando il modello di fabbrica astratto, aggiungendo un nuovo canale (cioè, Microsoft Teams) richiede semplicemente una nuova classe di prodotto e una nuova )] – e registrando le dichiarazioni di registro.
// Preference model
data class UserPreference(
val userId: String,
val channel: String // "email", "sms", "push", "slack"
)
class NotificationDispatcher(private val registry: NotificationFactoryRegistry) {
suspend fun dispatch(preference: UserPreference, message: String) {
val factory = registry.getFactory(preference.channel)
?: throw IllegalArgumentException("Unknown channel: ${preference.channel}")
val notification = factory.createNotification()
notification.send(message)
}
}
Questo design non è solo flessibile ma anche altamente testabile: è possibile mock il registro e verificare che la fabbrica corretta è utilizzata per ogni canale.
Migliori Pratiche per l'attuazione della fabbrica astratta in Kotlin
- Preferire le classi sigillate sulle definizioni di interfaccia sciolte[] quando il set di prodotti è chiuso e conosciuto al momento della compilazione.
- Utilizzare oggetti di compagnia o dichiarazioni di oggetti[[] per le fabbriche che non hanno stato.
- I valori dei parametri predefiniti di leva[] nei costruttori di prodotti per fornire predefinizioni ragionevoli senza sovraccaricare le fabbriche.
- Tenere sotto controllo l'interfaccia di fabbrica minimal[[]. Se vi trovate aggiungendo molti metodi di creazione, considerare l'utilizzo di un modello di costruttore o di un oggetto di configurazione.
- Make ] funzioni sospese[] per evitare filetti bloccanti.
- Documenti le responsabilità[] di ogni fabbrica e del suo prodotto. Poiché il modello aggiunge indiretto, il nome chiaro e la documentazione sono essenziali.
Risorse esterne
Per approfondire la vostra comprensione del modello di fabbrica astratta e la sua attuazione in Kotlin, fare riferimento alle seguenti risorse:
- Stile di fabbrica astratto – Guru di refactoring[] – una spiegazione completa con diagrammi UML ed esempi di codice.
- Kotlin Sealed Classs[[] – documentazione ufficiale su come le classi sigillate possono modellare gerarchie di classe limitate.
- Kotlin in Action[[] – il libro definitivo su Kotlin, comprese le implementazioni del modello di progettazione (il capitolo 11 copre i modelli).
Combinando il modello di fabbrica astratta collaudato con le caratteristiche linguistiche moderne di Kotlin, è possibile costruire un sistema di notifica che non è solo flessibile e scalabile ma anche sicuro e manutenbile. Se si supportano due canali o venti, il modello assicura che l'aggiunta di nuove funzionalità non si senta mai come una riscrittura, diventa un'estensione prevedibile e a basso rischio.