Come separare il codice pubblico e "per lo più privato" in C #? (Classi di amici, schema PIMPL, ecc.)

6

Promemoria: se hai dei suggerimenti, ricordati di mettere il motivo in modo obiettivo, ad esempio "avere due funzioni SetInt() distinte nello stesso file viola le aspettative del lettore che saranno sovraccariche e ostacola la capacità di trova la funzione giusta con ctrl + F. "

Problema:

Occasionalmente sono necessarie API di basso livello / pericolose. Dovrebbero essere nascosti, ma non possono essere privati. Ad esempio, le funzioni per manipolare i dati di salvataggio aggiuntivi (non caricati), rinominare / eliminare i file di configurazione dei dati di salvataggio sottostanti su disco o impedire che eventuali salvataggi futuri vengano scritti su disco. Come faccio a mantenere l'API pulita, quindi le classi normali non vedono questi metodi ausiliari?

In C ++, queste funzioni ausiliarie sarebbero private e le classi che necessitavano dell'accesso sarebbero classi di amici. In C #, il mio primo pensiero è stato quello di usare questo:

Pattern Pseudo-Facciata, nascondendo i metodi ausiliari:

// The Save.Foo() functions are used all the time. Concise/simple API needed. 
public static class Save
{
    public static void SetInt(string key, int value)
    {
        Save_Implementation.SetInt(currentSaveData, key, value);
    }
}

public static class Save_Implementation // ancillary or dangerous APIs
{
    public static void SetInt(object data, string key, int value) { /* ... */ }
    public static void StopAllSaves() { /* ... */ }
    public static string GetCurrentFilename() { /* ... */ }
}

Funziona, ma dovrei correlare le classi per chiarire meglio che fanno parte della stessa cosa. Sottoclasse? Non posso, è statico. Spazio dei nomi? Non possiamo, dobbiamo digitare questi nomi di funzioni con una frequenza brutale. Classe annidata? Sì, per favore.

Classe nidificata:

public static class Save
{
    public static void SetInt(string key, int value)
    {
        Impl.SetInt(currentSaveData key, value);
    }

    public static class Impl // ancillary or dangerous APIs
    {
        public static void SetInt(object data, string key, int value) { /* ... */ }
        public static void StopAllSaves() { /* ... */ }
        public static string GetCurrentFilename() { /* ... */ }
    }
}

È meglio, ma più cauto di quanto preferire, dal momento che ci sono metodi duplicati nello stesso file. (I.e, SetInt() chiama Impl.SetInt(currentSaveData) .) Successivamente ho provato a dividerlo:

Classi parziali, con elementi ausiliari in un file diverso:

Save.cs:

public static partial class Save
{
    public static void SetInt(string key, int value)
    {
        Impl.SetInt(currentSaveData, key, value);
    }

    public static partial class Impl // ancillary or dangerous APIs
    {
    }
}

SaveImplementation.cs:

public static partial class Save
{
    public static partial class Impl // ancillary or dangerous APIs
    {
        public static void SetInt(object data, string key, int value) { /* ... */ }
        public static void StopAllSaves() { /* ... */ }
        public static string GetCurrentFilename() { /* ... */ }
    }
}

Funziona, ma l'IDE non è abbastanza intelligente da aprire il file giusto quando passo alla definizione di Salva o Salva.Impl. C'è un modo perfetto per organizzare questo, o sarà sempre un compromesso?

Modifica: Save è una sostituzione drop-in per la funzionalità di salvataggio predefinita del motore di gioco, quindi è ideale per clonare l'API esistente (ed essere statica).

    
posta piojo 10.03.2016 - 08:34
fonte

4 risposte

3

Poiché sia le caratteristiche normali che i "qui sono i draghi", le funzionalità avanzate devono essere accessibili al pubblico, ma in genere non dovrebbero essere utilizzate, quindi separarle in due gruppi: uno "normale" e uno "avanzato" " uno. In questo modo, le funzionalità avanzate non possono essere utilizzate accidentalmente: uno sviluppatore deve aggiungere esplicitamente quell'assieme come riferimento per accedere a tali funzionalità.

    
risposta data 10.03.2016 - 12:19
fonte
2

