Usa la classe astratta in C # come definizione


21

Come sviluppatore C ++ sono abbastanza abituato ai file di intestazione C ++ e trovo utile avere una sorta di "documentazione" forzata all'interno del codice. Di solito mi trovo in un brutto momento quando devo leggere del codice C # per questo: non ho quel tipo di mappa mentale della classe con cui sto lavorando.

Supponiamo che come ingegnere del software stia progettando il framework di un programma. Sarebbe troppo folle definire ogni classe come una classe astratta non implementata, in modo simile a quello che faremmo con le intestazioni C ++, e permettere agli sviluppatori di implementarla?

Immagino che ci possano essere alcuni motivi per cui qualcuno potrebbe trovare questa una soluzione terribile, ma non sono sicuro del perché. Cosa dovremmo considerare per una soluzione come questa?


13
C # è un linguaggio di programmazione totalmente diverso rispetto a C ++. Alcune sintassi sembrano simili, ma è necessario comprendere C # per fare un buon lavoro al suo interno. E dovresti fare qualcosa per la cattiva abitudine che hai facendo affidamento sui file di intestazione. Ho lavorato con C ++ per decenni, non ho mai letto i file di intestazione, li ho solo scritti.
Bent

17
Una delle cose migliori di C # e Java è la libertà dai file header! Usa solo un buon IDE.
Erik Eidt,

9
Le dichiarazioni nei file di intestazione C ++ non finiscono per far parte del file binario generato. Sono lì per il compilatore e il linker. Queste classi astratte C # farebbero parte del codice generato, senza alcun beneficio.
Mark Benningfield,

16
Il costrutto equivalente in C # è un'interfaccia, non una classe astratta.
Robert Harvey,

6
@DaniBarcaCasafont "Vedo le intestazioni come un modo rapido e affidabile per vedere quali sono i metodi e gli attributi di una classe, quali tipi di parametri si aspetta e cosa restituisce". Quando uso Visual Studio, la mia alternativa è il collegamento Ctrl-MO.
wha7ever - Ripristina Monica

Risposte:


44

Il motivo per cui ciò è stato fatto in C ++ ha avuto a che fare con la creazione di compilatori più veloci e facili da implementare. Questo non era un progetto per facilitare la programmazione .

Lo scopo dei file di intestazione era consentire al compilatore di eseguire un primo passaggio super rapido per conoscere tutti i nomi delle funzioni previste e allocare posizioni di memoria per loro in modo che possano essere referenziati quando chiamati nei file cp, anche se la classe che li definisce aveva non ancora analizzato.

Non è consigliabile provare a replicare una conseguenza delle vecchie limitazioni hardware in un moderno ambiente di sviluppo!

La definizione di un'interfaccia o di una classe astratta per ogni classe ridurrà la produttività; cos'altro avresti potuto fare con quel tempo ? Inoltre, altri sviluppatori non seguiranno questa convenzione.

In effetti, altri sviluppatori potrebbero eliminare le tue classi astratte. Se trovo un'interfaccia nel codice che soddisfa entrambi questi criteri, la elimino e la refactoring fuori dal codice:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

L'altra cosa è che ci sono strumenti inclusi in Visual Studio che fanno ciò che mirano a realizzare automaticamente:

  1. Il Class View
  2. Il Object Browser
  3. In Solution Explorerpuoi fare clic sui triangoli per spendere le classi per vedere le loro funzioni, parametri e tipi di ritorno.
  4. Ci sono tre piccoli menu a discesa sotto le schede dei file, quello più a destra elenca tutti i membri della classe corrente.

Fai una delle prove precedenti prima di dedicare tempo alla replica dei file di intestazione C ++ in C #.


Inoltre, ci sono ragioni tecniche per non farlo ... renderà il tuo binario finale più grande di quanto deve essere. Ripeterò questo commento di Mark Benningfield:

