2010-04-15 11 views
12

C'è un modo di ottenere riferimenti di unità intorno circolari in Delphi?ottenere riferimenti circolari intorno a Delfi

Forse una nuova versione di Delphi o qualche trucco di magia o qualcosa del genere?

Il mio progetto Delphi ha 100 000+ righe di codice per lo più basati su classi di Singleton. Ho bisogno di refactoring, ma questo significherebbe diversi mesi di "riferimento circolare" all'inferno :)

risposta

37

Ho mantenuto quasi un milione di righe di codice legacy negli ultimi 10 anni, quindi capisco il tuo dolore!

Nel codice che mantengo, quando ho riscontrato utilizzi circolari, ho trovato spesso che sono causati da costanti o definizioni di tipo nell'unità A che sono necessarie per l'unità B. (A volte è anche un po 'di codice (o anche variabili globali) nell'unità A che è necessario anche dall'unità B.

In questa situazione (quando sono fortunato!) posso estrarre con attenzione quelle parti del codice in una nuova unità C che contiene le costanti, definizioni di tipo e il codice comune. Poi le unità A e B l'uso dell'unità C.

i post di cui sopra con una certa esitazione, perché io non sono un esperto di software di progettazione e rendo conto che ci sono molti altri qui che sono lontani più conoscenza più di quanto non lo sia io. Spero comunque che la mia esperienza sarà di qualche utilità per voi.

+5

+1. IMHO non c'è bisogno di esitare qui. –

+0

+1 Sono con l'onorevole Gerhardt. Non c'è bisogno di esitare. –

+0

+1 Un'unità "AppTypes.pas" è una cosa molto comune nelle app Delphi. –

8

Utilizzare la sezione dell'implementazione quando possibile, e limitare ciò che è nella clausola di utilizzo dell'interfaccia a ciò che ha per essere visibile nel dichiarazioni dell'interfaccia.

non c'è un "trucco magico". I riferimenti circolari causerebbero un ciclo infinito per il compilatore (l'unità A richiede l'unità di compilazione B che richiede la compilazione dell'unità A che richiede la compilazione dell'unità B, ecc.).

Se si dispone di una specifica istanza in cui si pensa che non si può evitare i riferimenti circolari, modificare il tuo post e fornire il codice; Sono sicuro che qualcuno qui può aiutarti a capire come risolverlo.

+1

No, non lo faranno. Ad esempio, i programmi C di solito non hanno assolutamente problemi con le dipendenze cicliche. Il compilatore compila il modulo A, trova il modulo B non compilato, analizza l'origine di B per compilare A e quindi compila B. – fuz

+0

Questa domanda non ha nulla a che fare con "programmi c"; è specificatamente contrassegnato come "Delphi" e "dipendenze dell'unità" non sono le stesse "dipendenze del modulo" di c. Grazie per la tua indicazione, comunque; potrebbe essere carino se tu fossi familiare con il compilatore prima di commentarlo. :-) –

+0

Dici "per il compilatore". Ho appena scritto che non sono d'accordo con l'affermazione che ciò vale per qualsiasi compilatore. È possibile scrivere compilatori che possono funzionare con dipendenze circolari (anche per Delphi), ma il compilatore predefinito semplicemente non è in grado. – fuz

