Beginner's mental model of Ruby object model

We create mental models for better understanding and orientation in a problem area. They do not have to correspond to exact models. They often simplify (based on level of our understanding) and also dynamically improve as our understanding develop.

This is my beginner's mental model of how objects are constructed in Ruby. It does not conform to real Ruby object model. It is simplification which enabled me to understand how object-oriented programming (OOP) in Ruby works.

In LaunchSchool, I created it at the beginning of LS 120 course. It is sufficient for that course, however later it has to be expanded.

Simplification of ruby object model

This model does not take advanced concepts into consideration: singleton classes/metaclasses, class instance variables, fact that instance methods are in fact parts of classes, not instances etc.

This mental model is not covering basics of OOP or constructing/using objects in Ruby. It is focusing on describing how we can ***imagine*** that Ruby implements OOP concepts. It helped me to orient in issues like method availability, selfchange, implicit caller, what variables are available and when, which methods I can call and how their syntax is chaned and similar.

This mental model uses four concepts for better understanding which are not part of standard terminology — I just made them up. I point it out by using quotes when I use them (“common class area”, “form for instances”, “composite objects”, and “method table/method inheritance table”)

Class

Class has two functions and we can imagine that it has two separate parts:

1. ”Form for instances”: It includes instance methods (their definitions) which will later initialize instance variables. They will be available in instance of object after we create it and will constitute its attributes and behavior. Until we create instances, these forms are just in implicit state (they are just form waiting for its substance) and cannot be called, used or referenced.

2. ”Common class area”: It includes class methods (their definitions), constants and class variables. These elements are shared by all instances of the class and are also accessible by calling the class itself.

Inheritance creates “composite objects/classes”

We can order classes into hierarchies. Some classes can constitute superclasses to our class (by using `<` in class definition) and some modules can be included in our class and/or its superclasses (by using includein class definition).

If we do not create explicit hierarchy of classes above our class, it will be nevertheless implicitly superclassed by class Object (and module Kernel and class BasicObject). So all classes and all instances will be basically in inheritance chain — either explicitly or implicitly.

Based on this knowledge we can imagine that our current (final) class (in fact every class and every object) is always ”composite class” (or instance), consisting of behaviors and attributes (methods and variables) of all classes and modules chained up in this inheritance hierarchy — starting from current class and going up towards BasicObject.

All methods and attributes which are defined in superclasses (or included modules) will be available in our “composite” final class. This is valid for both classes (their “common class area” — class methods and variables) and for instances created from “form for instances” of these classes.

Inheritance works only from current object up, not down — when we call Class B, method defined in its SubClass C will be not automatically (implicitly) available. Only methods from SuperClass A are available. We should always consider the hierarchy above the actual caller (see bellow: Caller), not general class hierarchy defined in our code structure — it may have other subclasses or modules which lie bellow the object we currently work from (=current object, caller) and are not available in this scope (or might be available just by using explicit caller in case of class methods). There is slightly different situation with class variables which are available everywhere in class hierarchy — see Variables.

  • See example bellow:
 class Animal
def self.general_noise
puts “This is noise”
end
end

class Mammal < Animal
end

class Dog < Animal
def self.noise
puts “rufff”
end
end

Mammal.general_noise # “This is noise”
Mammal.noise # => undefined method #noise…

Inheritance “method table”

We can imagine above mentioned hierarchy as a table which includes all available methods and which is included in our “composite object” (being it class or instance). Each class and module up in the inheritance hierarchy creates one individual row, which contains all the methods defined in the particular class or module. The lowest row belongs to our actual class, row above is of its first superclass (or included module) and so on, and the top row is of greatest common superclass — SuperObject. By calling ClassName.ancestorswe can view rows (and their order) of this imaginary (it is our mental model concept, not actual ruby construct!) “method table”

”Composite classes” are constructed from “common class areas” of current class and classes up in hierarchy and contain

  • “method table” with all class methods
  • class variables
  • and constants 
    … from current class and their predecessors.

”Composite objects” (instances) are constructed from “form of instances” part of individual classes in hierarchy and contain

  • “method table” containing all instance methods
  • instance variables

Whenever we call a method on/in our actual final “composite class” (or instance), ruby will start to look for this method in its table, starting from lowest rows, coming up through the table until it finds the method (or not finding it, raising an exception). It will use first found method (searching from bottom). It means that methods with the same name which are defined lower in the hierarchy will gain precedence and will be always used (higher positioned methods are overridden). Same-name methods higher in our table will be not used unless specifically called by explicit caller (for class methods) or by using supermethod (for instance methods)

Variables are shared for all rows of “method table”. When a method re-assigns a variable, this variable simply gets new value which will be available for all other methods in our “method table”.

Self and scope = object we are in

In ruby, because everything is an object, we are always “in” some object from which we call other objects (and their methods) or inside which we call object’s own methods.

This object we currently reside in is referenced by selfand whenever we change this object, it is also reflected in self.

self defines our scope = which variables and which methods are available to us and in which context we currently are.

self can change only in two situations: 
1. When calling a method. We invoke it by object_x.method_ycall and self is changed to this object_x (we enter this object and its method_y is invoked). When we use implicit caller (see bellow), self is not changing and we stay in the same scope (=method is in the self object)
 
2. When entering class/module definition. When executed code enters class or module definition (defined by class Xor module Y), self will change to this class or module. That is why using self inside class definition will be synonymous with using ClassNameitself and why we use def self.method_name for defining class methods. We could also use def ClassName.method_name See also Creating Class.

Room metaphore

We can imagine self as a room, we are currently in. There are different things (variables) in each room. Each room has also different functions (methods). I can even take with me some objects when I switch rooms (by passing arguments to methods).