Le dichiarazioni nei file di intestazione C ++ non finiscono per far parte del file binario generato. Sono lì per il compilatore e il linker. Queste classi astratte C # farebbero parte del codice generato, senza alcun beneficio.


Inoltre, citato da Robert Harvey, tecnicamente, l'equivalente più vicino di un'intestazione in C # sarebbe un'interfaccia, non una classe astratta.


2
Si finirebbe anche con molte chiamate di dispacciamento virtuale non necessarie (non va bene se il codice è sensibile alle prestazioni).
Lucas Trzesniewski,

5
ecco il principio di segregazione dell'interfaccia a cui si fa riferimento.
NH.

2
Si potrebbe anche semplicemente comprimere l'intera classe (tasto destro del mouse -> Struttura -> Comprimi in Definizioni) per vederla a volo d'uccello.
jpmc26,

"cos'altro avresti potuto fare con quel tempo" -> Uhm, copia e incolla e postwork molto minori? Mi ci vogliono circa 3 secondi, se non del tutto. Ovviamente, avrei potuto usare quel tempo per estrarre una punta del filtro in preparazione al rotolamento di una sigaretta, invece. Non proprio un punto rilevante. [Dichiarazione di non responsabilità: amo entrambi, C # idiomatico, così come C ++ idiomatico e alcune altre lingue]
phresnel

1
@phresnel: mi piacerebbe vederti provare a ripetere le definizioni di una classe ed ereditare la classe originale da quella astratta in 3 secondi, confermata senza errori, per qualsiasi classe sufficientemente grande da non avere una vista a volo d'uccello esso (dato che è il caso d'uso proposto dall'OP: classi apparentemente complicate altrimenti complicate). Quasi intrinsecamente, la natura complicata della classe originale significa che non puoi semplicemente scrivere a memoria la definizione astratta della classe (perché ciò significherebbe che già la conosci a memoria) E quindi considerare il tempo necessario per farlo per un'intera base di codice.
Flater,

14

Innanzitutto, capisci che una classe puramente astratta è in realtà solo un'interfaccia che non può fare l'ereditarietà multipla.

Scrivere classe, estrarre l'interfaccia, è un'attività cerebrale. Tanto che abbiamo un refactoring per questo. È un peccato. Seguendo questo schema "ogni classe ottiene un'interfaccia" non solo produce disordine ma manca completamente il punto.

Un'interfaccia non dovrebbe essere considerata semplicemente una riaffermazione formale di qualunque cosa la classe possa fare. Un'interfaccia dovrebbe essere pensata come un contratto imposto dall'uso del codice client che specifica i dettagli di cui ha bisogno.

Non ho alcun problema a scrivere un'interfaccia che attualmente ha solo una classe che la implementa. In realtà non mi interessa se nessuna classe lo implementa ancora. Perché sto pensando a cosa mi serve per usare il codice. L'interfaccia esprime ciò che richiede l'utilizzo del codice. Qualunque cosa succeda dopo, può fare ciò che gli piace purché soddisfi queste aspettative.

Ora non lo faccio ogni volta che un oggetto ne utilizza un altro. Lo faccio attraversando un confine. Lo faccio quando non voglio che un oggetto sappia esattamente con quale altro oggetto sta parlando. Qual è l'unico modo in cui il polimorfismo funzionerà. Lo faccio quando mi aspetto che l'oggetto che il mio codice client sta parlando possa cambiare. Certamente non lo faccio quando quello che sto usando è la classe String. La classe String è bella e stabile e non ho bisogno di guardarmi dal cambiarmi.

Quando decidi di interagire direttamente con un'implementazione concreta piuttosto che attraverso un'astrazione, prevedi che l'implementazione è abbastanza stabile da fidarti di non cambiare.

Proprio così c'è il modo in cui tempero il principio di inversione di dipendenza . Non dovresti applicare ciecamente fanaticamente questo a tutto. Quando aggiungi un'astrazione, stai davvero dicendo che non ti fidi della scelta della classe di implementazione per essere stabile durante la vita del progetto.

Tutto questo presuppone che tu stia cercando di seguire il principio aperto chiuso . Questo principio è importante solo quando i costi associati alla modifica diretta del codice stabilito sono significativi. Uno dei motivi principali per cui le persone non sono d'accordo su quanto sia importante il disaccoppiamento degli oggetti perché non tutti sostengono gli stessi costi quando apportano modifiche dirette. Se ripetere il test, ricompilare e ridistribuire l'intera base di codice è banale per te, risolvere una necessità di modifica con la modifica diretta è probabilmente una semplificazione molto interessante di questo problema.

Semplicemente non c'è una risposta cerebrale a questa domanda. Un'interfaccia o una classe astratta non è qualcosa che dovresti aggiungere ad ogni classe e non puoi semplicemente contare il numero di classi di implementazione e decidere che non è necessario. Si tratta di affrontare il cambiamento. Ciò significa che stai anticipando il futuro. Non essere sorpreso se sbagli. Mantienilo semplice come puoi senza tirarti indietro in un angolo.

Quindi, per favore, non scrivere astrazioni solo per aiutarci a leggere il codice. Abbiamo strumenti per questo. Usa le astrazioni per disaccoppiare ciò che deve essere disaccoppiato.


5

Sì, sarebbe terribile perché (1) introduce un codice non necessario (2) confonderà il lettore.

Se vuoi programmare in C # dovresti semplicemente abituarti a leggere C #. Il codice scritto da altre persone non seguirà comunque questo schema.


1

Test unitari

Suggerirei caldamente di scrivere test unit invece di ingombrare il codice con interfacce o classi astratte (salvo dove giustificato per altri motivi).

Un unit test ben scritto non solo descrive l'interfaccia della tua classe (come farebbe un file di intestazione, una classe astratta o un'interfaccia), ma descrive, ad esempio, la funzionalità desiderata.

