Feature #21194
openHow to manage application-level information in Ruby application
Description
Goal¶
I want to manage application-level information (e.g., application configuration) while making it easily accessible from the part classes of the application. Additionally, I want to support multiple instances of the application within a single process.
Current approach 1: Global variables¶
The simplest way to achieve this is by using global variables.
class MyApp
class Part1
def run
using $config[:part1]...
end
end
class Part2
def run
using $config[:part2]...
end
end
def initialize(config)
$config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: "aaa", part2: "bbb" })
# app2 = MyApp.new({ part1: "AAA", part2: "BBB" }) # Cannot create this
app1.run
This code is simple and clear, but it does not allow creating multiple MyApp
instances with different configurations.
To achieve that, we would need to create separate process using fork
or spawn
.
This limitation remains even if we replace global variables with constants (MyApp::Config
) or class methods (MyApp.config
).
Current approach 2: Passing configuration via initialize
A textbook and well-structured approach is to explicitly pass configuration through initialize
.
class MyApp
class Part1
def initialize(config)
@config = config
end
def run
using @config[:part1]...
end
end
class Part2
def initialize(config)
@config = config
end
def run
using @config[:part2]...
end
end
def initialize(config)
@part1 = Part1.new(config)
@part2 = Part2.new(config)
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This approach allows creating multiple MyApp instances with different configurations in a single Ruby process.
However, it has two major drawbacks:
-
config
must be passed explicitly in everyinitialize
andnew
call, making the code verbose. - Both
Part1
andPart2
instances hold their own@config
variables, which is redundant -- especially when creating a large number of small instances (e.g., tree nodes).
Current approach 3: Thread-local storage¶
Storing configuration in Thread[:config]
allows multiple application instances without explicit parameter passing.
class MyApp
class Part1
def run
using Thread[:config][:part1]...
end
end
class Part2
def run
using Thread[:config][:part2]...
end
end
def initialize(config)
@config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
Thread[:config] = config
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This approach is mostly effective but has an issue:
-
Thread[:config] = @config
must be set at the beginning ofMyApp#run
. While this is manageable if there is only one public API, it becomes error-prone when multiple APIs exist.
Note that using Fiber#[]
instead of Thread#[]
has the same issue.
Proposal¶
Ideally, we want to support multiple application instances while keeping the simplicity of the global variable approach.
To achieve this, I propose introducing a new type variable, such as $@config
:
-
$@config
belongs to an instance - When accessing
$@config
, it is looked up not only inself
but also by traversing the call stack to find the nearestself
instance that has$@config
.
With this, the code could be written as follows:
class MyApp
class Part1
def run
$@config[:part1] # accesses MyApp's $@config
end
end
class Part2
def run
$@config[:part2]
end
end
def initialize(config)
$@config = config
@part1 = Part1.new
@part2 = Part2.new
end
def run
@part1.run
@part2.run
end
end
app1 = MyApp.new({ part1: ..., part2: ... })
app2 = MyApp.new({ part1: ..., part2: ... })
app1.run
app2.run
This behaves similarly to dynamically scoped variables but differs in that it is resolved through the self
instances.
(Thread.new
is a bit problematic: if you use Thread.new
in a method of MyApp::Part1
, you wouldn't have access to $@config
in it. It might be nice to take over all $@x
variables.)
Feedback wanted¶
Whenever I write a large Ruby application, I encounter this problem.
However, TBH, I am not entirely confident that my proposed solution is the best one.
Do you ever encounter this problem? How do you deal with the problem when you do? Is there a better workaround?
No data to display