Le classi parziali sono principalmente destinate a separare il codice generato dal codice non generato, in modo che possano verificarsi modifiche nel codice non generato che non influisce sul codice generato. È questo il caso qui?

Le classi interne sono pensate per raccogliere insieme cose che appartengono concettualmente, ma non saranno mai utilizzate al di fuori della classe. Sono animali abbastanza rari. Quelli static sono incredibilmente rari. Nulla di ciò che hai mostrato qui indica che ne hai bisogno.

Le classi Impl sono pensate per servire Interfacce, che hanno il loro specifico scopo (cioè la capacità di scambiare le implementazioni). Non sembri avere questo requisito.

Alla fine della giornata, una singola classe (che funge da adattatore) per ciascuna API "pericolosa" potrebbe essere l'incapsulamento di cui avrai mai bisogno, a meno che il tuo codice di conversione sia abbastanza complesso da giustificare l'architettura aggiuntiva.

    
risposta data 10.03.2016 - 08:47
fonte
1

Rispondendo alla mia domanda - c'è una soluzione che è imbarazzantemente semplice: mentre l'API deve essere statica *, la classe stessa non ha bisogno di esserlo. Con quel piccolo cambiamento, il compilatore consentirà l'ereditarietà. I metodi altamente visibili possono andare nella classe padre e gli adattatori possono ereditare dalla classe padre per ottenere un accesso più intimo. Il "non chiamarlo a meno che tu non sappia cosa stai facendo" roba resterà protetto, nel genitore.

public class Save
{
    public static void SetInt(string key, int value)
    {
        SetInt(currentSaveData, key, value);
    }

    // ancillary / dangerous methods are protected
    protected static void SetInt(object dataBacking, string key, int value) { /* ... */ }
    protected static void StopAllSaves() { /* ... */ }
}

public class SaveDataMigration : Save // special-case code can go in subclasses/adapters
{
    public static void ReplaceSaveData()
    {
        StopAllSaves(); // adapter can access the dangerous methods
        // ...
    }
}

* Perché l'API deve essere statica? Questo è uno di quei pochi casi in cui contano le battiture. Quando aggiungo nuove parti al gioco, ho bisogno di energia mentale libera per i problemi difficili. Salvare i dati degli oggetti è un file standard, spesso scritto nel mezzo di un problema serio. Quel poco tempo e sforzo per scrivere il nome di una variabile di istanza singleton è un po 'di sforzo mentale che verrà deviato dal difficile problema.

    
risposta data 10.03.2016 - 16:53
fonte
0

Quello che puoi fare a livello della tua classe Save (con l'ulteriore vantaggio di farli apparire come metodi di istanza) è dichiarare un sacco di Func < ... > e / o Azione < ... > delegati dei membri (ad esempio, come campi pubblici di sola lettura che è possibile impostare solo in un costruttore) con gli stessi nomi e firme delle loro controparti di Impl, quindi in uno dei costruttori di Save li si vincola (grazie ad un helper BindTo (Type implType) vuoto protetto di Salva) all'implementazione effettiva trovata in Impl, con BindTo (...) usando:

Delegate.CreateDelegate(Type delegateType, MethodInfo methodInfo)

(se è necessario utilizzare il reflection e non solo il codice hard in BindTo (...))

In Save, probabilmente vorrai rendere quella prova futura di BindTo (...) dichiarandola come virtual protetta ...

Nota, se Salva consente il binding parziale di se stesso a Impl, un client dell'interfaccia pubblica di Save avrà la responsabilità di verificare se qualsiasi cosa a cui è interessato è effettivamente associato (non nullo) o non.

'HTH,

    
risposta data 10.03.2016 - 17:34
fonte

Leggi altre domande sui tag