Entorno de desarrollo sencillo para Ruby

por Fran Iglesias

Hace un tiempo que estoy estudiando algo de Ruby y estoy tratando de encontrar o preparar un entorno de desarrollo sencillo para practicar.

Creo que ya lo habrás notado porque en algunas entradas anteriores he estado poniendo ejemplos en Ruby. La verdad es que en un lenguaje muy agradable con el que trabajar y tengo la impresión de que es especialmente interesante para mejorar en el área de orientación a objetos.

En ese sentido, tengo que recomendar muy insistentemente el libro de Sandi Metz, Practical Object-Oriented Design, no solo si tienes interés en Ruby, sino como un gran manual de programación orientada a objetos desde un modelo de tipado dinámico.

Cómo es Ruby

Ruby es un lenguaje orientado a objetos, diseñado para ser agradable y natural.

Creo que lo consigue bastante bien y, en cualquier caso, viniendo de trabajar habitualmente con PHP, tratando de mantener una cierta disciplina de tipado estricto y paradigmas de OOP estilo Java, es un cambio que se agradece.

Entre otras características, y comparando sobre todo con PHP:

En Ruby todo es un objeto, incluso los tipos primitivos. Así que puedes escribir cosas como estas.

3.times do
   "Hello, world!"
end

Todos los objetos del lenguaje son modificables. Puedes añadir métodos a la clase Numeric, si crees que tiene sentido para tu aplicación. Por ejemplo, añadimos el método divisible_by? para ver si un número se puede dividir por otro. El nombre terminado en ? es una convención para indicar que la respuesta es boolean.

class Numeric
  def divisible_by?(divisor)
    (self % divisor).zero?
  end
end

if 15.divisible_by? 3
  '15 divisible by 3'
end

Otra característica llamativa son los bloques, que podemos entender como funciones anónimas o closures. En el primer ejemplo de arriba tenemos un bloque.

3.times do
   "Hello, world!"
end

Un bloque puede esperar parámetros. En este ejemplo examinamos los divisores de 20 y los vamos añadiendo en un array:

    response = []
    20.times do |number|
      if number > 0 and 20.divisible_by? number
        response.push"20 is divisible by #{number}"
      end
    end
    response

Existen algunas construcciones realmente interesantes, por ejemplo, el if del ejemplo anterior podría escribirse así:

response.push "20 is divisible by #{number}" if number > 0 and 20.divisible_by? number

Es decir, “añade la línea al array si se cumple la condición”.

También existe la condicional unless, cuando quieres que algo se haga excepto si se cumple la condición. Por ejemplo, esta línea asigna un pasajero a un coche excepto si no se ha definido todavía la propiedad @car. Por cierto, @ indica que la variable car es una propiedad de la clase en la que se define.

@car.allocate passenger unless car.nil?

Esto es interesante porque es una forma bastante común al expresarse en lenguaje natural (haz esto si pasa eso otro, o añade esto a menos que pase tal cosa) a la vez que resulta conciso y evita introducir un nivel de indentación en el código.

Por cierto, las propiedades son privadas por defecto en Ruby y tienes que declarar métodos para acceder a ellas, cosa que puedes hacer de forma explícita o declarando attribute accesors. Aquí, por ejemplo, se declara una clase Group, con las propiedades @id y @people a las cuales queremos poder acceder mediante los métodos id y people, respectivamente.

class Group
  attr_reader :id, :people

  def initialize(id, people)
    @id = id
    @people = people
  end
end

Esta característica nos ayuda a forzar el information hiding propio de OOP, pues nos obliga a pensar a qué propiedades necesitaríamos exponer y de qué forma, promoviendo el uso de métodos semánticos, por un lado, y evitándonos escribir accesors explícitos.

Ruby usa herencia simple, al igual que PHP, pero también incorpora la idea de mixin a través de módulos, lo que permite añadir comportamiento a un objeto simplemente incluyendo el módulo deseado. En PHP tendríamos los traits, así como en otros lenguajes existe un concepto semejante.

Ah, y además Ruby también es un lenguaje multi-hilo.

Entorno dockerizado

Preparar un entorno dockerizado para trabajar con Ruby no debería ser particularmente difícil, aunque como ocurre con otros lenguajes, da la impresión de que no es un set up tan generalizado como en PHP. Por una parte, la gestión de la instalación del lenguaje, sus versiones y dependencias, es relativamente sencilla en comparación con PHP.

Aparte de eso, trabajar en Ruby suele estar muy ligado a su framework Ruby on Rails, así que la mayor parte de información que encuentras se refiere a éste. Tanto es así, que frecuentemente se identifica al lenguaje con el framework. Sin embargo, hay otras formas de desarrollar con Ruby y no estoy especialmente interesado en Rails por el momento.

