Modules and Classes in Ruby


#1

I’ve been working with modules and classes for a while now but I don’t know that I truly understand all of the finer points on how to do specific things.

For instance I’m nesting classes inside of modules and those modules inside of other modules to carefully control the namespace for example:

module Medeek_Engineering_Inc_Extensions
	module MedeekFoundationPlugin
	module Slabongrade

class MedeekMethods
  	class << self

def some_method_for_slab_on_grades
   ....
end

.... (more methods  for this module)...

end
end
end
end
end

Then I might have another module for stemwalls:

module Medeek_Engineering_Inc_Extensions
	module MedeekFoundationPlugin
	module Stemwalls

class MedeekMethods
  	class << self

def some_method_for_stemwalls
   ....
end

.... (more methods  for this module)...

end
end
end
end
end

#2

Now I want to create a global module that the instance variables can be accessed from my other two modules:

module Medeek_Engineering_Inc_Extensions
	module MedeekFoundationPlugin

class MedeekMethods
  	class << self

def global_settings
  @globalvar1 = "something"
  @globalvar2 = "something"
end

.... (more methods  for this module)...


end
end
end
end

I want to be able to call the global settings method from either of my two other modules with something like this line of code:

Medeek_Engineering_Inc_Extensions::MedeekFoundationPlugin::MedeekMethods.global_settings

and then those variables created within the global_settings method be accessible from inside of the other two modules.

I have a number of books on Ruby now but they only serve to further confuse me with regards to nesting classes and modules and how all of this relates to the instance variables.

I’m already doing something similar to this in the other two plugins but knowing my lack of experience and overall newness with Ruby I’ve probably got it all wrong even though it somehow works.


#3

I’m not entirely sure I understand exactly what you are looking to do there. You want to be able to use @globalvar1 directly from another module?

As for accessing app settings in my extensions, what I have done some times is to assign an instance of my Settings class to a constant:

module Example
   SETTINGS = AppSettings.new
end

SETTINGS is then easily accessible to children classes/modules of Example due to Ruby’s namespace lookup.

That being said, it’s a quick and easy setup, but it also tightly couples my classes/modules to the constant. It probably would be better to inject the Settings instance into the classes or methods that needs it as an argument instead. (In order to loosen the coupling between the components.)


#4

Essentially yes, and again I may be going about this completely wrong.

I would like to keep my various parts of the Foundation plugin separate (for namespace and other reasons). So I’ve organized them into different modules as shown above.

However, I would like to maintain only one global settings (file/module) that any of these other modules can load up and run. Once the global settings has run once I will probably insert some logic so that it doesn’t need to reload all of this data within a session. The variables (parameters) from the global settings and its methods need to be accessible from all other modules of the plugin.

I’m just trying to figure out the best and most efficient way of doing this, I am sure there is more than one way to pull this off, but not being a real Ruby pro yet my way of handling it is probably neither efficient nor recommended.


#5

In this regard I’m also trying to better understand if using a Mix-in module might be the way to handle this sort of thing, and I’m a little unsure on how the syntax for the global settings module should be constructed.


#6

You don’t really need such a deep nesting hierarchy and while it keeps things compartmented, its does not keep things independent (parts of the extension need to know the naming of other parts to access shared functionality).
Besides that, some_method_for_stemwalls is used like a class/module function, it is not an instance method, it does not exploit the advantages of classes and instances.


Thomthom’s suggestion is to use instancing (e.g. create an instance of his Settings class), but since only one instance is likely to be used, you can make it statically accessible with a constant. You would still have the advantages of instancing (you can create another instance of Settings without conflicts).


Another alternative is to inject dependencies when initializing objects. Let’s assume you have two extensions or major modules Slabongrade and Stemwalls. If both access settings with Medeek_Engineering_Inc_Extensions::MedeekFoundationPlugin::MedeekMethods.global_settings they are strongly coupled to Medeek_Engineering_Inc_Extensions::MedeekFoundationPlugin and only work if this complete module path exists and has been loaded. It’s not easily replaceable by other settings.
You can however inject to a class everything it needs, e.g.:

# A class that has a dependency to settings
class Slabongrade
  def initialize(settings)
    @settings = settings
  end
  
  def some_method
    # A method that requires settings
    @settings.window_height
  end
end

# Another class that has a dependency to settings
class Stemwalls
  def initialize(settings)
    @settings = settings
  end
  # ...
end

# In the main initializing method of your plugin:
settings = Settings.new()
slabongrade = Slabongrade.new(settings)
stemwalls = Stemwalls.new(settings)
  • Both don’t directly access internal functionality of other modules from outside. Implementation details are somewhat protected.
  • They don’t even need to know naming of other modules (except methods one can call on the injected dependency).
  • The dependency is replaceable. You can initialize Slabongrade and Stemwalls with different setting objects/classes, or even two instances of Slabongrade with different settings. You can easily separate both into separate extensions without much effort.

#7

Which also brings up another question I’ve often had. Should the global settings be loaded up when the plugin is loaded (ie. SketchUp startup) or should I only load them up when the user initiates some command with the plugin. I guess I’m not wanting to load a bunch of stuff into the memory if it might slow down SU inadvertently and the user may not be using my plugin during that session.


#8

Sketchup.read_default is very fast.
I used to have my own settings class that stored everything in a single registry key to avoid clutter. It had to parse it, handle parsing errors, cache it and write back before SketchUp was closed. All too complicated for little benefit. With some tricks (by thomthom), you can store nested/hierarchical keys in SketchUp’s preferences.
Do not think about optimizations first (this leads to overengineering), but only when problems appear that make them necessary because then you know what to optimize.


#9

Okay, so you are saying its not bad to load up your global settings at startup. I’ve never noticed a slowdown so it is fast but I’m just worried about all of those variable/constant definitions floating around doing nothing, for my Wall plugin the number has really grown. The last thing I want to do is make SU unstable for the user with a bunch of data being maintained that they are not using in that particular session, maybe I am overthinking this.


#10

I meant it is fast enough to be called multiple times on demand (instead of building a huge caching mechanism around).


#11

I’ve tried several times to get you to do it the easy way … you don’t need a class, that just confuses things.

Otherwise just read the “ol’ Pick Axe” book’s chapter on Mixin modules.


#12

I’d suggest not to use constants since, as you say, they consume memory even when not needed. The easiest route to go is to use a Singleton, since the instance
is only created when you call it, and it will only be created once. See https://www.rubyguides.com/2018/05/singleton-pattern-in-ruby/

require 'singleton'
module Medeek
  module Common
    class GlobalSettings
      include Singleton

      def settingA()
        # get and return it
      end #def

    end #class
  end # module
end # module

# access it
Medeek::Common::GlobalSettings.instance.settingA()

Then, whenever you have some spare time, read about Dependency Injection and Service Providers.


#13

P.S. - As said many times, the pattern in my example template can also be used with @@variables to share variables among modules and classes via inclusion (not just constants.) The reference (be it constant or @@var) can point at a hash or Ostruct of settings.


#14

I’m slowly coming around to your thinking. This old dog can learn a few new tricks but you have to bash me over the head a few times before they stick.


#15

Which is useful for testing. Hence singletons isn’t ideal.

Normally I don’t worry about this. But some of my larger extensions, Vertex Tools and QuadFace Tools actually have quite a bit of code to load. With 50…100+ extensions this can add up. Though, it also appear that the creation of toolbars outnumbers this.

I disagree. I have experienced that this can have a notifiable effect when used in validation procs and mouse events. Maybe things have changed after perferences is now coming from JSONs. But before it went to the registry.

Validation procs are particulary something to pay attention to, under Windows they are called constantly. So you want to do as little as possible. Resolve your state it should represent into some cached value that doesn’t need to be computed every time.

I use a Settings wrapper class over read/write_default that cache the values in a hash for faster access times.