12
  1. Sembra che si abbiano problemi di progettazione del codice piuttosto seri. Accanto a molti segnali di tali problemi, quello è i riferimenti circolari unitari. Ma come hai detto tu non puoi rifattorizzare tutto il codice.
  2. Sposta tutto ciò che è possibile nella sezione IMPLEMENTAZIONE. Sono autorizzati ad avere riferimenti circolari.
  3. Per semplificare compito (2), è possibile utilizzare strumenti di terze parti 3d. Vorrei raccomandare - Peganza Pascal Analyzer (http://www.peganza.com). Suggerirà cosa puoi passare alla sezione di implementazione. Come ti darà molti più consigli per migliorare la qualità del tuo codice.
+3

Non ho agrre con il primo punto. I riferimenti circolari che possono apparire nel refactoring di una unità in molte unità non sono sempre una prova di una cattiva progettazione del codice iniziale. Sono l'effetto di una restrizione del compilatore, che non significa codice "buono" o "cattivo". Esempio: utilizzando Delphi 2009 Enterprise per creare il codice per il pattern visitatore GoF, tutto il codice è contenuto in un'unità: se provo a suddividerlo in unità per classi di modelli e visitatori, eseguo riferimenti circolari. Esiste quindi un problema di progettazione del codice nel pattern Visitor? (Vedi http://stackoverflow.com/questions/2356318/) – mjn

+0

No, ma il modello di visitatore non richiede più unità o –

+0

Nessuno ha incolpato anche la lingua (stessa). Penso che i riferimenti circolari siano una piaga tipica di Pascal (Delphi). So che anche in altre lingue ci sono, ma Pascal ... Penso anche che il compilatore potrebbe risolvere QUALCUNO del problema. Voglio dire, cosa c'è di sbagliato nell'impostare un puntatore a una classe anche se non abbiamo ancora compilato quella classe? – Ampere

1

Modelmaker Code Explorer ha un wizard davvero bello per elencare tutti gli usi, compresi i cicli.

Richiede la compilazione del progetto.

Sono d'accordo con gli altri utenti che si tratta di un problema di progettazione.
È necessario esaminare attentamente il progetto e rimuovere le unità non utilizzate.

A DelphiLive'09, ho eseguito una sessione dal titolo Smarter code with Databases and data aware controls che contiene alcuni suggerimenti su un buon design (non limitato alle app DB).

--jeroen

7

Esistono molti modi per evitare riferimenti circolari.

  1. Delegati. Troppo spesso, un oggetto eseguirà del codice che dovrebbe essere eseguito in un evento anziché essere eseguito dall'oggetto stesso. Che sia perché il programmatore che lavora al progetto ha avuto un tempo troppo breve (non lo siamo sempre?), Non aveva abbastanza esperienza/conoscenza o era semplicemente pigro, un codice come questo alla fine finisce nelle applicazioni. Esempio di mondo reale: componente TCPSocket che aggiorna direttamente alcuni componenti visivi sul MainForm dell'applicazione invece di fare in modo che il modulo principale registri una procedura "OnTCPActivity" sul componente.

  2. Classi/interfacce astratte. L'utilizzo di uno di essi consente di rimuovere una dipendenza diretta tra molte unità. Una classe astratta o un'interfaccia può essere dichiarata da sola nella propria unità, limitando al massimo le dipendenze. Esempio: la nostra applicazione ha un modulo di debug. Ha usi praticamente sull'intera applicazione in quanto visualizza informazioni da varie aree dell'applicazione. Ancora peggio, ogni modulo che consente di mostrare il modulo di debug finirà anche per richiedere tutte le unità dal modulo di debug. Un approccio migliore sarebbe avere un modulo di debug che è essenzialmente vuoto, ma che ha la capacità di registrare "DebugFrames".

    TDebugFrm.RegisterDebugFrame (Frame: TDebugFrame);

    In questo modo, TDebugFrm non ha dipendenze proprie (Tranne che nella classe TDebugFrame). Qualsiasi unità che richiede di mostrare il modulo di debug può farlo senza rischiare di aggiungere troppe dipendenze.

Ci sono molti altri esempi ... Scommetto che potrebbe riempire un libro a parte. Progettare una gerarchia di classi pulite in un modo efficiente nel tempo è piuttosto difficile da fare e viene fornito con esperienza. Conoscere gli strumenti disponibili per raggiungerlo e come usarli è il primo passo per raggiungerlo. Ma per rispondere alla tua domanda ... Non esiste una risposta adatta alla tua domanda, è sempre da prendere caso per caso.

1

Ho trovato una soluzione che non richiede l'uso di interfacce ma potrebbe non risolvere tutti i problemi del riferimento circolare.

Ho due classi in due unità: TMap e TTile.

TMap contiene una mappa e la visualizza utilizzando piastrelle isometriche (TTile).

Volevo avere un puntatore in TTile per puntare indietro sulla mappa. La mappa è una proprietà di classe di TTile.

Classe Var FoMap: TObject;

Normalmente, è necessario dichiarare ciascuna unità corrispondente nell'altra unità ... e ottenere il riferimento circolare.

Ecco, come mi aggiro.

In TTile, dichiaro che la mappa è un TObject e sposta l'unità Mappa nella clausola Uses della sezione Implementazione.

In questo modo posso utilizzare la mappa ma devo lanciarlo ogni volta su TMap per accedere alle sue proprietà.

Posso fare di meglio? Se potessi usare una funzione getter per scriverlo. Ma dovrò spostare Uses Map nella sezione Interface .... Quindi, torna al punto di partenza.

Nella sezione Implementazione, ho dichiarato una funzione getter che non fa parte della mia classe. Una semplice funzione.

Attuazione

Usa Map;

Mappa delle funzioni: TMap; Inizio Risultato: = TMap (TTile.Map); Fine;

Cool, ho pensato. Ora, ogni volta che devo chiamare una proprietà della mia mappa, uso semplicemente Map.MyProperty.

Ouch! Ho compilato! :) Non ha funzionato il modo previsto. Il compilatore usa la proprietà Map di TTile e non la mia funzione.

Quindi, rinominare la mia funzione in aMap. Ma la mia musa mi ha parlato. NOOOOO! Rinominare la proprietà della classe in aMap ... Ora posso usare Map come l'ho intented.

Map.Size; Questo chiama la mia piccola funzione, che ha premuto aMap come TMap;

Patrick Foresta

+0

Su StackOverflow devi pubblicare SOLO una risposta. Dovresti unire le tue risposte. E tienili corti e obiettivi. – Ampere

0

ho dato una risposta precedente, ma dopo un po 'di pensiero e graffi ho trovato un modo migliore per risolvere il problema di riferimento circolare. Ecco la mia prima unità che hanno bisogno di un puntatore su un oggetto TB definire in unità B.

unit Unit1; 

interface 

uses 
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, 
    Dialogs, b, StdCtrls; 

type 

    TForm1 = class(TForm) 
    Button1: TButton; 
    procedure Button1Click(Sender: TObject); 
    private 
    { Private declarations } 

    public 
    { Public declarations } 
    FoB: TB; 
    end; 

var 
    Form1: TForm1; 



implementation 

{$R *.dfm} 

procedure TForm1.Button1Click(Sender: TObject); 
begin 
    FoB := TB.Create(Self); 
    showmessage(FoB.owner.name); 
end; 

end. 

Ecco il codice dell'Unità B dove la tubercolosi ha un puntatore su TForm1.

unit B; 

interface 

    Uses 
    dialogs, Forms; 

    type 
    TForm1 = class(TForm); 

    TB = class 
    private 
     FaOwner: TForm1; 
    public 
     constructor Create(aOwner: TForm); 
     property owner: TForm1 read FaOwner; 
    end; 

implementation 
    uses unit1; 

    Constructor TB.create(aOwner: TForm); 
    Begin 
    FaOwner := TForm1(aOwner); 

    FaOwner.Left := 500; 
    End;//Constructor 
end. 

E qui perché compila. La prima unità B dichiara l'uso dell'unità 1 nella sezione dell'implementazione. Risolvere immediatamente l'unità di riferimento circolare tra Unità1 e Unità B.

Ma per consentire a Delphi di compilare, ho bisogno di dargli qualcosa da masticare sulla dichiarazione di FaOwner: TForm1. Quindi, aggiungo il nome della classe stub TForm1 che corrisponde alla dichiarazione di TForm1 in Unità1. Successivamente, quando arriva il momento di chiamare il costruttore, TForm1 è in grado di passare se stesso ha il parametro. Nel codice costruttore, devo digitare il parametro aOwner su Unit1.TForm1. E voilà, FaOwner il suo set per puntare sulla mia forma.

Ora, se la classe TB deve utilizzare FaOwner internamente, non è necessario convertirla ogni volta da a Unit1.TForm1 perché entrambe le dichiarazioni sono uguali. Si noti che è possibile impostare la dichiarazione di al costruttore per

Constructor TB.create(aOwner: TForm1); ma quando TForm1 chiamerà il costruttore e superare se stessa ha un parametro, è necessario typecast ha b.TForm1. Altrimenti Delphi genererà un errore che dice che entrambi TForm1 non sono compatibili. Quindi ogni volta che chiami la TB.costruttore dovrai digitare per l'appropriato TForm1. La prima soluzione, usando un antenato comune, è la sua migliore. Scrivi il typecast una volta e dimenticalo.

Dopo averlo pubblicato, mi sono reso conto che ho fatto un errore dicendo che entrambi TForm1 erano identici. Non sono Unit1.TForm1 ha componenti e metodi sconosciuti a B.TForm1. La TB lunga non ha bisogno di usarli o è solo necessario usare la comunanza data da TForm che stai bene. Se è necessario chiamare qualcosa di particolare a UNit1.TForm1 da TB, sarà necessario convertirlo in Unit1.TForm1.

Ho provato e testato con Delphi 2010 e ha compilato e lavorato.

Spero che ti aiuti e ti risparmi un po 'di mal di testa.

+0

Questo è come tenere una granata innescata e cercare di auto-indurre un attacco epilettico. Il problema è che ti stai affidando a trucchi per l'esplorazione contro-intuitivi. Ancora più importante, funziona davvero *** *** *** correttamente? Se avessi una terza unità e chiedessi un'istanza di TB per il suo proprietario, otterrebbe la classe del proprietario corretta? (** No **) Tutte le sostituzioni polimorfiche si comportano correttamente? (Non sono sicuro) Dato che il codice attualmente esiste, nulla mi impedisce di creare 'TB' con un'istanza di' TSomeEntirelyDifferentForm' che probabilmente porterà a un disastro estremo nel lungo periodo. (Almeno _questo_ può essere corretto) –

+0

Sono d'accordo che non risolve tutto. La maggior parte delle volte si desidera solo avere un puntatore all'oggetto proprietario. In tal caso, funziona. La soluzione perfetta sarebbe venuta da Delphi. Se Delphi è in grado di leggere tutte le sezioni dell'interfaccia, allora vedi se c'è qualcosa che non funziona per avvertirci. No, vai ora con le sezioni di implementazione. –

+0

@Craig E per rispondere alla tua _f hai avuto una terza unità e chiedere un'istanza di TB per il suo proprietario otterrebbe la classe del proprietario corretta? (No) _ Sì, 'FaOwner: TForm1;' dichiara semplicemente un puntatore a un oggetto. Una volta che il puntatore è inizializzato, punta sempre allo stesso oggetto. La prova di ciò potrebbe cambiare il mio codice, ignorare la dichiarazione di TForm1 nell'unità B e sostituirla con un puntatore. E il codice sta Ma dovrei dattilografarlo ogni volta che lo uso. Ma con il mio stub TForm1, ho solo bisogno di digitare se voglio usare qualcosa solo dichiarare in Unit1.TForm1. –