Generally, when we are in particular room, there are only things inside this room (or things we brought to this room with us) and function of this room available for us. When we enter another room, out context (what is available for us) will change. In ruby, we typically either switch rooms (by calling methods on other objects) or return to the previous room (by returning from method to previous object). In all these cases selfreflects this change.

When self is changed (=when I enter other room), I am in other object and have another “method table” and another variables available. Previous “method table”/variables are not available now.

I can use some functions of other rooms even if I am not in them (I can set thermostat in my garage remotely or I can call to my wive into kitchen to bring me something from it). However I am limited only to those functions (in ruby: methods) which particular room enables as public (=which are available from other rooms).

Caller = objects we call (and which bears methods)

Because everything in ruby is an object, all methods in ruby are defined in objects (classes, modules).

These objects on/in which methods are defined, are called receivers or callers (these two terms are used based on context we want to emphasize — whether we are passing some information into an object (receiver) or that method is available in it (caller). We will use term caller here. We call method on objects by using caller.method_name syntax. When we use this syntax, we say that we are using explicit caller (because we specify it).

We do not have always to specify caller. Sometimes we can use just method_namefor calling the method. But even in this case caller will be set. And this caller will be set to the object referenced by self (see above). We say that we are using implicit caller. Or — in other words — we rely on self as a caller.

There are two special cases when we do not have to use explicit caller:

  • when we are in mainobject (object which ruby provides for us and which we enter with starting code execution) and want to call some class methods or module methods available for us in Kerneletc. ( puts, print…)
  • when we are inside some instance (or class) and we want to call just another inner method, not outer method = We want to call method available in this particular “composite object” = from the same “method table”.

It can be:

  • instance methods of the same instance: we call them by using method_name(we can use sel.method_namebut we should do it only for calling setters which are defined with `=`)
  • class methods/constants of the same class: we call them by using method_name , CONSTANT (although we can use self.method_nameor ExplicitClassName.method_name)

Because instances and classes are not the same objects, when we want to call class method from inside instance of that class, we have to use explicit caller: Class.method_name . In case we would use just method_name it would be considered by ruby as call to instance method (searched in instance “method table”), because self would be implicitly added for a caller and self is always current object. Explicitly calling self.method_name would bring the same result.

When calling methods on other objects, we have to use explicit caller. We can call:

  • methods of other objects which are public (see Room metaphor and Visibility)
  • constants (using ClassName::CONSTANTsyntax)

Creating class

Class is created when code execution crosses the class definition (if we call the class before it, ruby raises an error). During this class creation, all the code inside class is executed in normal execution flow (=methods are not executed, but their names are registered and they become available) and all superclasses and their modules are joined (and their methods extracted into “method table” of our “composite class”). Two above mentioned areas of classes are set: ”form for instances” and ”common class area”.

To differentiate between methods, we use def self.method_namefor defining class methods (belonging to “common class area”) and def method_namefor defining instance methods (“form for instances”)

Creating module

Module is created in similar way as class: when code execution crosses the module definition. All the code is normally executed and method names are registered.

There are some exceptions regarding “common class area” when we include module into class:

  • During module creation self points to module itself (not the class into which is module included). That is why we cannot define class methods from module (by def self.method_name ) because we are not referencing class by self
  • We cannot reference (call) class variables directly, however we can initialize them from Module — it is paradoxical and it is better to avoid this situation (and use getter/setter methods when communicating directly between Module and class variables)
  • We can use constants defined in classes (not only from class into which module is included), but only with full explicit name (with caller). This is due to specific way constant names are looked for during runtime (lexical scope)

How instance methods defined in modules work:

  • They are defined without explicit caller and are not callable until module is included into class (and instance is created)
  • We can reference constants and class variables from instance methods in the same way as from “common class area” (see paragraph above)
  • We can normally use (reference, instantiate) instance variables, because instance methods defined in modules become parts of “method table” after instatiating object and including mixin.

We can register module methods using def self.methodand use them together for better organization. Module serves as namespace ( Math.cos)

Instance creation

By creating an instance, we create independent copy of the object, with independent variables (separated from other instances of the class) and with independent methods, which are distilled from “form for instances” of defining class (and its superclasses — see Inheritance “method table”)

Each instance has its own room with:

  • instance variables: `@A`shared for all inner methods and available to them
  • instance methods, which are available in “method table”

Variables/constants

Variables are not initialized (created) before the moment of first assignment. Often, we have to call particular method in which assignment is made, before variable is assigned, it is not set automatically with object creation.

If (class, instance) variables are not defined (initialized), they return `nil` (contrary to undefined local variables and constants which return error)

Availability of variables:

  • From inside class (or instance), all its variables can be used or re-assigned by using `@instance_var`, `@@class_var` syntax.
  • Instance variables are never available outside instance (we have to use getter/setter methods to reach them)
  • Class variables are available inside class and to all instances of the class (including all classes and instances of these classes down or up the inheritance chain). That broad availability also bears a problem — that re-assigning class variable will also reassign its value available in seemingly unrelated class (for example, changing class variable in SubClass Awill also change it for use in SubClass B, although these two classes are siblings and are not directly inheriting from each other.
  • Constants are available to other objects. They can be called by using ClassName::CONSTANTsyntax.

Visibility: Public, private, protected

Methods are public by default (callable from other objects)

Private and protected methods can be only called from within the class (or instance)
- we cannot use explicit caller (even self) for private methods
- we can use explicit caller (or self) for protected methods
- we can call (from inside of instance) protected methods of other objects, but they have to belong to the same class.