Esiste un modello per limitare le classi che possono aggiornare un'altra classe?

6

Supponiamo di avere una classe ImportantInfo con dati di proprietà pubbliche scrivibili. Molte classi leggeranno questa proprietà, ma solo alcune lo imposteranno. In sostanza, se vuoi aggiornare i dati dovresti sapere davvero cosa stai facendo.

C'è uno schema che potrei usare per rendere esplicito questo oltre a documentarlo? Ad esempio, un modo per far si che solo le classi che implementano IUpdateImportantData possano farlo (questo è solo un esempio)? Non sto parlando di sicurezza qui, ma più di un "hey, sei sicuro di volerlo fare?" tipo di cosa.

UPDATE

Ho avuto a che fare con situazioni simili prima, ecco perché ho cercato di mantenere la domanda più generica ma sembra che sia richiesto un esempio più concreto. Quindi ecco qui:

Ho una classe Context con una proprietà CurrentYear. Molte classi usano questo oggetto Context e, ogni volta che CurrentYear cambia, hanno bisogno di reagire (ricaricare se stessi, per esempio). Ora, ci sono alcune classi che possono legittimamente cambiare CurrentYear. Ovviamente, non vuoi che tutti modifichino CurrentYear perché ha un effetto in molte altre classi. Come faresti a riguardo?

    
posta Mike 25.10.2013 - 21:20
fonte

8 risposte

7

Quindi, al momento hai:

public class ImportantInfo {
    // constructor
    public ImportantInfo() { ... }

    // getter
    public Data getData() { ... }

    // dangerous setter
    public void setData(Data d) { ... }
}

Gestione della mutevolezza

Per prima cosa, forse non vuoi includere un metodo set di default. Per un altro, probabilmente non vuoi un costruttore pubblico. Forse hai già una fabbrica, ma la tua fabbrica potrebbe restituire un'interfaccia immutabile (che non espone il metodo impostato). L'interfaccia seguente ha un metodo che restituisce l'implementazione mutabile, ma che richiede lavoro (quindi le persone non lo faranno per caso) e il nome del metodo fornisce un avvertimento:

public interface ImportantInfo {
    public Data getData();

    // ... expose any other safe methods here ...

    public ImportantInfoImplementation getMutableImplementionButBeCareful();
}

Ora usa un costruttore privato per ImportantInfo e un metodo factory che restituisce l'interfaccia immutabile:

public class ImportantInfoImplementation {
    // constructor is now private so people can't instantiate this
    // willy-nilly.
    private ImportantInfoImplementation() { ... }

    // public factory method is now the way to get instances of this class
    // and you only get the interface which doesn't expose the dangerous
    // method.
    public static ImportantInfo of() {
        return new ImportantInfoImplementation();
    }

    // getter is mentioned in the interface
    @Override
    public Data getData() { ... }

    // dangerous setter is hidden by the interface.
    public void setData(Data d) { ... }

    // returns the mutable implementing class.
    @Override
    public ImportantInfoImplementation getMutableImplementionButBeCareful() {
        return this;
    }
}

Ora per utilizzare il metodo pericoloso, i client devono chiamare:

ImportantInfo ii = ImportantInfoImplementation.of();

// Living free and easy with safe immutable version.
someUntrustedMethod(ii);

// probably not messed up yet!
ii.getData();

// OK, I want it badly enough...
ii.getMutableImplementionButBeCareful().setData(d);

Penso che @Izkata stia girando per qualcosa di simile a quanto sopra, ma non ne sono sicuro.

Sfruttando l'immutabilità

Non sono sicuro che il tuo metodo set esponga qualche pericolo sottostante, o se rende instabile l'istanza di questa classe. Se il primo, questo non ti aiuterà, ma se quest'ultimo, questo potrebbe risolvere il tuo problema: Rendi la tua classe immutabile:

public class ImportantInfo {
    private static final Data data;

    // constructor
    public ImportantInfo(Data d) {
        data = d;
    }

    // getter
    public Data getData() { return d; }

    // no-longer dangers setter returns a new ImportantInfo leaving
    // the old one unchanged.
    public ImportantInfo setData(Data d) {
        return new ImportantInfo(d);
    }
}

