Programando en Ruby

La Guía de los "Programadores Pragmáticos"

Classes, Objects, and Variables



Después de los ejemplos que hemos mostrado hasta ahora, podrías sorprenderte sobre nuestra pronta afirmación sobre que Ruby es un lenguaje orientado a objetos. Bien, en este capítulo es donde justificamos esta aseveración. VAmos a ver como crear clases y objetos en Ruby, y algunos de los modos en los que Ruby es más potente que la mayoría de los lenguajes orientados a objetos. A lo largo del camino, implementaremos parte de nuestro siguiente producto millonario, una jukebox de Jazz y Blue Grass conectada a Internet.

Después de meses de trabajo, nuestros bien pagados amigos de Investigación y Desarrollo han determinado que nuestra jukebox necesita canciones. Así que parece una buena idea comenzar creando una clase Ruby que represente cosas tales como canciones. SAbemos que una canción real tiene un nombre, un artista, una duración, así que querremos estar seguros que el objeto canción de nuestro programa también.

Comenzaremos creando una clase básica Song,[Como hemos mencionado en la página 9, nos nombres de clases comienzan con una letra mayúscula, mientras los nombres de métodos comienzan con letra minúscula.] la cual contiene un único método, initialize,

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end

initialize es un método especial en los programas Ruby. Cuando llamas Song.new para crear un nuevo objeto Song, Ruby crea un objeto no inicializado y entonces llama al método del objeto initialize, pasándole los parámetros pasados a new. Esto te da la oportunidad de escribir código que establezca el estado del objeto.

Para la clase Song, el método initialize toma tres parámetros. Estos parámetros actuán como variables locales dentro del método, así que ellos siguen la convención de nombres de variables locales, comenzando con letra minúscula.

Cada objeto representa su propia canción, así que necesitamos que cada uno de nuestros objetos Song tenga su propio nombre de canción, artista y duración. Esto significa que necesitamos almacenar estos valores como variables de instancia dentro del objeto. En Ruby, una variable de instancia es simplemente un nombre precedido de un símbolo arroba (``@''). Es nuestro ejemplo, el parámetro name es asignato a la variable de instancia @name, artist es asignado a @artist, y duration (la longitud de la canción en segundos) es asignado a@duration.

Vamos a probar nuestra maravillosa nueva clase.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.inspect » "#<Song:0x401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">"

Vale, parece que funciona. Por defecto, el mensaje inspect, que puede ser enviado a cualquier objeto, vuelca el identificador del objeto y las variables de instancia. Parece que lo hemos hecho correctamente todo.

Nuestra experiencia nos dice que durante el desarrollo imprimiremos el contenido del objeto Song muchas veces, y formato por defecto de inspect deja bastante que desear. Afortunadamente, Ruby tiene un mensaje estándar, to_s, el cual se puede enviar a cualquier objeto que queremos mostrar como una cadena. Vamos a intentarlo con nuestra canción.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "#<Song:0x401b499c>"

Esto no fue demasiado útil --- sólo informa del id del objeto. Así que vamos a sobrescribir to_s en nuestra clase. Al hacer esto, deberemos tomarnos un momento para hablar sobre cómo estamos mostrando las definiciones de clases en este libro.

En Ruby, las clases nunca están cerradas: siempre puedes añadir métodos a una clase existente. Esto se aplica tanto a las clases que tu mismo escribes como a las estándar incorporadas. Todo lo que tienes que hacer es abrir una definición de clase para la clase existente, y el nuevo contenido que especifiques se añadirá donde corresponda.

Esto es increíble para nuestros propósitos. A medida que avanzamos por el capítulo, añadiendo características a nuestras clases, mostraremos las definiciones de clase para los nuevos métodos; los viejos seguirán ahí. Eso nos ahorrará tener que repetir cosas redundantes en cada ejemplo. Obviamente, sin embargo, si estuvieses creando este código desde cero, probablemente tendrías que poner todos los métodos en una única definición de clase.

Suficiente detalle. Vamos a regresar para añadir un meétodo to_s a nuestra clase Song.

class Song
  def to_s
    "Song: #{@name}--#{@artist} (#{@duration})"
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "Song: Bicylops--Fleck (260)"

