Come tornare fluentemente?

1

Sto diventando dipendente dalla scrittura fluente (forse in modo errato originariamente detto "dichiarativamente") e lo sto prendendo per estremi forse poco saggio, ma più scrivo in questo modo più una cosa spicca come completamente incapace di scrivere in questo modo, e questa è la dichiarazione return .

Qualcosa come quello che voglio scrivere (per esempio):

StartSomewhere
.Get<Something>()
.OrderBy(something => something.SomeProperty)
.Transform(ToSomethingElse)
.ChooseSomeOption("some value")
.Run()
.ReturnIfNull() // impossible!
.ContinueUsingNonNullValue();

Di cosa sono bloccato invece:

if (
   StartSomewhere
   .Get<Something>()
   .OrderBy(something => something.SomeProperty)
   .Transform(ToSomethingElse)
   .ChooseSomeOption("some value")
   .Run()
   .IsNull(out var notNullValue)
)
   return;

if (
   notNullValue
   .ContinueUsingNonNullValue()
   // etc.
)
   return;

Sospetto che non ci sia modo di ottenere ciò che voglio qui oltre a fare qualcosa di veramente avanzato e folle come avvolgere tutto in un Abortable monad o qualcosa in modo tale che la catena possa essere disattivata a metà strada, consentendo di ignorare le chiamate rimanenti , quindi forse è inutile chiedere, ma chi lo sa.

    
posta Dave Cousineau 08.10.2018 - 21:10
fonte

4 risposte

2

Immagina come sarebbe il tuo codice se avessi dei metodi fluenti ...

DoSomething()
  .If(true)
  .Step2()
  .Else()
  .Step3()
  .EndIf()
  .Step4()

Saresti davvero felice con questo?

Potresti ottenerlo con qualche oggetto di stato interno. Ma non è più leggibile, e se inseriamo un po 'di nidificazione, la sua situazione peggiora solo.

Puoi migliorare il tuo metodo attuale mantenendo l'oggetto. Permettendoti di spostare i blocchi di codice dall'istruzione if.

var i = DoSomething()
    .Step1()

if(i.Result() != null) 
{
     i.Step2(); 
}
    
risposta data 08.10.2018 - 23:26
fonte
3

Lasciando da parte l'editor per un momento , vengono in mente due opzioni:

1. Operare sempre su una "raccolta" o "builder".

Invece di avere ChooseSomeOption(<string>) restituire uno scalare, sempre restituisce un TypeCollection . (Lo stesso vale per ogni metodo nella catena. ) L'interfaccia fluente esiste solo in una sorta di classe TypeCollection . E, se un'operazione filter() o remove() -type riduce il Count a 0 , non ha importanza. Tutto funziona solo .

Se è richiesto un valore di ritorno non- Collection , basta fare il tuo ultimo passaggio "di costruzione".

E come accennato, non è strettamente necessario essere una raccolta. Potrebbe essere solo un "builder."

2. Utilizza il pattern Null Object .

In sostanza, se un metodo come ChooseSomeOption(<string>) non trova un oggetto ChainLink corrispondente, restituisce un new NullChainLink() , una classe figlio di ChainLink che supporta gli stessi metodi di un ChainLink reale, ma non in realtà fa qualsiasi cosa.

Questo potrebbe diventare più complicato dell'opzione 1, perché dovrai creare oggetti null per ogni possibile risposta nullable e ogni tipo successivo , che molto anche essere "nullable".

Penso che sarei negligente se non includessi l'editoriale:

Mi piacciono molto le interfacce fluenti. Ma, se sono onesto con me stesso e tu , sono davvero veramente fastidiosi di eseguire il debug. Un significativo numero di interfacce fluenti che ho scritto nel corso degli anni è stato molto piacevole da scrivere e molto facile da leggere , ma alla fine spezzato durante gli sforzi di debug.

Se a te e al tuo team piace questo tipo di concatenamento di metodi, due consigli:

  1. Non fare sacrifici per questo.
  2. Scrivi unit test in modo ossessivo .

Anche con molti test, è probabilmente meglio sacrificare una catena di metodi "elegante" per codice che funziona adesso di quello è quello di ristrutturare tutto il resto per far funzionare la tua interfaccia fluente.

    
risposta data 08.10.2018 - 23:58
fonte
0

