Ho una piccola applicazione in Ruby che risolve un problema simile usando il modello di progettazione di porte e adattatori (esagonali) di Alistair Cockburn. L'applicazione invia avvisi ai singoli destinatari sulla loro piattaforma di comunicazione preferita (ad esempio SMS, e-mail, Twitter, ecc.). Ecco come appare (in Ruby / Rails):
class Recipient < ActiveModel
# Recipient model has two attributes:
# - channel: one of sms, email, twitter, messenger, whatsapp
# - address: one of phone number, email address, twitter handle, etc.
NOTIFIER_CLASSES = {
sms: SMSNotifier,
email: EmailNotifier,
twitter: TwitterNotifier,
messenger: FacebookMessengerNotifier,
whatsapp: WhatsNotifier
}.freeze
def notify(message)
notifier.notify(address, message)
end
private
def notifier
NOTIFIER_CLASSES[channel].new(address)
end
end
Qui, cerco di creare un'istanza del notificatore corretto e di passargli gli argomenti corretti. Mi piace così (in RSpec):
describe Recipient do
subject(:recipient) { described_class.new(recipient_attributes) }
let(:recipient_attributes) { { channel: channel, address: address } }
let(:channel) { 'some channel' }
let(:address) { 'some address' }
let(:message) { 'some message' }
describe "#notify" do
subject(:notify) { recipient.notify(message) }
context 'when channel is sms' do
let(:channel) { :sms }
before do
allow(SMSNotifier).to receive(:notify)
notify
end
it 'uses the correct notifier with the correct attributes' do
expect(SMSNotifier).to have_received(:notify).with(address, message)
end
end
context 'when channel is email' do
let(:channel) { :email }
before do
allow(EmailNotifier).to receive(:notify)
notify
end
it 'uses the correct notifier with the correct attributes' do
expect(EmailNotifier).to have_received(:notify).with(address, message)
end
end
end
end
E così via ... (Nota: in realtà, questi test verrebbero prosciugati con esempi condivisi.)
Tutti i notificanti definiscono lo stesso metodo notify
(digitazione anatra in Ruby, sebbene si voglia utilizzare un'interfaccia in un linguaggio tipizzato staticamente). Sembrano così:
class SMSNotifier
FROM = '+18005551212'.freeze
def notify(address, message)
messages.create(from: FROM, to: address, body: message)
end
private
def messages
client.api.account.messages
end
def client
Twilio::REST::Client.new(ENV['ACCOUNT_SID'], ENV['AUTH_TOKEN'])
end
end
class EmailNotifier
SENDGRID_ENDPOINT = 'https://api.sendgrid.com/v3/mail/send'.freeze
FROM = '[email protected]'.freeze
def notify(address, message)
RestClient.post(
SENDGRID_ENDPOINT,
{
to: [{ email: address }],
subject: 'Alert',
content: [{ value: message, type: 'text/plain' }]
},
'Authorization' => "Bearer #{ENV['SENDGRID_API_KEY']}"
)
end
end
Come puoi vedere, gli interni sono abbastanza diversi per ciascun Notifier, a seconda dell'API sottostante. In questi casi, utilizzo i mock per assicurarmi di effettuare le chiamate API corrette con i parametri corretti. Confido che queste API siano completamente testate. Non ho bisogno di fare un round trip in un test unitario.
describe SMSNotifier do
subject(:notifier) { described_class.new }
describe "#notify" do
subject(:notify) { notifier.notify(address, message) }
let(:address) { '+12345678900' }
let(:message) { 'some message' }
let(:client) { instance_double(Twilio::REST::Client, api: api) }
let(:api) { instance_double('api', account: account) }
let(:account) { instance_double('account', messages: messages) }
let(:messages) { instance_double('messages', create: true) }
before do
allow(Twilio::REST::Client).to receive(:new).and_return(client)
notify
end
it 'uses the correct notifier with the correct attributes' do
expect(Twilio::REST::Client).to have_received(:new).with(account_sid, auth_token)
end
it 'makes the correct api call with the correct attributes' do
expect(messages).to have_received(:create).with(address, message)
end
end
end
describe EmailNotifier do
subject(:notifier) { described_class.new }
describe "#notify" do
subject(:notify) { notifier.notify(address, message) }
let(:address) { '[email protected]' }
let(:message) { 'some message' }
let(:email_attributes) do
{
to: [{ email: address }],
subject: 'Alert',
content: [{ type: 'text/plain', value: message }]
}
end
let(:headers) { { 'Authorization' => "Bearer sendgrid_api_key" } }
before do
allow(RestClient).to receive(:post)
notify
end
it 'uses the correct notifier with the correct attributes' do
expect(RestClient).to have_received(:post).with(
'https://api.sendgrid.com/v3/mail/send',
email_attributes,
headers
)
end
end
end
Quindi, il modello si riduce a porte e adattatori, dove i Notificatori sono le porte e le API Twilio e SendGrid sono adattatori per i servizi specifici.