Qual è il modo migliore per gestire gli aggiornamenti solo quando un valore è cambiato?

5

Ho un'applicazione che riceve un numero di valori che devono essere applicati a varie proprietà di un oggetto (classe sealed). Inizialmente ho semplicemente impostato il valore senza controllare nulla e aggiornato l'oggetto, ma ovviamente a volte i nuovi valori non sarebbero validi e altre volte sarebbero identici ai valori esistenti, quindi eseguire un aggiornamento era uno spreco di risorse di sistema, e francamente un processo piuttosto lento.

Per ovviare a questo, ho creato un bool privato che è impostato su true se / quando viene applicato un nuovo valore, che è determinato con un metodo che controlla se il valore è diverso (e valido):

private bool updated = false;
private bool updateValue(string property, string value, bool allowNulls = false)
{
    if ((allowNulls || !string.IsNullOrEmpty(value)) && !property.Equals(value)) { updated = true;  return true; }
    return false;
}

Quindi, ogni volta che ho bisogno di impostare il valore di una proprietà su qualcosa di nuovo, ho una singola istruzione If:

if (updateValue(obj.property, newValue))
{ obj.property = newValue; }

E poi, ovviamente, quando a tutte le proprietà sono stati assegnati i loro nuovi valori:

if (updated)
{ obj.commitChanges(); }

Quindi, 1) C'è un modo migliore per farlo, e 2) c'è un modo più conciso per eseguire la mia istruzione If? Se le frasi come questa mi infastidiscono; sembra che dovrei essere in grado di dire "Imposta da A a B quando C".

    
posta Michael Bailey 10.01.2018 - 20:51
fonte

4 risposte

8

No.

No.

Tutto il no!

Innanzitutto, non far fare ai tuoi chiamanti tutto ciò che funziona! Quando voglio impostare un valore, voglio solo impostarlo! E stai lavorando in un linguaggio che lo rende facile per scrivere setter e getter. Utilizzali.

Ecco cosa dovrebbero scrivere i tuoi chiamanti:

obj.propA = valueA;
obj.propB = valueB;

E arriva il momento di persistere obj , se stai facendo qualcosa di stile di registrazione attivo, è solo questo:

obj.Save();

I tuoi setter devono generare eccezioni se fornisci un valore errato.

In secondo luogo, non fare in modo che ai chiamanti importi ciò che Save() o commit() fa o non fa. Il metodo di persistenza stesso ha bisogno di controllare i flag sporchi o confrontare gli hash o le serializzazioni per determinare se effettivamente funziona.

Qualunque cosa sia, qualsiasi strategia tu scelga è probabilmente totalmente soddisfacente. Veramente! Confronta le linee di codice, le implicazioni sulle prestazioni e il "sentimento" semantico di ciascuno e scegli quello che sembra giusto.

Diamine, ciò che è "giusto" potrebbe anche variare tra classi o progetti.

In ogni caso, non preoccuparti dei chiamanti.

    
risposta data 11.01.2018 - 02:02
fonte
2

Ci sono molti approcci a questo problema:

Librerie di terze parti Utilizzare una soluzione di terze parti, ad esempio collegata nei commenti. Questo è destinato ad essere più robusto (e testabile) rispetto alla deviazione dal compito principale per costruire questa funzionalità.

Metodi Get / Set della proprietà Gestisci le modifiche all'interno dei normali metodi di proprietà:

private bool hasUpdates = false;

...

private string name;
public string Name
{
  get { return name; }
  set
  {
     if (!value.Equals(name) && !string.IsNullOrEmpty(value))
     {
       name = value;
       hasUpdates = true;
     }
  }
}

Questo richiede un po 'più di ingombro del codice, quindi potrebbe non essere preferibile se ci sono molte di queste proprietà, o ti piace davvero scrivere, o questo codice non viene generato da un DSL.

Metodo Smart-Update singolo

private bool hasUpdates = false;

...

private string name;

public bool TryUpdate(string propertyName, T value, out T newValue) {
   var classProperty = this.GetType().GetProperty(propertyName);

   if (classProperty == null) { return false; }

   if (!classProperty.GetValue(this).Equals(value)) {
     classProperty.SetValue(value);
     hasUpdates = true;
     newValue = value;
   }
}

Qui è necessario un solo metodo ingombrante, piuttosto che uno per ogni proprietà.

Confronto hash

Un'altra soluzione sarebbe quella di mantenere una cache di oggetti e il loro valore di hash quando recuperati dal DB, e quindi confrontare di nuovo l'hash quando viene chiamato commitChanges() . Elabora solo quegli oggetti i cui hash sono cambiati.

    
risposta data 10.01.2018 - 21:55
fonte
1

Se il tuo oggetto era passivo e aveva campi pubblici invece di proprietà, potresti scrivere una procedura più elegante Update() , accettando il campo come parametro ref :