Quello che ho fatto è invertire la condizione e scrivere alcuni metodi di estensione If... . Invece di dire "Annulla se condizione", io dico "fai questa se non condizione".

La ragione principale per annullare è evitare il nidificazione (in particolare il rientro), ma puoi ancora evitare di annidare estraendo le azioni in metodi nidificati con nome. Ciò suddivide la logica in blocchi denominati che sembrano effettivamente utili.

Questo probabilmente non funzionerebbe se avessi bisogno di restituire un valore, però.

void MyMethod() {
   StartSomewhere
   .Get<Something>()
   .OrderBy(something => something.SomeProperty)
   .Transform(ToSomethingElse)
   .ChooseSomeOption("some value")
   .Run()
   .IfNotNull(continueUsingNonNullValue);

   void continueUsingNonNullValue(object nonNullValue) =>
      nonNullValue
      .DoSomethingWithValue()
      .DoSomethingElseWithValue()
      // etc.
}

Questo alla fine si è rivelato un po 'troppo disordinato / puzzolente. È stato interessante, tuttavia, poiché ha spezzato un metodo in sezioni più piccole, ognuna delle quali con il proprio nome descrittivo. Ma è diventato più difficile capire esattamente cosa stesse facendo la funzione nel suo complesso. In particolare, non era ovvio dove e quando la logica avrebbe o non avrebbe continuato.

Quello che ho fatto invece è rendere (buono?) l'uso di out var . Piuttosto che racchiudere la catena stessa in if , e piuttosto che scrivere lo stesso var result = , if (result == null) return; di codice più e più volte, uso un metodo che può essere usato in questo modo:

StartSomewhere
.Get<Something>()
.OrderBy(something => something.SomeProperty)
.Transform(ToSomethingElse)
.ChooseSomeOption("some value")
.Run()
.IsNull(out var value, out var isNull);

if (isNull)
   return;

// continue using non-null value

Con Resharper ContractAnnotation Ottengo avvertimenti se il valore non è noto per essere non nullo e provo a usarlo comunque. Penso che qualsiasi cosa di più richieda l'aggiunta di qualcosa di nuovo nella lingua.

    
risposta data 08.10.2018 - 21:50
fonte
-1

Ci sono due approcci che puoi seguire qui, entrambi che ho usato con successo in passato: uno che usa stato, uno no.

Uso dello stato

Supponendo che tutta la tua fluente catena abbia usato un solo oggetto, MyFluentType , quindi ReturnIfNull farebbe qualcosa del tipo:

MyFluentType ReturnIfNull()
{
    inReturnNullState = SomeNullTest();
    return this;
}

e ContinueUsingNonNullValue fa qualcosa del tipo:

MyFluentType ContinueUsingNonNullValue()
{
    if (!inReturnNullState)
    {
        // do whatever the method does as we aren't in that state
    }
    return this;
}

Quindi viene sempre chiamato ContinueUsingNonNullValue , e usa lo stato corrente dell'oggetto per determinare se fa qualcosa o semplicemente restituisce.

Nessuna soluzione di stato

Qui abbiamo MyFluentType implementare un'interfaccia, IFluentType . Quindi ReturnIfNull decide quale implementazione di IFluentType restituire. Se è null , restituisce un'implementazione con ContinueUsingNonNullValue vuota:

class MyFluentType : IFluentType
{
    IFluentType ReturnIfNull() => SomeNullTest() ? new NullFluentType(this) : this;

    MyFluentType ContinueUsingNonNullValue()
    {
        // do whatever the method does as we aren't in that state
        return this;
    }
}

class NullFluentType : IFluentType
{
    private readonly MyFluentType _parent;
    NullFluentType(MyFluentType parent) => _parent = parent;        

    MyFluentType ContinueUsingNonNullValue() => _parent;
}

In questo modo, nessuno stato è necessario. Invece, viene fornito un tipo diverso, con un'implementazione diversa di IFluentType , a seconda del controllo Null.

Nessuna delle due soluzioni "restituisce" al punto ReturnIfNull ; semplicemente ignorano qualsiasi ulteriore elaborazione, così efficacemente finiscono la catena e ritornano in quel punto, anche se in realtà non lo fanno.

    
risposta data 08.10.2018 - 23:05
fonte

Leggi altre domande sui tag