Interfacce "immutabili"

7

Sono confuso riguardo alla nozione di immutabilità. Considera la seguente struttura di una calcolatrice semplicistica:

Eccoleinterfacce:

interfaceIOperationalInterface{intSum(inta,intb);}interfaceIAuditInterface{intSumInvocations();}

IOperationalInterface.SumdovrebbecalcolarelasommadidueinterieIAuditInterface.SumInvocationsdovrebberestituireilnumerototaledichiamateSumricevute.

Eccoun'implementazionebanale:

classCalculatorImpl:IOperationalInterface,IAuditInterface{privateintinvocations=0;publicintSum(inta,intb){invocations++;returna+b;}publicintSumInvocations(){returninvocations;}}

ÈCalculatorImplimmutabile?Ovviamenteno,perchéilsuostatocambiaconogniinvocazionedelmetodoSum.L'operazioneSumèpura?Secondo Wikipedia non lo è, dal momento che cambia lo stato di un oggetto mutevole, e tale effetto collaterale è osservabile attraverso il %codice%. Ovviamente anche l'operazione IAuditInterface non è pura, poiché potrebbe restituire risultati diversi.

In sintesi, SumInvocations è mutabile e tutti i suoi metodi sono impuri.

Tuttavia, dal punto di vista di CalculatorImpl , che parla ad esso solo attraverso CalculatorClient , sembra essere immutabile e l'operazione IOperationalInterface sembra essere pura nel senso che non potrebbe osservare alcun effetto collaterale tramite quell'interfaccia .

D'altra parte, dal punto di vista di Sum , è completamente diverso: è ovvio che l'oggetto che implementa AuditClient è mutabile e la sua operazione AuditInterface è impura, e questo segue direttamente dalla specifica del SumInvocations .

Quindi, è possibile partizionare un'interfaccia / specifica di una classe mutabile in modo tale che alcune parti di essa sembrino mutabili e altre no. In questo caso, considerando solo l'operazione IAuditInterface e tralasciando il requisito di conteggiare le invocazioni, otteniamo qualcosa che non ha effetti collaterali.

Ora, implementando il Sum si può prendere in considerazione il fatto che l'oggetto dietro l'interfaccia appare per essere immutabile. Almeno uno non poteva dire la differenza.

Quindi la mia domanda è: ha senso parlare di "immutabilità" delle interfacce o è una cattiva idea? In quale altro modo posso comunicare il fatto che non ci saranno effetti collaterali osservabili attraverso quell'interfaccia? E se è brutto, cosa potrebbe andare storto?

Aggiorna

Grazie per le vostre risposte / commenti! Ora vedo che non c'è modo di dire che CalculatorClient è puro; le condizioni di purezza sono troppo forti per essere applicate in questo caso. Tuttavia la domanda rimane se esiste una nozione più debole (forse "immutabilità"?) Che è applicabile.

    
posta proskor 31.07.2014 - 11:19
fonte

4 risposte

5

However, from the viewpoint of CalculatorClient, who talks to it only through the IOperationalInterface, it appears to be immutable and the Sum operation appears to be pure in the sense that it could not observe any side effects through that interface.

Qualunque cosa tu possa osservare attraverso l'interfaccia è irrilevante per il concetto di purezza. Se Sum ha registrato risultati in un file, sarebbe comunque impuro anche se non saresti in grado di osservare alcuna modifica attraverso l'interfaccia.

La purezza è una condizione molto strong. Se dici che qualcosa è puro, ti prenderò in parola e supporterò che non accadrà nulla di male se lo avrò eseguito da più thread, memorizzerò i risultati nella cache e lo chiamerò solo una volta per ogni dato insieme di input, o che posso chiamare 1.000.000 di volte senza esaurire gli handle di file del sistema operativo. Queste ipotesi possono ritorcersi contro quando si utilizza CalculatorImpl .

Non c'è niente di sbagliato nel partizionare le interfacce come te, ma dovresti essere preciso. La specifica che vuoi per IOperationalInterface è probabilmente più vicina a: "Il valore di ritorno di questi metodi deve dipendere solo dai loro argomenti, ma la loro esecuzione può avere effetti collaterali." Puoi essere più specifico sugli effetti collaterali e dire che i metodi non devono eseguire alcun tipo di I / O. Ma non direi che è puro, perché lo stai chiaramente usando in questo modo.

    
risposta data 31.07.2014 - 15:26
fonte
4

Sei quasi impiccato alle definizioni senza preoccuparti del perché ti interessi se una funzione è pura o meno. Una funzione pura dà più libertà al chiamante a spese delle restrizioni sull'implementatore. Una funzione con effetti collaterali offre maggiore libertà all'installatore a spese delle restrizioni sul chiamante.

Molti hanno familiarità con le restrizioni sull'implementatore di una funzione pura, ma molti non hanno familiarità con le libertà che concede al chiamante. Libera il chiamante per memorizzare i risultati nella cache e non chiamare la funzione una seconda volta. Libera il chiamante per creare tante copie dell'oggetto come vogliono, magari su thread differenti o anche su sistemi diversi, per velocizzare il calcolo. Libera il chiamante a non doversi preoccupare di mantenere l'oggetto in ambito, perché possono sempre ricreane uno identico in seguito.

Se non si concedono questi tipi di libertà al chiamante, non è utile etichettare un'interfaccia immutabile. Stai mettendo delle restrizioni sull'implementatore senza alcun beneficio commisurato al chiamante, quindi potresti anche rimuovere le restrizioni dell'implementatore.

    
risposta data 31.07.2014 - 15:21
fonte
1

L'implementazione non è chiaramente immutabile, quindi parliamo della "purezza" della funzione Sum .

Può avere senso parlare dell'implementazione, non dell'interfaccia. Ad esempio, cosa succede se ho creato una nuova implementazione dell'interfaccia con questa implementazione di Sum ?

public int Sum(int a, int b) {
    invocations++;
    if(invocations >= 1000)
    {
        return a + b + 1;
    }
    return a + b;
}

L'interfaccia non è cambiata, ma il metodo non è più "puro".

In un senso puramente tecnico, anche la tua ingenua implementazione non è pura perché alla fine invocations andrà in overflow e otterrai un'eccezione quando chiami Sum invece del risultato previsto.

    
risposta data 31.07.2014 - 13:41
fonte
1

Credo che quello che vuoi sia una variante di Design by Contract (le interfacce C # sono una versione povera di loro) . In C # potrebbe essere ottenuto tramite attributi personalizzati e un'estensione dell'analizzatore statico (ad esempio per Roslyn o ReSharper):

[Pure] //This is just a sample name, not to be confused with System.Diagnostics.Contracts.PureAttribute
interface IOperationalInterface
{
    int Sum(int a, int b);
}

L'estensione dell'analizzatore statico personalizzato dovrebbe quindi verificare se le implementazioni sono prive di effetti collaterali su tutti i metodi dell'interfaccia. Ciò potrebbe complicarsi a seconda del livello di garanzia che vorresti ottenere (ad esempio, semplicemente controllando che non ci siano operatori mutanti usati contro le catene di invocazione del metodo).

Se ritieni che questo approccio sia fattibile per il tuo scenario, allora considera la possibilità di controllare gli strumenti relativi al contratto esistenti per .NET (non funziona bene però):

JetBrains: link Microsoft: link

    
risposta data 31.07.2014 - 15:09
fonte

Leggi altre domande sui tag