Excelente, estamos haciendo progresos. Sin embargo, hemos patinado en algo sutil. Hemos dicho que Ruby soporta to_s para todos los objetos, pero no hemos dicho como. La respuesta tiene que ser mediante la herencia, subclases y cómo Ruby determina que método ejecutar cuando envías un mensaje a un objeto. Este es un tema para una nueva sección, así que...

Herencia y Mensajes

La herencia te permite crear una clase que es un refinamiento o especialización de otra clase. Por ejemplo, nuestra jukebox tiene el concepto de canciones, el cual hemos encapsulado en la clase Song. Entonces marketing nos dice que necesitamos proporcionar soporte para el karaoke. Una canción de karaoke es igual que cualquier otra (no hay voces, pero eso no nos importa). Sin embargo, también tiene asociada un conjunto de letras, así como información de cronometraje. Cuando nuestra jukebox interpreta una canción de karaoke, las letras deberían fluir por la pantalla en frente de la jukebox al mismo tiempo que la música.

Una aproximación a este problema consiste en definir una nueva clase, KaraokeSong, que es igual que Song, pero con las letras.

class KaraokeSong < Song
  def initialize(name, artist, duration, lyrics)
    super(name, artist, duration)
    @lyrics = lyrics
  end
end

El ``< Song'' en la línea de definición de clase le dice a Ruby que una KaraokeSong es una subclase de la clase Song. (No es sorprendente, esto significa que Song es una superclase de KaraokeSong. La gente también habla sobre relaciones padre-hijo, así que el padre KaraokeSong podría ser Song.) Por ahora, no nos preocuparemos demasiado sobre el método initialize; hablaremos sobre la llamada a super más tarde.

Vamos a crear KaraokeSong y ver si nuestro código funciona. (En el sistema final, las letras estarán contenidas en un objeto que incluye el texto y la información de sincronización. Para testear nuestra clase, sin embargo, usaremos una cadena. Este es otro beneficio de un lenguaje no tipado --no tenemos que definir todo antes de comenzar a ejecutar código.

aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225)"

Bien, funcionó, pero ¿por qué el método to_s no mostró las letras?