static void Update( ref string curVal, string newVal, ref bool isDirty )
{   if( curVal != newVal )
    {   isDirty = true;
        curVal  = newVal;
    }
}
//...
updated = false;
Update( ref obj.Prop1, "NewValue1", ref updated );
Update( ref obj.Prop2, "NewValue2", ref updated );
//... updates of many other properties...
if( updated )
{   CommitChanges( obj );  }

Con il sovraccarico di OOP, tuttavia, hai più lavoro da fare. La seguente applicazione console dimostra una possibile soluzione per le proprietà con setter insignificanti (la classe Trivial ) e non banali (la classe NonTrivial ), che eseguono calcoli arbitrari:

// TODO: Maybe store property names as constants?
using System;
using System.Collections.Generic;

class Program {

class Trivial
{   Dictionary<string, string> Props;

    public bool IsDirty // TODO: Make it writable for resetting?
    {   get; private set;  }

    public void PropSet( string prop, string value )
    {   string currentVal;

        currentVal = Props[ prop ];
        if( currentVal != value )
        {   IsDirty = true;
            Props[ prop ] = value;
        }
    }

    public string Prop1
    {   set
        {   Props["Prop1"] = value;  }      
        get
        {   return Props["Prop1"];  }
    }

    public string Prop2
    {   set
        {   Props["Prop2"] = value;  }      
        get
        {   return Props["Prop2"];  }
    }

    public Trivial()
    {   Props = new Dictionary<string, string>();
        Props.Add("Prop1", "");
        Props.Add("Prop2", "");     
    }
}

class NonTrivial
{   delegate void FPropSet( string value );
    // TODO: Modify PropInfo according to your needs, e.g. add an
    //       allowNull flag...
    class PropInfo
    {   public FPropSet Set;
        public string   Value;
    }

    Dictionary< string, PropInfo > Props;

    public bool IsDirty // TODO: Make it writable for resetting?
    {   get; private set;  }

    public string Prop1
    {   set
        {   PropSet( "Prop1", value );  }       
        get
        {   return PropGet( "Prop1" );  }
    }

    void SetProp1( string value )
    {   // Very heavy calculations!
    }

    public string Prop2
    {   set
        {   PropSet( "Prop2", value );  }       
        get
        {   return PropGet( "Prop2" );  }
    }

    void SetProp2( string value )
    {   // Super heavy calculations!
    }   

    void RegisterProp( string prop, FPropSet setter, string defValue )
    {   PropInfo info = new PropInfo();
        info.Set   = setter;
        info.Value = defValue;
        setter( defValue );
        Props.Add( prop, info );
    }

    string PropGet( string prop )
    {   return Props[ prop ].Value;  }

    public void PropSet( string prop, string value )
    {   PropInfo info;
        info = Props[ prop ];
        // TODO: Modify the comparison according to your needs:
        if( info.Value != value )
        {   info.Set( value );
            info.Value = value;
            IsDirty = true;
        }
    }

    public NonTrivial()
    {   Props = new Dictionary< string, PropInfo >();
        RegisterProp( "Prop1", SetProp1, "A" );
        RegisterProp( "Prop2", SetProp2, "1" );
    }
}

public static void Main(string[] args)
{   Trivial    triv    = new Trivial   ();
    NonTrivial nonTriv = new NonTrivial();

    triv.Prop1 = "A"; // The setter will not change .IsDirty
    triv.Prop2 = "1"; // Should it?
    triv.PropSet("Prop1", "B"); // Update property only if changed
    if( triv.IsDirty )
    {   // This work will not be wasted
    }

    nonTriv.Prop1 = "A"; // The setter will change .IsDirty
    nonTriv.Prop2 = "1"; // Should it?
    nonTriv.PropSet("Prop1", "B");  // Update property only if changed
    if( nonTriv.IsDirty )
    {   // It will not be labor lost
    }   

    Console.ReadKey(true);
}

}

Se al chiamante non interessa sapere se i dati effettivi sono cambiati, puoi annullare la pubblicazione della proprietà IsDirty e utilizzarla internamente nel tuo metodo CommitChanges() , che dovrei rinominare in CommitChangesIfAny() per riflettere la natura condizionale del operazione di commit.

Un'altra opzione è Reflection in cui puoi fare la stessa cosa dinamicamente ( Modifica: vedi il Metodo Smart-Update singolo in Risposta di Dan1701 ).

    
risposta data 10.01.2018 - 22:56
fonte
0

Che ne dici di cambiare il metodo updateValue per restituire null invece di false (e magari rinominarlo per getNewValue), e quindi basta impostare le mie proprietà usando un operatore condizionale:

obj.property = getNewValue(obj.property, newValue) | obj.property;

Il metodo getNewValue è ancora responsabile dell'impostazione del bit "IsDirty", che può essere utilizzato per confermare che i nuovi valori devono essere confermati.

    
risposta data 12.01.2018 - 21:31
fonte

Leggi altre domande sui tag