Ora se si chiama setData () il vecchio oggetto ImportantInfo è invariato e il codice client ottiene un nuovo oggetto ImportantInfo che riflette lo stato modificato. Ovviamente, anche il tuo oggetto Data deve essere immutabile perché funzioni correttamente. Se Data è o contiene una raccolta di qualche tipo, puoi trovare il mio suggerimenti per rendere le raccolte immutabili in Java per essere utili. Aggiornamento 2017-09-14: invece di quello che ho utilizzato Paguro per eliminare o gestire la maggior parte mutazione in Java.

Deprecation

Se nessuno di questi ti rende felice, potresti deprecare il metodo setData () in modo che le persone debbano sopprimere l'avviso per usarlo. Non mi piace abbastanza, ma potrebbe essere perché non prevedo il motivo per cui è male chiamare questo metodo.

// Be careful when setting data because
//  1. first reason
//  2. second complication
//  3. ...
@Deprecated
public void setData(Data d) { ... }

Conclusione

In ogni caso, mi piace l'idea di dare alle persone un facile accesso alla cosa sicura, ma fargli fare più lavoro per ottenere la cosa pericolosa. Ad ogni modo, per favore spiega esattamente cosa fare attenzione, e perché questo è un problema, in modo che le persone sappiano a cosa stanno entrando prima che lo usino.

    
risposta data 26.10.2013 - 00:27
fonte
10

Per favore, per amore di tutto ciò che è santo, non provare mai a farlo. Ti manca completamente il punto di progettazione della classe e l'incapsulamento.

In OOP, le classi hanno invarianti . Queste sono le condizioni che devono essere presenti affinché la classe funzioni correttamente. Quasi tutte le classi avranno almeno alcuni invarianti a meno che non sia un "sacchetto di proprietà" stupido (AKA Oggetto di trasferimento dati ), e troppi sono considerati anti-pattern ( Modello di dominio anemico ).

