Here’s a post about getting down and dirty with an example of modeling something in both C and Ruby.

In C, Object Orientation can be used (somewhat clumsily) as a pattern for modeling and implementing your problem domain. The power of real object oriented languages like Ruby is that this pattern is integrated seamlessly into the language itself.

I’m not planning on detailing Ruby syntax itself, but hope that you’ll pick it up in the course of the discussion.

For a complete overview of OO concepts, refer to Object-oriented programming at Wikipedia.

This is the first of a series of articles covering Ruby OO techniques for C developers:
  1. Basic Modeling in OO
  2. Inheritance
  3. Encapsulation
  4. Polymorphism

Anyway. Let’s model!

Baby steps

We’re going to model TV channels.

Our TV channels are viewed using a sophisticated set top box device. From a technical perspective, the box needs a few pieces of information about channels in order to tune to them and show them to the viewer.

  • the name of the channel.
  • the channel number.
  • some data to pass to the underlying hardware to tune to the channel.

Viewers can tune to a channel by choosing it from a graphical menu.

C

structure

In C-land, we give form to blobs of memory using structs. A struct defines the shape of a Channel when its allocated in memory.

typedef struct {
  char *name;
  int number
  int tuning_info[3];
} Channel;

allocation

To make a real channel from the struct, we allocate it some memory and initialise its information.

Channel *channel_v = (Channel *)malloc(sizeof(Channel));

channel_v->name = "Channel V";
channel_v->number = 801;
channel_v->tuning_info[0] = 4096;
channel_v->tuning_info[1] = 0;
channel_v->tuning_info[2] = 123;

channel_v is now pointing to an instance of the Channel struct.

Note that channel_v is only known to be shaped like a Channel struct at compile time. Once the code is running, the program has no knowledge of this shape.

Using the information stored in an instance is easy:

printf("Channel: %s\n", channel_v->name);
// output: Channel: Channel V

verbs

Now we have our data sorted out, let’s define some functions that do something with instances of the Channel struct.

void channel_tune(Channel *channel) {
  viewer_notify("Tuning to channel %s", channel->name);
  box_tune_to(channel->tuning_info[0], channel->tuning_info[1], channel->tuning_info[2]);
}

To perform the action “Tune to channel V” :

channel_tune(channel_v);

struct verbs

Here’s a function for reading a database file and creating a list of Channel instances:

Channel **channel_load_from_file(char *filename) {
  ...
}

This function doesn’t take a Channel instance to work on. Instead, it returns a list of Channel instances. You could view this function as working with knowledge of the Channel struct in order to produce its Channel instances.

Now lets build the same thing in Ruby…

Ruby

structure

In Ruby-land, classes define the shape of data. In object oriented circles, the shape is called the interface, because the data itself is not directly accessible (later on, we’ll learn that this concept is called encapsulation).

class Channel
  def name
    @name
  end

  def name=(name)
    @name = name
  end

  def number
    @number
  end

  def number=(num)
    @number = num
  end

  def tuning_info
    @tuning_info
  end

  def tuning_info=(tuning_info)
    @tuning_info = tuning_info
  end
end

Yuk, that’s a heap of code to do not much at all. It does expose a pile of Ruby concepts to dig through.

First of all, functions associated with a class are known as methods. They are like functions which automatically work on instances of the class, or the class itself.

The methods defined above are called instance methods, as they only work on instances of Channel.

These methods are called accessors—getters and setters—because all they do is get and set the data inside an instance.

You may have heard that one of Ruby’s principles is “don’t repeat yourself” (or DRY). Luckily, we can rewrite all this repetitive code much more succinctly:

class Channel
  attr_accessor :name, :number, :tuning_info
end

allocation

Channel objects are allocated (or instantiated) using the new method of the Channel class.

channel_v = Channel.new

channel_v.name        = "Channel V" 
channel_v.number      = 801
channel_v.tuning_info = [4096,0,123]

At this point the channel_v variable holds a Channel object. It retains complete knowledge of its structure and place in the hierarchy:

channel_v.class # => Channel
channel_v.instance_variables # => ["@number", "@name", "@tuning_info"]

This rich reflection information is very useful in many advanced techniques, collectively known as metaprogramming.

verbs

Guess what we use to get things done with Objects and Classes? Yes, right, methods.

class Channel
  attr_accessor :name, :number, :tuning_info

  def tune
    Viewer.notify("Tuning to channel #{@name}")
    Box.tune_to(self.tuning_info)
  end
end

To tune to Channel V, we call the tune method on the channel_v instance:

channel_v.tune

an aside on variables

Since we’ve seen a little bit of Ruby code by now, you might have noticed some odd things about the variables. Ruby has a few different kinds of variables and uses a couple of sigils to differentiate their type.

local variables

Local variables are only visible within the current scope, and any subscopes. They are written as variable (i.e. lower case with underscores)

Note that while and for loops don’t introduce new scope in Ruby! Blocks do.

instance variables

Instance variables are variables associated with an instance of a class. They’re visible anywhere within the instance, but not from the outside. They’re somewhat analogous to struct members, except for the encapsulation aspect (don’t worry, we’ll talk about it…).

They are written @variable (i.e. like a local variable preceded by an ‘at’).

others

Constants are visible within the current module scope and are written as Constant or CONSTANT

Globals are visible anywhere within the current ruby interpreter, and are written as $global

self

Did you notice the use of the word self in the example above? self is a special built in variable which refers to the instance for which the method is defined (In this case it wasn’t strictly required, however sometimes its necessary for disambiguation or to take advantage of encapsulation).

class verbs

Methods which act on the class rather than instances of the class are called class methods (or singleton methods). In the method Channel#tune above, Viewer.notify and Box.tune_to are singleton methods.

Let’s define a class method for Channel which reads records from a text file and returns a list of Channel objects.

class Channel
  def self.load_from_file(file)
    ...
  end
end

The class method is called directly on the class:

channels = Channel.load_from_file("channels.txt")

Bye till next time

Up to this point there’s not much difference between the structure of the C and Ruby versions.

In the next post, I’ll take OO modeling to the next logical step of inheritance. The true utility of OO will become much clearer.

I hope you’ve found the journey interesting so far—feedback is always welcome.

1 Response to “Ruby OO for C developers - part 1 Basic Modeling in OO”

  1. Ben Askins Says:
    Nice intro Lachie, looking forward to you imparting more of your knowledge.

Leave a Reply