|
|||
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\">"
|
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>"
|
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)"
|
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...
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 |
< 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)"
|
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...]"
|
@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...]"
|
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.
Song.
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
|
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
|
: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.
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)
|
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
|
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
|
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
|
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.
@@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 |
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."
|
new crea un nuevo objeto Song pero que no está
asociado con ninguna canción en particular.
aSong = Song.new(....) |
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")
|
class Example def instMeth # instance method end def Example.classMeth # class method end end |
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
|
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 |
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
|
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 |
Shape.
class Shape def Shape.triangle(sideLength) Shape.new(3, sideLength*3) end def Shape.square(sideLength) Shape.new(4, sideLength*4) end end |
initialize, que es privado siempre).
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 |
class MyClass def method1 end # ... and so on public :method1, :method4 protected :method2 private :method3 end |
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 |
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 |
balance es protegido, es disponible
sólo para objetos Account.
| Figure not available... |
person = "Tim"
|
||
person.id
|
» |
537771100
|
person.type
|
» |
String
|
person
|
» |
"Tim"
|
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"
|
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"
|
TypeError.
person1 = "Tim" person2 = person1 person1.freeze # prevent modifications to the object person2[0] = "J" |
prog.rb:4:in `=': can't modify frozen string (TypeError) from prog.rb:4 |