Ma torniamo al punto principale. Le responsabilità di una classe sono quindi:

  • Assicurarsi che il suo stato iniziale sia valido, tramite costruttori e convalida parametrizzati.
  • Non fidarti di nulla dall'esterno; convalida gli argomenti a tutti public e probabilmente a tutti protected metodi e setter di proprietà.
  • Rifiuta di eseguire qualsiasi operazione che lascerebbe in uno stato non valido (ad esempio, generando un'eccezione, regolando altre parti dello stato per rimanere valido, o semplicemente eseguendo un no-op e restituendo uno stato di errore). O, ancora meglio, usa il sistema di tipi per renderlo un errore in fase di compilazione per fornire un valore non valido, ad es. utilizzando un'enumerazione anziché un parametro stringa / intero.

Questo è incapsulamento . Un oggetto non consente l'accesso esterno a tutto ciò che non è assolutamente necessario, e dove fornisce l'accesso, lo fa con molta attenzione.

Con questo in mente, è evidente che qualsiasi classe propriamente progettata non dovrebbe importare a chi la chiama perché è impossibile fare qualcosa di pericoloso - o almeno , qualsiasi cosa più pericolosa di ciò che la classe è in realtà progettata per fare. Se la classe è intesa per fornire funzionalità pericolose, probabilmente hai bisogno di un altro livello di astrazione, ovvero progetta la tua applicazione in modo che solo i chiamanti fidati possano ottenere un riferimento all'oggetto pericoloso nella prima luogo .

Un esempio comune di quest'ultimo è l'accesso ai dati. Generalmente non si fornisce l'oggetto di connessione effettivo ai componenti dell'interfaccia utente in cui è possibile che si corrano il rischio di corruzione dei dati o di peggio, SQL injection o altre vulnerabilità di sicurezza. Al contrario, fornisci repository o oggetti comando / query che incapsulano la connessione.

Anche se hai trovato un codice che potrebbe tranquillamente validare il chiamante, anche quando è in esecuzione in modalità di rilascio o con i rewriter o i proxy di intercettazione coinvolti - potrebbe farlo solo in runtime . Il che significa che i programmatori avrebbero nessun indizio che stanno facendo qualcosa di sbagliato finché non provano effettivamente a eseguirlo , che è un modo terribile di trattare i poveri bastardi che cercano di mantenere più tardi. Incontrerai anche un pasticcio di dipendenze perché dovrai portare questa interfaccia segnaposto o attribuire tutto il posto.

Quindi hai problemi di "fiducia transitiva" - in altre parole, la classe chiamata può implementare qualsiasi interfaccia tu abbia deciso, ma ti fidi della classe che sta chiamando quella classe? Con ogni probabilità, i programmatori di manutenzione saranno così frustrati da questa limitazione che creeranno solo una classe wrapper che soddisfa tecnicamente i tuoi requisiti, che potranno quindi utilizzare senza timore di errori di runtime.

Quindi, per favore, non mai tentare di convalidare il chiamante. Convalida gli argomenti se necessario e progetta le tue classi in modo che non sia mai necessario convalidare il chiamante.

Fatto interessante: la versione originale di .NET Framework in realtà aveva un sistema per, in sostanza, la convalida del chiamante e dell'intero stack di chiamate. Si chiamava Protezione dall'accesso di codice . Il sistema di policy si è rivelato così complesso, scomodo da utilizzare e generalmente disprezzato dalla comunità .NET, che Microsoft ha deciso di rimuoverlo in .NET 4 . Ci sono ancora alcune tracce del CAS stesso, ma è tutto roba per lo più a livello di infrastruttura intorno a assembly e AppDomains (sandboxing). Il fatto che usato sia un mezzo a prova di proiettile di validazione del chiamante che era in realtà rimosso dal framework dovrebbe dirti molto su ciò che la comunità di programmazione pensa a questa pratica .

    
risposta data 26.10.2013 - 15:16
fonte
2

Una cosa mi viene in mente, se passi a un membro privato ed esporti getter / setter. Ricorda che non ho mai fatto .NET e sono fuori allenamento con Java, ma questo è lo pseudocodice I ' Sto pensando:

class SomeFickleClass {
   public interface IUpdateImportantData {
      public Data getData();
   }

   private Data myData;

   public function setData(IUpdateImportantData Thing) {
      myData = Thing.getData();
   }
   public function getData() {
      return myData;
   }
}

E l'utilizzo potrebbe essere lungo le linee di:

final Data newData = ....
fickleClassInstance.setData(new SomeFickleClass.IUpdateImportantData() {
   public Data getData() {
      return newData;
   }
});

Quindi impone un'interfaccia che richiede IUpdateImportantData per eseguire l'aggiornamento e può farlo al volo con un'istanza di classe anonima. È anche un po 'scomodo, che dovrebbe scoraggiare l'uso frequente.

    
risposta data 25.10.2013 - 22:24
fonte
1

In base alla @ risposta di GlenPeterson, ecco cosa penso che farò. Avrò due classi:

  • Contesto. Questa classe è mutabile e fornisce un metodo SetData.
  • ContextObserver. Questa classe ha un riferimento privato al contesto, ascolta le modifiche nel contesto e ogni volta che il contesto cambia, eseguirà qualsiasi metodo tu gli indichi di eseguire. Ha anche un metodo GetContext che restituisce una versione immutabile di Context.

Quindi, coloro che hanno davvero bisogno di modificare i dati in Context ottengono un riferimento all'oggetto Context, mentre quelli che consumano solo i dati ottengono un riferimento alla classe ContextObserver.

Semplice e semplice, se vuoi rendere una classe in sola lettura per determinati consumatori, basta dare loro un wrapper readonly rispetto alla classe originale.

    
risposta data 27.10.2013 - 02:38
fonte
0

I was trying to create some way for the important data container to "know" what was setting the data, as mentioned in the question - Izkata

Quindi penso che questo è quello che vuoi:

public interface IUpdateImportantData {
    public Data newDataValue();
}

public class ImportantInfo {
    // constructor
    public ImportantInfo() { ... }

    // getter
    public Data getData() { ... }

    // safer setter
    public void setData(IUpdateImportantData iuid) {
        // instanceof always returns true when used on null
        if (iuid == null) {
            throw new IllegalArgumentException("Can't set data with null");
        }
        if ( (iuid instanceOf OkClass1) ||
             (iuid instanceOf OkClass2) ) {
            this.data = iuid.newDataValue();
        } else {
            throw new IllegalArgumentException("That class is not allowed to change data");
        }
    }
}

Ecco un esempio di utilizzo:

public OkClass2 implements IUpdateImportantData {
    @Override
    public Data newDataValue() { return Data(3); }
}

public OtherClass implements IUpdateImportantData {
    @Override
    public Data newDataValue() { return Data(3); }
}

ImporatntInfo ii = new ImportantInfo();
OkClass2 oc2 = new OkClass2();
ii.setData(oc2); // works.

OtherClass oc1 = new OtherClass();
ii.setData(oc1); // throws exception.

Penso di avere ora risposto alla tua domanda, ma questo è un problema con più complessità. È sempre meglio risolvere il problema sottostante.

A volte è necessario interfacciarsi con codice o servizio che non è possibile controllare. Se non è possibile risolvere il problema sottostante, è possibile progettare un'API che consenta solo gli usi validi della difficoltà sottostante senza preoccuparsi di chi utilizza tale API. Prevenire utilizzi non validi è di gran lunga superiore alla prevenzione di utenti non validi. Puoi:

  • Rendi ImportantInfo una classe interna privata della classe che deve accedervi?

  • Inserisci ImportantInfo nello stesso pacchetto (cartella) delle poche classi che hanno bisogno di accedervi e dare accesso predefinito a setData () (pacchetto-scope, non-modificatore specificato)?

  • Dividi ImportantInfo in 2 classi, una che contiene le cose sicure e una che protegge solo le cose non sicure?

  • Ridisegna ImportantInfo per prevenire i pericoli derivanti dalla modifica dei dati sottostanti?

  • Rendi i dati sottostanti non modificabili. Inietta nuove versioni di Dati, se necessario, senza modificare l'originale.

In realtà, potrei dare suggerimenti migliori se avessi capito esattamente cosa c'era di pericoloso nella modifica dei dati. Se lo capisci, probabilmente la migliore soluzione si presenterà senza il mio aiuto. In caso contrario, probabilmente dovresti fare degli esperimenti per capire la causa principale del problema e risolverlo invece di creare strati di complessità attorno ad esso.

    
risposta data 26.10.2013 - 14:54
fonte
0

Sembra un approccio preoccupante se hai diverse altre classi che sanno come attingere all'interno di un'istanza di ImportantInfo per aggiornare i dati, non perché sia sicuro o meno, ma piuttosto perché hai ottenuto più di una violazione della legge di Demeter.

Sarebbe preferibile inserire il codice di aggiornamento all'interno della classe e fare in modo che le altre classi trasmettano semplicemente le istruzioni su quale aggiornamento deve essere eseguito. Nel caso semplice, queste istruzioni sono solo argomenti per un metodo, ma se vuoi fare qualcosa di più complesso allora le istruzioni possono essere impacchettate in una piccola classe a parte (il che probabilmente dovrebbe essere il più vicino possibile alla funzionalità possibile) : è solo una descrizione, un POJO in gergo in Java). E il codice di aggiornamento stesso, perché è all'interno della classe, può essere gestito molto più facilmente in sincronia con il resto della classe.

Considererei seriamente anche se davvero dovresti supportare la modifica dello stato. Molto spesso, puoi rendere l'oggetto interamente immutabile e invece gestire le modifiche restituendo invece un nuovo oggetto. Questo è un approccio di programmazione più funzionale, e funziona davvero piuttosto bene in quanto significa che gli utenti non aggiornanti dell'oggetto possono presumere che non avranno i cambiamenti nascosti da dietro le loro spalle; Non posso esagerare su quanto sia più facile questo rende la programmazione!

Se hai per supportare l'aggiornamento dall'esterno, considera la possibilità di distribuire un vero delegato di sola lettura per l'oggetto, in modo che i consumatori non possano penetrare all'interno per errore. In Java, prenderei in considerazione l'utilizzo del meccanismo di proxy per fai questo, e vorrei che uno dei metodi del lettore restituisca un ID (che gli scrittori dovrebbero presentare a qualche altra classe manager per ottenere l'entità aggiornabile). Alcuni framework possono renderlo molto facile da fare ...

    
risposta data 26.10.2013 - 16:32
fonte
-1

Purtroppo, non credo sia possibile. Ho provato lo stesso approccio e molte ore di ricerca ma non ho avuto successo. La parte peggiore è che anche i commenti sull'uso della documentazione possono diventare un po 'confusi. Farò comunque più ricerche e vedrò cosa riesco a trovare.

    
risposta data 25.10.2013 - 21:28
fonte
-2

Puoi sovrascrivere e contrassegnare i metodi con @RestResource (esportato = falso).

Questi metodi sono

T findOne(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);

Riceverete lo stato HTTP 405 del metodo non consentito per tutte le richieste GET al repository.

    
risposta data 14.09.2017 - 13:19
fonte

Leggi altre domande sui tag