Ejemplo de POO
Para este primer ejemplo, vamos a reescribir un juego escrito de forma procedimental a programación orientada por objetos. El juego se trata de adivinar el número secreto, veamos la implementación procedimental:
num_attempts = 0
number = rand(1..10)
found = false
until found
print "Adivina el número de 1 a 10 que estoy pensando: "
guess = gets.chomp.to_i
if guess == number
puts "Muy bien! Lo lograste en #{num_attemps} intentos!"
found = true
else
puts "Lo siento! No es el número, intenta nuevamente."
num_attempts += 1
end
end
Este código empieza inicializando las variables que vamos a utilizar: num_attempts
y number
. También creamos una variable found
que nos va a indicar si ya se encontró la respuesta.
Después iteramos mientras que found
sea false
. En cada iteración le pedimos al usuario que adivine el número y si es correcto termina el juego (cambiamos found
a true
para que termine la iteración). De lo contrario, incrementamos el número de intentos.
En este momento nuestro juego está muy acoplado a la línea de comandos y sería muy difícil adaptarlo a otros ambientes como una aplicación de escritorio o Web. Lo que vamos a hacer con programación orientada a objetos es separar el juego de la interfaz de usuario.
Primero vamos a crear una clase Game
que se encargue de la lógica del juego. Crea un nuevo archivo llamado game.rb
y escribe lo siguiente:
class Game
attr_reader :num_attempts, :found
def initialize
@number = rand(1..10)
@num_attempts = 0
@found = false
end
def attempt(guess)
if guess == number
@found = true
else
@num_attempts += 1
end
end
end
Nuestra clase Game
va a tener 3 métodos públicos: attempt
, num_attempts
y found
(los últimos dos los crea el attr_reader
). El constructor se va a encargar de inicializar los atributos, incluyendo el número que se va a adivinar.
Ahora, si quisiéramos utilizar nuestro juego en otro ambiente lo podríamos hacer fácilmente, sería cuestión de copiar esta clase. Ahora veamos cómo lo utilizaríamos desde una interfaz de línea de comando (como lo hicimos anteriormente). Reemplaza el contenido de guess_number.rb
con el siguiente código:
require "./game"
game = Game.new
until game.found
print "Adivina el número de 1 a 10 que estoy pensando: "
guess = gets.chomp.to_i
game.attempt(guess)
if game.found
puts "Muy bien! Lo lograste en #{game.num_attemps} intentos!"
else
puts "Lo siento! No es el número, intenta nuevamente."
end
end
Veamos qué hace este código. El primer paso es importar el archivo con la clase Game
. Luego creamos una nueva instancia de Game
e iteramos mientras que el número no haya sido encontrado. En cada iteración utilizamos nuestra case Game
para realizar los intentos y verificar si ya fue encontrado.
Código más fácil de probar
Supongamos que queremos crear pruebas automatizadas para nuestra clase Game
. Lo que queremos probar es que:
Al crear un nuevo juego el número de intentos es 0 y no se ha encontrado el número.
El número de intentos (
num_attempts
) se incremente correctamente.found
se actualiza atrue
cuando sea adivina el número.
Crea un archivo game_test.rb
y escribe lo siguiente:
require "minitest/autorun"
require "./game"
class GameTest < Minitest::Test
def setup
@game = Game.new
end
def test_constructor_initializes_num_attempts_and_found
assert_equal 0, @game.num_attempts
assert_not @game.found
end
def test_increments_num_attemps_on_failed_attempts
end
def test_found_is_updated_when_number_is_guessed
end
end
Tenemos un problema para implementar las dos últimas pruebas. Actualmente el número se genera de forma aleatoria. ¿Cómo podemos saber cuál es el número?
Vamos a hacer uso de la programación orientada a objetos para solucionar este problema. Primero, vamos a crear una clase RandomNumberGenerator
con un único método generate
que retorne un número aleatorio. Crea una nueva clase random_number_generator.rb
y escribe lo siguiente:
class RandomNumberGenerator
def generate
rand(1..10)
end
end
Ahora, vamos a cambiar Game
para que reciba un argumento en el constructor, que por defecto va a ser una nueva instancia de RandomNumberGenerator
:
require './random_number_generator.rb'
class Game
attr_reader :num_attempts, :found
# recibe un objeto que responda a generate, por defecto va a ser RandomNumberGenerator
def initialize(number_generator = RandomNumberGenerator.new)
@number = number_generator.generate
@num_attempts = 0
@found = false
end
...
end
¿Cuál es la ventaja de este cambio? Que para las pruebas podemos crear un nueva clase que nos devuelva un número fijo para poder probar fácilmente. Al constructor de Game
no le importa qué objeto le llegue como argumento siempre y cuando responda al método generate
. A esto se le conoce en el mundo de la programación como duck typing.
Crea un nuevo archivo llamado fixed_number_generator.rb
con el siguiente contenido:
class FixedNumberGenerator
def initialize(number)
@number = number
end
def generate
@number
end
end
Esta clase recibe un argumento en el constructor, el número que se va a generar. El método generate
siempre retorna el número que le pasamos en el constructor. De esta forma vamos a saber cuál es el número que se debe adivinar.
Ahora podemos utilizar nuestro FixedNumberGenerator
para implementar nuestras pruebas:
require "minitest/autorun"
require "./fixed_number_generator"
require "./game"
class GameTest < Minitest::Test
def setup
# el número a adivinar siempre va a ser 5
generator = FixedNumberGenerator.new(5)
@game = Game.new(generator)
end
def test_constructor_initializes_num_attempts_and_found
assert_equal 0, @game.num_attempts
assert_not @game.found
end
def test_increments_num_attemps_on_failed_attempt
@game.attempt(4)
assert_not @game.found
assert_equal 1, @game.attempts
end
def test_found_is_updated_when_number_is_guessed
@game.attemp(5)
assert @game.found
end
end
Hemos utilizado la programación orientada a objetos para dos cosas:
Separar la lógica del juego de la forma en que se le muestra al usuario.
Hacer nuestro código más fácil de probar.
Esto, en el mundo de la programción, es muy valioso!
Last updated