La respuesta tiene que ver con el modo en que Ruby determina qué método debería ser llamado cuando enviamos un mensaje a un objeto. Cuando Ruby compila la invocación del método aSong.to_s, realmente no sabe donde encontrar el método to_s. En lugar de eso, aplaza la decisión hasta que el programa está ejecutándose. Por aquel entonces, mira en la clase de aSong. Si la clase implementa un método con el mismo nombre que el mensaje, ese método es ejecutado. En otro caso, Ruby busca en la clase padre, y entonces en la clase abuelo, y así por la cadena de antecesores. Si recorre la cadena de antecesores sin encontrar el método apropiado, toma una acción especial que normalmente termina en la generación de un error. [De hecho, puedes interceptar este error, lo cual permite falsificar métodos en tiempo de ejecución. Esto es descrito bajo Object#method_missing en la página 355.]

Así que, regresando a nuestro ejemplo... Nosotros enviamos el mensaje to_s a aSong, un objeto de la clase KaraokeSong. Ruby buscan en KaraokeSong un método llamado to_s, pero no lo encuentra El intérprete buscan al padre de la clase KaraokeSong, la clase Song, y ahí encuentra el método to_s method que hemos definido en la página 18. Eso es por lo que imprime los detalles de la canción pero no las letras --la clase Song no sabe nada sobre letras de canción.

Vamos a arreglar esto implementando KaraokeSong#to_s. Existen varias maneras de hacer eso. Vamos a comenzar por el modo incorrecto. Copiaremos el métodoto_s de la clase Song y añadiremos la letra de la canción.

class KaraokeSong
  # ...
  def to_s
    "KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "KS: My Way--Sinatra (225) [And now, the...]"

Estamos mostrando el valor de la variable de instancia @lyrics. Para hacer esto, la subclase accede directamente a las variables de instancia de sus antecesores. Asi que, ¿por qué es un modo incorrecto de implementar el método to_s?

La respuesta tiene que ver con un buen estilo de programación (y algo llamado desacople). Escarbando demasiado en el estado interno de nuestro padre nos estamos atando demasiado estrechamente a su implementación. Imagina que decidimos cambiar Song para almacenar la duración en milisegundos. De pronto, KaraokeSong comenzaría a informar de valores ridículos. La idea de una versión karaoke de ``My Way'' de 3750 minutos es demasiado alarmante para considerarlo.

Nosotros esquivamos este problema haciendo que cada clase gestione sus propios estados internos. Cuando KaraokeSong#to_s es llamado, llamaremos al método to_s de sus padres para conseguir los detalles de la canción. Entonces añadirá a esto la información de la letra de la canción y devolverá el resultado. El truco aquí está en la palabra clave en Ruby ``super''. Cuando invocamos super sin argumentos, Ruby envía un mensaje al padre del objeto, preguntándole para invocar un método del mismo nombre que el método actual, y pasándole los parámetros que fueron pasados al método actual. Ahora podemos implementar nuestro nuevo y mejorado método to_s.

class KaraokeSong < Song
  # Format ourselves as a string by appending
  # our lyrics to our parent's #to_s value.
  def to_s
    super + " [#{@lyrics}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225) [And now, the...]"

Nosotros le dijimos explícitamente a Ruby que KaraokeSong era una subclase de Song, pero no especificamos una clase padre para la clase Song. Si no se especifica un padre cuando definimos una clase, Ruby toma la clase Object por defecto. Esto significa que todos los objetos tienen a la clase Object como antecesor, y que los metodos de instancia de Object están disponibles para cada objeto en Ruby. Volviendo a la página 18 demos que to_s está disponible para todos los objetos. Ahora sabemos por qué; to_s es uno de los más de 35 métodos de instancia en la clase Object. La lista completa comienza en la página 351.

Herencia y Mixins

Algunos lenguajes orientados a objetos (de manera notable C++), proporcionan herencia múltiple, por la cual una clase puede tener más de un padre directo, heredando funcionalidades de cada uno. Aunque es potente, esta técnica puede ser peligrosa, ya que la jerarquía de herencias puede volverse ambigua.

Otros lenguajes, como Java, soportan herencia simple. Una clase sólo puede tener un padre inmediato. Aunque es limpio (y fácil de implementar), la herencia simple también tiene inconvenientes --en el mundo real las cosas suelen heredar atributos de múltiples orígenes (una pelota es a la vez algo que rebota y algo esférico, por ejemplo).

Ruby ofrece un potente e interesante compromiso, dándote la simplicidad de la herencia simple y la potencia de la herencia múltiple. Una clase Ruby puede tener un único padre directo, y por ello Ruby es un lenguaje con herencia simple. Sin embargo, las clases Ruby pueden incluir funcionalidad de un número de mixins (un mixin es como una definición parcial de clase). Este proporciona una capacidad multiherencia pero controlada sin ninguno de los inconvenientes. Exploraremos en más profundidad los mixins a partir de la página 98.

Así que en este capítulo hemos visto las clases y sus métodos. Ahora es tiempo de seguir adelante con los objetos, como las instancias de la clase Song.

Objetos y Atributos

Los objetos Song que hemos creado hasta ahora tienen un estado interno (como el título de la canción y el artista). Ese estado es privado para esos objetos --otros objetos no pueden acceder a las variables de instancias del objeto. En general, esto es bueno. Significa que el objeto es único responsable de mantener su propia consistencia.

Sin embargo, un objeto que es totalmente reservado, no es muy útil ---lo puedes crear, pero ya no puedes hacer nada más con el. Normalmente definirás métodos que permitan acceder y manipular el estado de un objeto, permitiendo al mundo exterior interactuar con el objeto. Estas facetas visibles desde fuera de un objeto son llamadas atributos.

Para nuestros objetos Song, la primera cosa que necesitaríamos es la posibilidad de encontrar el título y artista (par que podamos mostrarlo mientras la canción suena) y la duración (para que podamos mostrar algún tipo de barra de progreso).

class Song
  def name
    @name
  end
  def artist
    @artist
  end
  def duration
    @duration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

Aquí hemos definido tres métodos de acceso para devolver los valores de los tres atributos de instancia. Como esto es algo muy común, Ruby proporciona un conveniente atajo: attr_reader crea los métodos de acceso por tí.

class Song
  attr_reader :name, :artist, :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

Este ejemplo ha introducido algo nuevo. El constructor :artist es una expresión que devuelve un objeto Symbol correspondiendo a artist. Puedes pensar en :artist como el nombre de la variable artist, mientras que artist es el valor of the variable. En este ejemplo, hemos llamado a los metodos de acceso name, artist, y duration. Las variables de instancia correspondientes, @name, @artist, y @duration, serán creadas automáticamente. Estos métodos de acceso son idénticos a los que escribimos a mano antes.

Writable Attributes

A veces necesitas se capaz de dar valor a un atributo desde fuera del objeto. Por ejemplo, vamos a asumir que la duración que está inicialmente asociada con una cación es una estimación (quizá recogida de un CD o de los datos de un MP3). La primera vez que reproducimos la cación, descubrimos la duración realm, y almacenamos ese nuevo valor en el objeto Song.

En los lenguajes como C++ o Java, tu harías esto con funciones setter.

class JavaSong {                     // Java code
  private Duration myDuration;
  public void setDuration(Duration newDuration) {
    myDuration = newDuration;
  }
}
s = new Song(....)
s.setDuration(length)

En Ruby, los atributos de un objeto pueden ser accedidos como si ellos fuesen otra variable. Hemos visto esto anteriormente con frases como aSong.name. Así que, parece natural ser capaz de asignar a estas variables cuando quieres asignar un valor a un atributo. it seems natural to be able to assign to these variables when you want to set the value of an attribute. De acuerdo con el Principio de la Menor Sorpresa, podemos hacerlo en Ruby.

class Song
  def duration=(newDuration)
    @duration = newDuration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration » 260
aSong.duration = 257   # set attribute with updated value
aSong.duration » 257

La asignación ``aSong.duration = 257'' invoca al método duration= en el objeto aSong, pasándole 257 como un argumento. De hecho, definiendo un nombre de método terminado en un símbolo igual hace que el nombre pueda aparecer en la parte izquierda de una asignación.

De nuevo, Ruby proporciona un atajo para crear estos sencillos métodos de asignación de atributos.

class Song
  attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257

Atributos Virtuales

Estos métodos de acceso a atributos no tiene que ser simples envolturas alrededor de las variables de instancia de un objeto. Por ejemplo tu podrías querer acceder a la duración en minutos y fracciones de minuto, mejor que en segundos como hemos estado haciendo.

class Song
  def durationInMinutes
    @duration/60.0   # force floating point
  end
  def durationInMinutes=(value)
    @duration = (value*60).to_i
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.durationInMinutes » 4.333333333
aSong.durationInMinutes = 4.2
aSong.duration » 252

Aquí hemos utilizado métodos de atributo para crear una variable de instancia virtual. Para el mundo exterior, durationInMinutes parece ser un atributo como otro cualquiera. Internamente, en cambio, no hay correspondencia con una variable de instancia.

Esto es más que una curiosidad. En su importante libro Object-Oriented Software Construction , Bertrand Meyer llama a esto Principio de Acceso Uniforme. Mediante la ocultación de la diferencia entre variables de instancia y valores calculados, estás guardando del resto del mundo la implementación de tu clase. Eres libre de cambiar como las cosas trabajan en el futuro sin impactar en los millones de líneas de código que utilizan tu clase. Esto es una gran victoria.

Variables de Clase y Métodos de Clase

Hasta aquí, todas las clases que hemos creado han contenido variables de instancia y métodos de instancia: variables que están asociadas con una instancia particular de una clase, y métodos que trabajan sobre esas variables. Algunas veces las clases mismas necesitan tener sus propios estados. Aquí es cuando entran las variables de clases.

Variables de Clase

Una variable de clase es compartida por todos los objetos de una clase, y es también accesible a los métodos de la clase como describiremos después. Hay solo una copia de una variable de clase concreta para una clase dada. El nombre de una variable de clase comienza con dos símbolos ``arroba'', como ``@@count''. Pero al revés que las variables globales y de instancia, las variables de clase deben ser inicializada antes de ser utilizadas. Frecuentemente esta inicialización es una simple asignación en el cuerpo de la definición de clase.

Por ejemplo, nuestra jukebox podría querer almacenar cuantas veces ha sonado cada canción en particular. Este contador, probablemente sería una variable de instancia de objetos de la clase Song. Cuando una canción es reproducida, el valor en la instancia es incrementado. Pero podríamos querer saber cuantas canciones han sido reproducidad en total. Podríamos hacer esto buscando en todos los objetos Song y sumando sus contadores, o podríamos arriesgarnos a una excomunicación de la Iglesia del Buen Diseño y utilizar una variable global. En cambio, vamos a utilizar una variable de clase.

class Song
  @@plays = 0
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
    @plays    = 0
  end
  def play
    @plays += 1
    @@plays += 1
    "This  song: #@plays plays. Total #@@plays plays."
  end
end

Con fines de depuración, hemos arreglado Song#play para devolver una cadena conteniendo el número de veces que la canción ha sonado, así como el número total de canciones que han sonado. Podemos probar esto fácilmente.

s1 = Song.new("Song1", "Artist1", 234)  # test songs..
s2 = Song.new("Song2", "Artist2", 345)
s1.play » "This  song: 1 plays. Total 1 plays."
s2.play » "This  song: 1 plays. Total 2 plays."
s1.play » "This  song: 2 plays. Total 3 plays."
s1.play » "This  song: 3 plays. Total 4 plays."

Las variable de clase son privadas a la clase y sus instancias. Si queremos hacerlas accesibles al mundo exterior, necesitarás escribir un método de acceso. Este método podría ser tanto un método de instancia, o aproximándonos a la siguiente sección, un método de clase.

Métodos de Clase

Algunas veces una clase necesita proporcionar métodos que trabajen sin estar ligados a un objeto en particular.

Ya nos hemos topado con uno de tales métodos. El método new crea un nuevo objeto Song pero que no está asociado con ninguna canción en particular.

aSong = Song.new(....)

Encontrarás métodos de clase repartidos a lo largo de las librerías de Ruby. Por ejemplo, objetos de la clase File representan ficheros abiertos en el sistema operativo que está por debajo. Sin embargo, la clase File también proporciona metodos de clase para manipular ficheros que no están abiertos y por consigui9ente no tienen un objeto File. Si quieres borrar un fichero, llamas al método de clase File.delete , pasándole el nombre.

File.delete("doomedFile")

Los métodos de clase se distinguen de métodos de instancia por su definición. Los métodos de clase son definidos poniendo el nombre de la clase y un punto antes del nombre del método.

class Example

  def instMeth              # instance method   end

  def Example.classMeth     # class method   end

end

Las jukebox cobran dinero por cada canción que reproducen, no por los minutos. Esto hace que las canciones cortas son más rentables que las largas. Queremos prevenir que haya canciones demasiado largas en SongList. Podemos definir un método de clase en SongList que chequee si hay canciones que exceden el límite. Estableceremos este límite utilizando una constante de clase, que es una simple constante (¿recuerdas las constantes? comienzan con una mayúscula) que es inicializada en el cuerpo de la clase.

class SongList
  MaxTime = 5*60           #  5 minutes
  def SongList.isTooLong(aSong)
    return aSong.duration > MaxTime
  end
end
song1 = Song.new("Bicylops", "Fleck", 260)
SongList.isTooLong(song1) » false
song2 = Song.new("The Calling", "Santana", 468)
SongList.isTooLong(song2) » true

Singletons and Other Constructors

Algunas veces querrás sobrescribir el modo por defecto en el que Ruby crea objetos. Como ejemplo, vamos a verlo en nuestra jukebox. Como querremos muchas jukebox, repartidas a lo largo de todo el país, queremos hacer el mantenimiento tan sencillo como sea posible. Parte de los requerimientos es guardar un registro de todo lo que pasa en la jukebox: las canciones que son reproducidas, el dinero recogido, los extraños fluidos vertidos dentro, y demás. Co0mo queremos reservar el ancho de banda para la música, almacenaremos esos ficheros de eventos localmente. Esto significa que necesitaremos una clase que maneje los registros de eventos. Sin embargo, queremos sólo un objeto de registro de eventos por jukebox, y queremos que el objeto sea compartido por todos los otros objetos.

Utilizamos el patrón Singleton, documentado en Design Patterns . Arreglaremos las cosas para que el único modo de crear un objeto de registro de eventos sea llamando a Logger.create, y nos aseguraremos que solo un objeto de registro de eventos sea creado.

class Logger
  private_class_method :new
  @@logger = nil
  def Logger.create
    @@logger = new unless @@logger
    @@logger
  end
end

Haciendo el método new de la clase Logger privado, prevenimos que alguien cree un objeto de registro de eventos utilizando el constructor convencional. En cambio, proporcionamos un método de clase, Logger.create. Este utiliza la variable de clase @@logger para conservar la referencia a una única instancia de logger, devolviendo esa instancia cada vez que es llamada. [La implementación de singletons que presentamos aquí no es thread-safe; si múltiples hilos están ejecutándose, podría ser posible crear multiples objetos logger. Antes que añadir seguridad de hilos nosotros mismos, probablemente utilicemos el mixin proporcionado por Ruby Singleton, que está documentado en la página 468.] Podemos comprobar esto observando los identificadores de objeto que el método devuelve.

Logger.create.id » 537766930
Logger.create.id » 537766930

Utilizando métodos de clase como pseudo-constructores también podemos hacer la vida más fácil a los usuarios de nuestras clases. Como ejemplo trivial, vamos a echar un vistazo a la clase Shape que representa un polígono regular. Instancias de Shape son creadas cando al constructor el número requerido de caras y el perímetro total.

class Shape
  def initialize(numSides, perimeter)
    # ...
  end
end

Sin embargo, un par de años más tarde, esta clase es utilizada en una aplicación diferente, donde los programadores están creando figuras por nombre, y especificando la longitud del lado, no el perímetro. Simplemente añadimos algunos métodos de clase a Shape.

class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end

Hay muchos interesantes y potentes usos de los métodos de clase, pero explorándolos no tendremos nuestra jukebox terminada antes, así que vamos adelante.

Control de Acceso.

Cuando se diseña el interface de un clase, es importante considerar cuando acceso a tu clase expondrás al mundo exterior. Permite demasiado acceso a tu clase, y el se incrementará el riesgo de acople en tus aplicaciones ---usuarios de tu clase estarán tentados de depender de los detalles de implementación de tu clase, en vez de su interface lógico. Las buenas noticias están en que el único medio para cambiar el estado de un objeto en Ruby es llamando a uno de sus métodos. Controla el acceso a los métodos y estarás controlando el acceso al objeto. Una buena regla es nunca exponer métodos que pudiesen dejar a un objeto en un estado inválido. Ruby nos da tres niveles de protección.

La diferencia entre ``protected'' y ``private'' es bastante sutil, y es diferente en Ruby que en la mayoría de los lenguajes orientados a objetos. Si un método es protegido, puede ser llamado por cualquier instancia de la clase que se esta definiendo o sus subclases. Si un método es privado, podría ser llamado solo dentro del contexto del objeto llamante --nunca es posible acceder a métodos privados de otro objeto directamente, incluso si el objeto es de la misma clase que el llamado.

Ruby se diferencia de otros lenguajes orientados a objetos de una manera importante. El control de acceso es determinado dinámicamente, mientras el programa se está ejecutando, no estáticamente. Obtendrás una violación de acceso sólo cuando el código intenta ejecutar el método restringido.

Especificando Control de Acceso

Los niveles de acceso los especificas dentro de la definición de módulo o clase utilizando una o más de estas tres funciones public, protected, y private. Cada función puede ser utilizada en dos modos diferentes.

Si no se utilizan argumentos, las tres funciones establecen el control de acceso por defecto de los métodos siguientes. Este es el comportamiento familiar para los programadores de C++ y Java, donde utilizarías palabras claves como public para conseguir el mismo efecto.

class MyClass

      def method1    # default is 'public'         #...       end

  protected          # subsequent methods will be 'protected'

      def method2    # will be 'protected'         #...       end

  private            # subsequent methods will be 'private'

      def method3    # will be 'private'         #...       end

  public             # subsequent methods will be 'public'

      def method4    # and this will be 'public'         #...       end end

Alternativamente, puedes establecer los niveles de accesos a los métodos listándolos como argumentos de las funciones de control de acceso.

class MyClass

  def method1   end

  # ... and so on

  public    :method1, :method4   protected :method2   private   :method3 end

Un método initialize de una clase es automáticamente declarado privado.

Es el momento de algunos ejemplos. Quizá estamos modelando un sistema contable donde cada débito tiene un crédito correspondiente. Como queremos asegurar que nadie pueda romper esta regla, haremos que los métodos de crédito y débito sean privados, y definiremos nuestro interface externo en termino de transacciones.

class Accounts

  private

    def debit(account, amount)       account.balance -= amount     end     def credit(account, amount)       account.balance += amount     end

  public

    #...     def transferToSavings(amount)       debit(@checking, amount)       credit(@savings, amount)     end     #... end

El acceso protegido es utilizado cuando los objetos necesitan acceder al estado interno de otros objetos de la misma clase. Por ejemplo, podríamos querer permitir a objetos individuales Account comparar sus balances, pero podríamos querer ocultar esos balances del resto del mundo (quizas porque queremos presentarlos en diferente forma).

class Account
  attr_reader :balance       # accessor method 'balance'

  protected :balance         # and make it protected

  def greaterBalanceThan(other)     return @balance > other.balance   end end

Com0o el atributo balance es protegido, es disponible sólo para objetos Account.

Variables

Ahora que hemos creado todos esos objetos, vamos a estar seguros de que no los perdemos. Las variables son utilizadas para mantener la pista de los objetos. Cada variable almacena la referencia a un objeto.

Figure not available...

Vamos a comprobarlo con algo de código.

person = "Tim"
person.id » 537771100
person.type » String
person » "Tim"

En la primera línea, Ruby crea un nuevo objeto String con el valor ``Tim.'' Una referencia a este objeto es puesta en la variable local person. Un rápido chequeo muestra que la variable ha tomado, en efecto, la personalidad de una cadena, con un id de objeto y un valor.

Asi que, ¿es una variable un objeto?

En Ruby, la contestación es ``no.''. Una variable es simplemente una referencia a un objeto. Los objetos flotan alrededor de una gran piscina donde (el heap, la mayoría del tiempo) y son apuntadas por variables.

Vamos a hacer un ejemplo ligeramente más complicado.

person1 = "Tim"
person2 = person1
person1[0] = 'J'
person1 » "Jim"
person2 » "Jim"

¿Qué ha sucedido aquí? Hemos cambiado el primer caracter de person1, pero ambos person1 y person2 han cambiado de ``Tim'' a ``Jim.''

Esto nos lleva al hecho de que las variables recogen referencias a objetos, no son los objetos mismos. La asignación de person1 a person2 no crean ningun nuevo objeto; simplemente copia la referencia al objeto person1 en person2, así que ambos person1 y person2 apuntan al mismo objeto. Mostramos esto en la Imagen 3.1 en la pagina 31.

La asignación de alias a objetos, te da potencialmente multiples variables que hacen referencia al mismo objeto. Pero, ¿puede esto causar problemas en tu código?. Puede, pero no tanto como podrías pensar (los objetos en Java, por ejemplo, trabajan exactamente del mismo modo). Por ejemplo, en el ejemplo de la Imagen 3.1, podrías evitar aliasing mediante el uso del método dup de String, que crea un nuevo objeto de la clase String con idénticos contenidos.

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 » "Jim"
person2 » "Tim"

También puedes prevenir el cambio de algún objeto particular congelándolo (hablaremos más sobre congelar objetos en la página 251). Los intentos de alterar objetos congelados, provocarán que se genere una excepción TypeError.

person1 = "Tim"
person2 = person1
person1.freeze       # prevent modifications to the object
person2[0] = "J"
produces:
prog.rb:4:in `=': can't modify frozen string (TypeError)
	from prog.rb:4


Extraído del libro "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. liberado bajo los términos de la Open Publication License V1.0.
La referencia está disponible en: download.