Así, ante mi posible interés de escribir algún tipo de aplicación web prefiero un framework más específico como es Sinatra, con el que puedo tener un MVC suficientemente potente para aplicaciones web tradicionales o exponer una API. Sinatra instala el servidor web Rack, lo que cubre este apartado también y de una forma sencilla.

En cualquier caso, usar Rails o Sinatra o cualquier otra dependencia es algo que se gestiona mediante la instalación de Gemas (o paquetes), de modo que realmente lo único que necesitamos es preparar un Dockerfile bastante minimalista basado en una imagen específica de Ruby.

Puntos de partida

Para empezar a crear este entorno he partido de los siguientes artículos.

La idea es montar una entorno que me sirva de base tanto para proyecto web como utilidades de línea de comando y, en general, para explorar ideas y también para escribir ejemplos para los artículos del blog o libros.

No es recomendable usar la versión latest, o no especificar ninguna, porque podrías encontrarte con conflictos. Esto no impide actualizar, pero te ayuda a hacerlo de manera coherente y evitando problemas inesperados.

En este sentido, son útiles estas recomendaciones de Florin Lipan.

Y aquí tenemos una primera aproximación a un Dockerfile bastante funcional. He añadido algo de documentación para explicar cada uno de los pasos.

# We use an official ruby image based on alpine as starting point.

FROM ruby:2.7.2-alpine

# Set or change some environment variables in order to make it easy configure docker to particular use cases
# Apply https://bundler.io/guides/bundler_docker_guide.html

ENV PORT=3000 \
    APP_DIR="/home/app" \
    GEM_HOME="/usr/local/bundle" \
    PATH=$GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH \
    USERNAME="rubycon"

# Install system dependencies needed to build and install gems

RUN apk add nodejs && apk add --virtual build-dependencies build-base gcc wget git
RUN gem install bundler

# Set the app directory and expose desired port

WORKDIR $APP_DIR
EXPOSE $PORT

# Create a user in the container and ensure that it is the owner of files

RUN adduser -D $USERNAME && chown -R $USERNAME $GEM_HOME

# Copy project files to the container ensuring that user will be the owner

COPY --chown=$USERNAME . $APP_DIR/

# Set the created user as current user in container

USER $USERNAME

# Execute this script when the container is up, and jump into the shell

ENTRYPOINT [ "./entrypoint.sh", "/bin/sh" ]

El docker-compose.yml quedaría inicialmente así:

version: "3.7"

services:
  rubycon:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: rubycon
    ports:
      - "3000:3000"
    volumes:
      - ./:/home/app

(super original el nombre del proyecto, ¿eh?)

Y al que le añadimos el siguiente entrypoint.sh:

#!/bin/sh

# Use this script to run anything you need in the container

# Apply https://bundler.io/guides/bundler_docker_guide.html

unset BUNDLE_PATH
unset BUNDLE_BIN

# This is be kind and ensure that docker container is up and runnig
echo 'Welcome to Ruby!'

# Init a ruby project with bundler if no Gemfile
if [ ! -f Gemfile ]; then
    bundle init
fi

# Install project dependencies
bundle install

# Execute the extra commands passed in the ENTRYPOINT step of the dockerfile
exec "$@"

Uso

Para construir la imagen, levantar el contenedor y entrar al shell, todo de una sola vez, podemos usar:

docker-compose run --service-ports rubycon

Es decir: este comando monta nuestra imagen, levanta el contenedor exponiendo el puerto 3000 y ejecuta el shell. De este modo, podemos empezar a trabajar dentro del entorno virtual.

Si no hay un archivo Gemfile se creará uno mediante bundle init y, en todos los casos, se actualizará la instalación.

Esta configuración es útil como punto de partida para un proyecto, pero puede necesitar modificaciones si quieres tener un entorno de desarrollo web, ya sea con Rails o con otro framework. Por ejemplo, para arrancar automáticamente el servidor y este tipo de detalles.

Añadir dependencias a un proyecto

Esta plantilla no instala nada más que bundler. Si queremos instalar una determinada dependencia para el proyecto:

bundle add gem-name

Por ejemplo, para una aplicación web, podríamos querer usar Sinatra:

bundle add sinatra sinatra-contrib rack

Repositorio

He puesto un repositorio público que puedes utilizar como plantilla. Si tienes ideas para mejorarlo serán muy bienvenidas.

Temas

good-practices

refactoring

php

testing

tdd

design-patterns

python

blogtober19

design-principles

tb-list

misc

bdd

legacy

golang

dungeon

ruby

tools

hexagonal

tips

software-design

ddd

books

bbdd

soft-skills

pulpoCon

oop

javascript

api

typescript

sql

ethics

agile

swift

java