Esempio: dove potresti aver scritto un file di intestazione myclass.h in questo modo:

class MyClass
{
public:
  void foo();
};

Invece, in c # scrivi un test come questo:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Questo test molto semplice trasmette le stesse informazioni del file di intestazione. (Dovremmo avere una classe denominata "MyClass" con una funzione senza parametri "Foo") Mentre un file di intestazione è più compatto, il test contiene molte più informazioni.

Un avvertimento: un tale processo di avere un ingegnere software senior che fornisce (fallendo) test ad altri sviluppatori per risolvere violenti scontri con metodologie come TDD, ma nel tuo caso sarebbe un enorme miglioramento.


Come si fa Test unitario (test isolati) = Manichino necessario, senza interfacce? Quale framework supporta il derisione da un'implementazione effettiva, la maggior parte che ho visto utilizza un'interfaccia per scambiare l'implementazione con l'implementazione fittizia.
Bulan,

Non penso che le beffe siano necessariamente necessarie per gli scopi di OP: s. Ricorda, non si tratta di unit test come strumenti per TDD, ma di unit test in sostituzione dei file di intestazione. (Potrebbero ovviamente evolversi in "soliti" test unitari con simulazioni ecc.)
Guran,

Ok, se considero solo i lati positivi di OP, capisco meglio. Leggi la tua risposta come una risposta applicativa più generale. Grazie per il chiarimento!
Bulan,

@Bulan la necessità di una lunga derisione è spesso indicativa di un cattivo design
TheCatWhisperer

@TheCatWhisperer Sì, ne sono ben consapevole, quindi nessun argomento lì, non riesco a vedere che ho fatto quell'affermazione da nessuna parte: DI stava parlando di test in generale, che tutto il Mock-framework che ho usato usa le interfacce per scambiare l'implementazione effettiva, e se ci fosse qualche altra tecnica su come sei andato a deridere se non hai interfacce.
Bulan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.