modello di progettazione per comportamento condizionale a seconda dell'ID client

3

Non so se ci sia effettivamente un modello di design, o più insieme, o forse quello che sto chiedendo è anti-pattern.

Contesto (semplificato): la mia azienda vende servizi SaaS (tramite un'API) a diversi clienti, che a loro volta vendono questo servizio agli utenti finali. Ogni client utilizza il servizio, ma alcuni vogliono comportamenti personalizzati in diverse funzionalità. Ovviamente, il comportamento personalizzato dovrebbe verificarsi solo per quel cliente specifico. L'utente finale è collegato a un cliente specifico, quindi quando si utilizza il servizio, sappiamo quale sia l'identificativo del client, quindi quale comportamento deve avere sulla funzione richiesta.

La mia domanda è: mantenendo una sola base di codice, come faccio a gestire comportamenti diversi a seconda dell'identificatore del client?

L'attuale implementazione è ovviamente ingenua e sporca (semplificata):

function someFeature() {
  // common code

  if(clientId === 'someClient') {
  // does something
  } else if(clientId === 'otherClient') {
   // does something else
  } // else if ... for every other custom behaviour, or on other parts of the feature

  // common code
}

Ho letto degli schemi di progettazione, ma sembrano tutti concentrati su parti del mio problema, e non vedo come usarli tutti insieme ...

Se dovessi scavare una soluzione probabilmente andrei con un'implementazione di Bridge e una Factory per instanciare varie classi (per ogni client) che implementerebbero una stessa funzionalità in modo diverso (comportamento personalizzato di ogni cliente). Tenendo presente questo, quando un utente vuole utilizzare una funzione, la classe appropriata, grazie alla fabbrica, verrà instatata e utilizzata con il comportamento personalizzato desiderato.

Questa implementazione sembra il modo corretto per farlo, ma penso che incoraggerebbe la duplicazione del codice (per i bit di "codice comune") e richiederebbe ai programmatori di creare forse migliaia di nuove interfacce / classi per ogni funzionalità moltiplicata dal comportamento personalizzato di ogni cliente, a volte solo per una semplice riga di codice personalizzato.

Quale pensi che sarebbe il modo corretto per risolvere il mio problema?

    
posta Strannch 01.03.2018 - 19:08
fonte

6 risposte

3

Pensare a quali schemi di progettazione usare di solito non è un esercizio fruttuoso. Pensa a quali proprietà dovrebbero avere le tue soluzioni.

Ovviamente avere se le istruzioni che controllano l'ID client cosparso nel codice non si adatta bene a molti clienti. Probabilmente vorrai molti clienti, quindi vuoi aggiungerne uno nuovo per essere semplice. Quindi vorrai tutta la configurazione del client in un unico posto, il più possibile.

Inoltre, forse in futuro qualcuno vorrà che una funzionalità funzioni come client A per alcuni utenti e come client B per gli altri. O in base all'ora del giorno. Quindi probabilmente non vuoi che la funzione sia responsabile della conoscenza delle opzioni che dovrebbe applicare. Il codice per decidere che dovrebbe essere al di fuori della funzionalità e passare le opzioni nella funzionalità.

  var customerOptions={
  surfaceConsistencyPreference : 'springy',
  staleDataSweepingPolicy      : 'draconian'
  }


//...in some other file
function someFeature(surfaceConsistencyPreference) {
  // common code

  if(surfaceConsistencyPreference === 'mushy') {
  // does something
  } else if(surfaceConsistencyPreference === 'springy') {
   // does something else
  } // else if ... for every other custom behaviour, or on other parts of the feature

  // common code
}

Il principio generale è di mantenere una funzione dipendente il meno possibile. La funzione someFeature non ha bisogno di dipendere da un clientID - probabilmente non dovrebbe sapere di avere clienti. Ha bisogno di sapere quale comportamento personalizzato utilizzare. Quindi, passatelo e non dovrete preoccuparvi di questo, e non conoscerete in modo sottile come vengono determinati i comportamenti personalizzati replicati su tutta l'applicazione in un modo che ne rende molto difficile la modifica.

Il modo in cui si passa da un file di configurazione del cliente a passare il valore a una funzione è troppo specifico per l'applicazione perché io possa avere qualcos'altro da dirti.

    
risposta data 01.03.2018 - 20:07
fonte
3

Il comportamento specifico può essere incapsulato?

Se il comportamento è un insieme correlato di azioni utilizzate in più luoghi, utilizza il modello di strategia . Il tuo cliente speciale userebbe una diversa strategia concreta.

Esempi: avvia i flussi di lavoro con alcune varianti, personalizza i registri, personalizza le autorizzazioni e i controlli di sicurezza.

Esistono diversi comportamenti non correlati che possono essere incapsulati separatamente?

Se non si tratta di un singolo comportamento specifico ma di una combinazione di diversi comportamenti non correlati, che potrebbero essere incapsulati e interessare a più di un cliente, utilizzare diverse strategie . Ogni cliente utilizzerebbe quindi una combinazione di strategie concrete.

Esempio: una strategia per l'autenticazione dell'utente (SSO vs password), una strategia diversa per il flusso di lavoro (due passaggi vs tre passaggi) e una diversa strategia di output (email html, pdf allegati). Ciò offre 8 combinazioni possibili per un cliente.

Sono necessari comportamenti diversi a livello di classe?

Se il comportamento specifico non può essere incapsulato facilmente nelle strategie, ma sono così diversi da dover essere incorporati nelle classi, allora si potrebbe optare per un approccio di polimorfismo: incapsulare comportamenti comuni nelle classi base, usare metodi di prova per ridurre la ridondanza. Ma le cose diventeranno molto più complesse in quanto avrai bisogno di una fabbrica, o anche di una fabbrica astratta per creare oggetti, potresti anche aver bisogno di ponti per disaccoppiare il perfezionamenti specifici dell'applicazione, dalle variazioni del cliente.

ATTENZIONE

Avere un unico codice base può essere fonte di produttività e sinergia. Tuttavia, uno standard di alta qualità richiederà test molto più complessi, in quanto i test devono essere eseguiti per tutte le combinazioni possibili al fine di garantire che non vi siano effetti collaterali inattesi.

    
risposta data 01.03.2018 - 22:01
fonte
2
        IList<string> list = new List<string>
        {
            { "someClient" },
            { "otherClient" }
        };

        list.Contains(clientId);

Utilizza il polimorfismo e prova a creare classi per ogni implementazione. Ciò eliminerebbe tutti gli elementi, e sarebbe una buona pratica.

Per rispondere alla tua domanda:

"richiederebbe ai programmatori di creare forse migliaia di nuove interfacce / classi per ogni funzionalità moltiplicate per comportamento personalizzato di ogni cliente, a volte solo per una semplice riga di codice personalizzato."

Se stai scrivendo tutto in una classe, stai facendo 2 cose:

  1. Creazione di una futura classe di Dio.

  2. Rompendo la prima regola del principio di responsablità SRP.Single.

"Molte piccole classi sono migliori"

"Le classi grandi e multifunzionali ci obbligano a scorrere il codice di cui non abbiamo bisogno di sapere al momento."

link

    
risposta data 01.03.2018 - 19:44
fonte
2

Dovresti prendere questo tipo di decisioni il più presto possibile nell'applicazione.
Nel caso in cui l'utente possa utilizzare solo un'opzione della funzionalità, è necessario spostare la decisione sul punto di ingresso delle richieste dell'utente.

Potresti introdurre una parte della funzione che dovrebbe essere cambiata come interfaccia e inserirla nella funzione stessa.

public class MyFeature
{
    private readonly ISomeLogic _logic;
    public MyFeature(ISomeLogic logic)
    {
        _logic = logic
    }

    public void DoWork(object someArguments)
    {
        .. do staff
        var logicResult = logic.Run();
        .. use result
    }
}

Quindi avrai due implementazioni per ISomeLogic .

public class CustomerOneSomeLogic : ISomeLogic { }

public class CustomerTwoSomeLogic : ISomeLogic { }

Quindi, nel punto di ingresso della tua applicazione, inserirai la corretta implementazione.

In caso di API Web, puoi farlo da qualche parte nel middleware, dove accedi alle informazioni attuali dell'utente.

Questo è solo il luogo in cui decidi quale implementazione dovrebbe essere utilizzata.
Poiché lo fai sul punto di ingresso dell'applicazione, il resto dell'applicazione non presenta complicate condizioni di if .. else .

public MyFeature CreateFeature(User userInfo)
{
    if (userInfo.useLogicOne)
    {
        return new MyFeature(new CustomerOneSomeLogic());
    }
    if (userInfo.useLogicTwo)
    {
        return new MyFeature(new CustomerTwoSomeLogic());
    }
}

Se stai usando contenitori di Dipendenza Iniezione, allora aggiungerai quella logica al contenitore, che ogni volta che l'applicazione chiederà l'istanza di ISomeLogic restituirà uno corretto.

L'idea principale è spostare il processo decisionale il più vicino possibile al punto di ingresso dell'applicazione . Spostandolo, salverà altri elementi di livello inferiore di avere condizioni e renderà i test molto più semplici rimuovendo rami di esecuzione aggiuntivi.

Quando un nuovo cliente chiede modifiche, estrai pezzo se codice, che deve essere modificato nell'interfaccia. Introdurre due implementazioni: predefinite e richieste dal cliente. Quindi iniettare uno corretto.

    
risposta data 02.03.2018 - 09:14
fonte
2

E il polimorfismo?

Entrypoint:

function someFeature() 
{
   // eg: \MyCompany\ClientResponseBehaviour\AwkwardClientBehaviour
   $clientResponseBehaviour = Client::Get(client_id_here)->getBehaviour();
   $responseBehaviour = new $clientResponseBehaviour();
   $responseBehaviour->doMagic();
}

Implementazioni concrete:

class AwkwardClientBehaviour implements ClientBehaviourInterface {
   public doMagic() { echo 'im the awkward client!'; }
}

class NormalClientBehaviour implements ClientBehaviourInterface {
   public doMagic() { echo 'hi there, im normal.'; }
}

Non dimenticare l'interfaccia:

interface ClientBehaviourInterface {
    public doMagic();
}
    
risposta data 12.04.2018 - 11:17
fonte
1

Questo problema è grande. È più grande del modello di strategia. È più grande del modello del ponte. È più grande del polimorfismo stesso.

Questo non è altro che una buona ragione per usare un framework di iniezione delle dipendenze.

Raramente ho avuto qualcosa di buono da dire sui framework DI perché ti invitano a fare cose stupide come ingombrare le tue classi con annotazioni proprietarie. La maggior parte dei problemi potrebbero essere risolti senza di essi, ma qui avete una buona argomentazione per uno.

Il motivo per cui è necessario essere in grado di consentire a ciascun utente di utilizzare le implementazioni predefinite a meno che la configurazione personalizzata non le sovrascriva. Questo è davvero un argomento per un sistema che può gestire la scelta delle implementazioni come gruppo. Non solo uno alla volta.

Fatto in questo modo, la conoscenza di quale utente viene servito viene completamente rimossa dal codice. Succede tutto nel file di configurazione degli utenti.

    
risposta data 02.03.2018 - 19:03
fonte

Leggi altre domande sui tag