Jump to content
DavidJr.

A better way to share global data structures than as global variables?

Recommended Posts

Hi,

 

I am supporting a large "legacy" application and it uses a lot of global variables especially Array of records that are used by different methods.  Recently I moved all the methods into a TThread class and implemented properties with "getters" and "setters" to make certain information available to the Main Form thread.  However, there is still the problem of global variables that I need to address in a clean way.  These global variables are NOT accessed outside the Thread class right now using the global variables as class members but they are still technically global.  .   How can I get access to these global variables without them being global.  The application just works as it,  but I would like to start refactoring the application.

 

Is there  basic hierarchy using classes and sub classes I should consider?

 

Thanks.

Share this post


Link to post

I tend to move it all into a DataModule (one per thread type/definition) so it ends up under an instance variable for that namespace wise. 

Share this post


Link to post

Global variables in a unit often suggest there are classes that could be created to encapsulate them, possibly as static / class variables.

 

How are they being used? Two things come to mind most frequently: they may be associated with singleton patterns; or they could be acting as buffers for data loaded from a DB or INI files.

 

The presence of lots of global vars tells me the original writers probably used VB a lot and weren't very skilled with OOP concepts.

 

(I once saw a Delphi app that had a ton of global vars in it, and a couple of classes that were used to "collect" a few dozen methods together. There were no data members in these classes; the methods all used the global vars. Yet the code had numerous places where they created and freed these "objects". it was the goofiest code I've ever seen. But overall, it looked like a big VB program simply translated into Pascal and made me want to gag. I told my boss I wouldn't touch it with a 10' pole.)

 

I don't think moving methods that access global vars into TThreads is advisable because that can cause contention issues if they're ever updated, and that would require even more coding to resolve.

 

If the algorithm goes through a process first and loads up data into the global vars, then treats it all as as read-only at run-time, then maybe having multiple threads could be useful, but they won't in the main thread. And that's an optimization that I'd leave for later.

 

17 hours ago, DavidJr. said:

I moved all the methods into a TThread class and implemented properties with "getters" and "setters" to make certain information available to the Main Form thread.

 

Why did you think of doing this first? (Just curious.)

 

Off-hand, it sounds like a recipe for disaster. The threads will NOT run in the main form thread (they're separate threads!), and simply using properties won't buy you any kind of protection against contention at run-time between multiple threads unless you build that into the getters and setters -- which, BTW, won't know what threads they're dealing with unless you add even MORE global vars! They need to be encapsulated into classes FIRST or you're just creating a bigger, more complex mess for yourself.

 

17 hours ago, DavidJr. said:

How can I get access to these global variables without them being global.

 

You need to study the code and see what it's doing.

 

But start encapsulating things based on space first (with vars moved into classes), not time (based on threading).

 

Identify groups of related variables and the methods that refer to them, and put them into separate classes and give each one a meaningful name.

 

If the vars are only initialized once and then read-only, then make them static / class vars. They're probably used as configuration parameters.

 

If they're used for buffering values being read in or processed, then they're going to be private data members that need properties defined to access them. The logic will be working like this:

- initialize operational / config parameters (some of the global vars)

- for each line or batch of input data do

-- read the data and put it into some global vars

-- process the data that was just read

-- save the results somewhere

- go on to the next step (if needed) -- eg, display results

- wrap everything up and shut down

 

If you've got some global vars that are all set up as arrays of the same length, this is a sign that you can put them into a class (as single vars, not arrays) and you'd have as many instances as the length of the arrays.

 

You could then put the class instances into a single array or list.

 

 

 

 

Edited by David Schwartz

Share this post


Link to post
16 hours ago, DavidJr. said:

How can I get access to these global variables without them being global

Copy the values either in c-tor once or in some init method periodically.

I'd start the task step by step:

- group the standalone globals into logical structures, f.ex., one record Globals with subrecords

- examine each usage of these globals (now easily locatable by name search - that's why Globals record!) and reduce access by extending c-tors and so on

Share this post


Link to post
3 hours ago, Ruslan said:

I've moved all global variables to a Singleton

why? That might make sense if they're all related. It's roughly equivalent to moving them into a Data Module, because there's usually only one of them ever instantiated.

 

But most of the time a bunch of global vars represent collections of related state vars that belong inside of distinct classes with setter and getter methods moved into the class as well.

 

1 hour ago, Fr0sT.Brutal said:

group the standalone globals into logical structures, f.ex., one record Globals with subrecords

 

So you're simply putting bunch of global vars into a bucket that itself is a global var? 

 

All you've accomplished is adding a namespace to the global variables that makes references to the vars longer.

 

It's like taking a long list of variables in a C program and putting STRUCT { ... } MYGLOBALS; around  them. 

 

You're still dealing with the same bunch of global variables, and if you try defining multiple instances, you'll end up with quite a mess.

Edited by David Schwartz

Share this post


Link to post
1 hour ago, David Schwartz said:

So you're simply putting bunch of global vars into a bucket that itself is a global var? 

You likely didn't read the next line I wrote.

 

1 hour ago, David Schwartz said:

It's roughly equivalent to moving them into a Data Module, because there's usually only one of them ever instantiated.

Datamodule usage is senseless here for me. It's a container for components useful for design-time customizing but nothing else.

Share this post


Link to post

A datamodule has its advantage when component properties are loaded from the DFM - that includes wiring of components and event handlers. (I am aware that some see that as a disadvantage)

 

Another point is that, if you don't actively prohibit that, ist is registered in Screen.DataModules and can be found without that dreaded global instance variable.

  • Like 1

Share this post


Link to post
1 hour ago, Fr0sT.Brutal said:

You likely didn't read the next line I wrote.

 

Datamodule usage is senseless here for me. It's a container for components useful for design-time customizing but nothing else.

Yeah, I caught the "... and so on" part. That's where you wave your hands to refer to everything that happens when you push the button to launch the space shuttle, right?

 

It does not matter what kind of bucket or box or container you put global variables into -- they're still defined at the global level, and they do not belong there.

 

A Data Module is simply a class, which is a container just like records. It's referenced by the global instance var that every auto-created form in Delphi has, and there's usually just one of them. You can put anything into it that you want because, as I said, it's just a class.

 

If one is senseless to use as a container, then the other is equally senseless for the same reasons. It's not worth arguing about or defending.

 

You are free to have a preference for records over classes, and forms over data modules. It doesn't matter. They're all containers available to Delphi programmers and mostly interchangeable.

 

Encapsulation in OOP terms is a way of creating abstractions around collections of related data and methods. 

 

Classes are an abstraction mechanism that lets you encapsulate state and model related behaviors that maintain the state, all in a single "object". Forms and Data Modules are both classes.

 

Records are very similar only they have "pass by value" semantics rather than "pass by reference", so local instances usually take up a LOT of space on the stack. You also need to manage dynamic instances of them wtih additional syntactical decorations. 

 

I'm not talking about your personal preferences here, I'm talking about basic OOP principles, and particularly encapsulation. Because that's what the OP asked for help with.

 

 

Share this post


Link to post

To start I will answer a question that was asked:

13 hours ago, David Schwartz said:

 

Why did you think of doing this first? (Just curious.) 

 

Off-hand, it sounds like a recipe for disaster. The threads will NOT run in the main form thread (they're separate threads!), and simply using properties won't buy you any kind of protection against contention at run-time between multiple threads unless you build that into the getters and setters -- which, BTW, won't know what threads they're dealing with unless you add even MORE global vars! They need to be encapsulated into classes FIRST or you're just creating a bigger, more complex mess for yourself.

 

 

The Getters and Setters were put in place for the purpose of reading a value or initializing a value only,  but all the work is done in the (for lack of a better term) back-end thread.  So there is some sorting and data manipulation that takes place with these array of records.  Also in the "backend thread"  I need to keep track of a "global index" to track where I am at as I process/copy the original array of record.  Ultimately the new data in the second array of record will replace the first array of record before being written to a file.  It all just works  however, its a careful juggling of data that is very complex.  It was this way before I started working on it.  

 

Someone mentioned a "data module",  this sounds like a class just for the data,  am I understanding this correctly?  right off the bat I am trying to identify what global variables can actually be localized in the methods they are used in.  But from there, the data that needs to be visible within the "backend" thread instance (and all its methods).  I think if I write this from the ground up I would have maybe created a parent class with the data available,  however I do not see how an array of record can be used as a property.

Edited by DavidJr.

Share this post


Link to post
59 minutes ago, David Schwartz said:

That's where you wave your hands to refer to everything that happens when you push the button to launch the space shuttle, right?

I wasn't going to go too deep in describing all these design patterns, I'll leave the prize for most huge posts to you 😉 All I wanted to say by "and so on" is said in the 1st line of my post which seem to hide from your attention as well, eh?

1 hour ago, David Schwartz said:

It does not matter what kind of bucket or box or container you put global variables into -- they're still defined at the global level, and they do not belong there.

Sure. What does matter is how to perform further refactoring. With standalone global var you have to search for its usage filtering out possible name collisions with local vars. My way leaves all this work to compiler - force "namespace" and then easy search by an unique name of that "namespace".

1 hour ago, David Schwartz said:

A Data Module is simply a class, which is a container just like records. It's referenced by the global instance var that every auto-created form in Delphi has, and there's usually just one of them. You can put anything into it that you want because, as I said, it's just a class.

A data module is not simply a class but more like a form. It requires DFM. Why DFM when there are no components inside? That's excess. You don't have full and flexible control over when a DM is created. You have to track form creation order. For me that's kind of riding down the hill on an office wheeled chair. In theory, you can. In practice, it's senseless.

Share this post


Link to post

I use datamodules alot - I remove them from the autocreate list and then manually create and delete them in code (initialising with a nullptr for the owner (I'm a C++ gent)). I find this very convenient. Using pooled connections I find this works well.

  • Like 1

Share this post


Link to post

Related to your title "A better way to share global data structures than as global variables?",

 

Not sure if always "better", but for sure an "other" way:

I like to "share" global data by TMessage sometimes, which could completely decouple separate units.

You only have to initialize a "global" module, sunscribing to its message,

and then you can request global (or non-global) data by messaging from everywhere you want.

 

You can implement thread-safetyness, imutability as you like.

Share this post


Link to post
On 1/27/2022 at 7:09 AM, Fr0sT.Brutal said:

A data module is not simply a class but more like a form. It requires DFM. Why DFM when there are no components inside? That's excess. You don't have full and flexible control over when a DM is created. You have to track form creation order. For me that's kind of riding down the hill on an office wheeled chair. In theory, you can. In practice, it's senseless.

I think that if Delphi was created today, the DFM file would be a JSON file. Since you're familiar with JSON, you know that if there's nothing in it, it's very small. The format used by DFM files is like JSON only simpler.

 

DFM files are used to save property values defined in the Object Inspector at design time for a given form or DFM. 

 

If there are no components on the form or DM canvas, then the DFM file is basically empty.

 

Also, since it's managed the same way as any other form in Delphi, you have the same degree of control of when they're created.

 

By default, they're created automatically. But you can right-click on your project, select Options... then Forms and move it to the right-hand column which represents all of the forms and DMs that you do NOT want to be created automatically.

 

A simpler way is to just right-click on the project, select View Source, and delete the line where a form or DM is created, then save it.

 

In your code, simply call Create where you want an instance and save the reference in a variable somewhere. You can use the one that's emitted in the file by default (a global var in the unit), or you can use any other variable.

 

You can also call it multiple times to create multiple instances.

 

When you're done, just just Free them.

 

I'm sorry for so many words, but I take a lot of words to explain stuff because it seems a lot of people cannot read between the lines of one-line answers.

 

Since I have taken so many words to explain this, I hope you'll actually try it out and see that what I said earlier is correct.

Edited by David Schwartz
  • Like 1

Share this post


Link to post
On 1/27/2022 at 6:56 AM, DavidJr. said:

To start I will answer a question that was asked:

 

 

The Getters and Setters were put in place for the purpose of reading a value or initializing a value only,  but all the work is done in the (for lack of a better term) back-end thread.  So there is some sorting and data manipulation that takes place with these array of records.  Also in the "backend thread"  I need to keep track of a "global index" to track where I am at as I process/copy the original array of record.  Ultimately the new data in the second array of record will replace the first array of record before being written to a file.  It all just works  however, its a careful juggling of data that is very complex.  It was this way before I started working on it.  

 

Someone mentioned a "data module",  this sounds like a class just for the data,  am I understanding this correctly?  right off the bat I am trying to identify what global variables can actually be localized in the methods they are used in.  But from there, the data that needs to be visible within the "backend" thread instance (and all its methods).  I think if I write this from the ground up I would have maybe created a parent class with the data available,  however I do not see how an array of record can be used as a property.

 

I don't see multi-threading as a solution for reducing the complexity of having too many global variables. I imagine it just creates more complexity. 

 

Also, I don't say this to be critical, but you don't seem to understand OOP. There are a LOT of programmers who don't. And that's fine.

 

Delphi's language is called "Object Pascal" to differentiate it from original Pascal that was more like 'C'. The Delphi IDE generates forms that are all objects and a 'main' procedure that's used to create everything. The entire Visual Control Library (VCL) as well as FireMonkey (FMX) is based around objects. The component libraries are all objects. Everything in Delphi is oriented around objects.

 

If there are a bunch of global variables in a Delphi form unit, that means that whomever wrote it was probably not skilled with OOP.

 

You should probably refactor the code to get all of those global vars into classes, along with all of the methods that read and write to them. It's not entirely straightforward, but a lot of it is. 

 

There's a free book available that teaches a lot of this basic OOP stuff. 

 

Object Pascal Handbook by Marco Cantu


If you login to your Embarcadero Code Central portal, it's hiding there somewhere as a free download.

 

I also found this link:

 

http://forms.embarcadero.com/DownloadMarcoCantueBook

 

 

Share this post


Link to post
On 1/27/2022 at 6:37 PM, Rollo62 said:

I like to "share" global data by TMessage sometimes, which could completely decouple separate units.

You only have to initialize a "global" module, sunscribing to its message,

and then you can request global (or non-global) data by messaging from everywhere you want.

TMessage from System.Messaging?

 

On 1/27/2022 at 6:37 PM, Rollo62 said:

You can implement thread-safetyness, imutability as you like.

System.Messaging is not thread-safe so you cannot use it for anything thread related.

Share this post


Link to post
On 1/29/2022 at 10:05 AM, David Schwartz said:

so many words

and so many actions just to hold non-TComponent stuff? Honestly this is a bloat for no reason. 0 pro's and several con's => no-go for me

Share this post


Link to post
23 minutes ago, Fr0sT.Brutal said:

and so many actions just to hold non-TComponent stuff? Honestly this is a bloat for no reason. 0 pro's and several con's => no-go for me

I'm really not sure what to make of this because you have to declare these things somewhere, right?

 

Putting them in a DataModule is one option, of many. There is no "bloat" if there are no components other than two lines in the DFM:

 

object TmyDataModule

end

 

nothing else is stored in the DFM, and there's no more code required than you'd have if you understood the proper role of "encapsulation" in OOP design.

 

Which explains why you obsessively prefer the use of records over classes -- so you don't have to bother with calling Create or Free or dealing with any sort of memory management. Which is to say, you make them all global variable to avoid "bloat".

 

That's NOT proper OOP design!

 

But yes, it certainly is another option.

 

Just for the record, the point you're trying to make is that one should avoid the use of proper OOP design techniques in a full OOP language simply to avoid unavoidable "bloat" that comes with using an OOP language.

 

There are people who claim you can do everything in C that you can do in C++ without the supposed "overhead". Then they proceed to write code that has 5x the SLOC as the equivalent C++ code would have, is harder to maintain, has a similar memory footprint, and benchmarks within a few percent of the other. Only it took a lot longer to write and costs more to maintain.

 

This is the thing that opponents of OOP don't understand: the faster computers get, the less that alleged "bloat" actually costs. That is, bloat costs less and less every year.

 

But the cost of programming and maintenance just keep going UP and UP!

 

So sticking with programming methods that forsake programming and maintenance costs in favor of reducing "bloat" is a fool's errand -- you're constantly increasing your budget, spending more to save less over time.

 

And that is a choice you have as well.

Share this post


Link to post
1 hour ago, David Schwartz said:

Which explains why you obsessively prefer the use of records over classes -- so you don't have to bother with calling Create or Free or dealing with any sort of memory management. Which is to say, you make them all global variable to avoid "bloat".

Not at all. I'm fine with objects where applicable. Some of my projects have singletone Settings, some use more complicated techs for communication. However I won't bother with object overhead where there are only a few global options exist.

What I consider bloat is using datamodule for storage. Just a simple class located in common shared unit will serve equally and without excess entities involved.

Anyway, I didn't see YOUR suggestion besides placing stuff into datamodule which is just "the same eggs but from a sideview" as global singletone. Okay, my habits are bad and old and non-OOP, what will be your solution?

Edited by Fr0sT.Brutal

Share this post


Link to post
16 hours ago, Fr0sT.Brutal said:

Okay, my habits are bad and old and non-OOP, what will be your solution?

I don't throw away dollars to save pennies.

 

That is to say, I value my time more than the cost of computer memory and CPU cycles, because the value of my time keeps going up while the cost of memory and CPU cycles drops much faster.

  • Like 1

Share this post


Link to post
5 hours ago, David Schwartz said:

I don't throw away dollars to save pennies.

 

That is to say, I value my time more than the cost of computer memory and CPU cycles, because the value of my time keeps going up while the cost of memory and CPU cycles drops much faster.

Amazingly exhaustive answer! Looks like you also highly value your time on writing a meaningful post 😄

Edited by Fr0sT.Brutal

Share this post


Link to post

sometimes the test routines to check that my own memory is still working go a little haywire....

 

 

 

 

Edited by David Schwartz

Share this post


Link to post

In your case, I would put the variables you are working with in a class or even a record and set the scope to what you selected as private to the thread.  Keep it simple, its all about preventing access from multiple threads.  If you have a global variable that needs to be accessed by multiple threads, protect the access using some sort of semaphore, such as a critical section or spin lock.

  • Like 1

Share this post


Link to post
6 hours ago, InfoHills said:

Refactoring a legacy application to reduce the usage of global variables is a good step towards improving its maintainability and readability.

Straight Outta ChatGPT

  • Like 9

Share this post


Link to post

I'm on the edge of my seat to see where this will lead...

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×