Modularizando tu proyecto Scala con sbt
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/echo-app-sbt
Antes de que una biblioteca crezca —antes de agregar pruebas o automatizar tareas—, es fundamental estructurar bien el proyecto.
Si en la lección anterior aprendiste a crear un proyecto básico con sbt y a ejecutar tu primer programa en Scala 3, esta vez daremos un paso más allá: modularizar tu aplicación.
En esta lección aprenderás a organizar un proyecto multi-módulo con sbt, una práctica esencial para desarrollar bibliotecas reutilizables y aplicaciones escalables.
A través de un ejemplo simple pero completo, veremos cómo:
- Definir múltiples subproyectos (
lib
yapp
) dentro de un mismobuild.sbt
. - Compartir configuraciones comunes entre módulos.
- Reutilizar lógica definida en un módulo desde otro.
- Ejecutar un subproyecto específico desde la línea de comandos.
Este enfoque modular te permitirá escribir código más limpio, reutilizable y fácil de mantener.
Lo que comienza como un proyecto simple puede convertirse en una base sólida para bibliotecas profesionales.
Paso a paso, construimos una arquitectura que Scala (hehe).
🏗️ Estructura esperada del proyecto
Antes de configurar el build.sbt
, es importante visualizar cómo estará organizado el proyecto. Nuestro objetivo es dividirlo en dos subproyectos: una biblioteca (lib
) que contendrá la lógica de negocio reutilizable, y una aplicación (app
) que funcionará como punto de entrada y consumirá esa biblioteca.
La siguiente estructura refleja esta separación, mostrando cómo se distribuyen los archivos fuente dentro de cada módulo y cómo se relacionan entre sí:
Este proyecto está dividido en dos módulos: una biblioteca (lib
) y una aplicación (app
). Ambos se definen en el archivo raíz build.sbt
, lo que permite compartir configuraciones y compilarlos como parte del mismo proyecto.
lib/
contiene la lógica de negocio reutilizable, organizada en el paquetecom.github.username.echo
.app/
define la aplicación que importa y utiliza la funcionalidad delib
, usando el mismo paquete para mantener consistencia.EchoMessage.scala
representa una función o clase de utilidad en la biblioteca.App.scala
actúa como punto de entrada de la aplicación.- El subproyecto
app
declara una dependencia explícita sobrelib
, lo que permite acceder a su código directamente sin duplicación.
Esta estructura modular refleja buenas prácticas en proyectos reales, donde separar la lógica de negocio de la lógica de ejecución permite lograr mayor claridad, mantenibilidad y escalabilidad.
src/
que habíamos creado antes?Puedes eliminar el directorio src/
que creaste en la lección anterior, o guardarlo como recuerdo.
- Windows
- macOS
- Ubuntu/Debian
# Enable cmdlet binding to support -Verbose, -WhatIf, etc.
[CmdletBinding(SupportsShouldProcess)]
param ()
$Script:targetPath = 'src'
# Define a function to remove the 'src' directory recursively and forcefully
function Script:Remove-SrcDirectory {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param ()
# Check if the user wants to proceed
if ($PSCmdlet.ShouldProcess('src', 'Remove src directory')) {
# Deletes the 'src' directory and all its contents, forwarding any bound parameters
Remove-Item -Path $Script:targetPath -Recurse -Force @PSBoundParameters
}
}
# Call the function, forwarding any bound parameters like -Verbose or -WhatIf
Remove-SrcDirectory @PSBoundParameters
Luego puedes ejecutar el script con:
.\scripts\windows\Remove-SrcDirectory.ps1 -Verbose
#!/bin/bash
# Exit on error, undefined variables, or failed pipeline commands
set -euo pipefail
# Directory to be removed
TARGET_PATH="src"
remove_src_directory() {
echo "⚠️ About to delete directory: $TARGET_PATH"
# Prompt user for confirmation
read -r -p "Are you sure? [y/N] " confirm
# Proceed only if user confirms with 'y' or 'Y'
if [[ "$confirm" =~ ^[Yy]$ ]]; then
rm -rf "$TARGET_PATH" # Forcefully remove the directory
echo "✅ Directory '$TARGET_PATH' has been removed."
else
echo "❌ Operation cancelled."
fi
}
# Invoke the function
remove_src_directory
Luego puedes ejecutar el script con:
bash scripts/unix/remove_src_directory.sh
#!/bin/bash
# Exit on error, undefined variables, or failed pipeline commands
set -euo pipefail
# Directory to be removed
TARGET_PATH="src"
remove_src_directory() {
echo "⚠️ About to delete directory: $TARGET_PATH"
# Prompt user for confirmation
read -r -p "Are you sure? [y/N] " confirm
# Proceed only if user confirms with 'y' or 'Y'
if [[ "$confirm" =~ ^[Yy]$ ]]; then
rm -rf "$TARGET_PATH" # Forcefully remove the directory
echo "✅ Directory '$TARGET_PATH' has been removed."
else
echo "❌ Operation cancelled."
fi
}
# Invoke the function
remove_src_directory
Luego puedes ejecutar el script con:
bash scripts/unix/remove_src_directory.sh
📦 Paso 1: Crear la estructura de carpetas
- Windows
- macOS
- Ubuntu/Debian
[CmdletBinding()] # Support for advanced features like -Verbose and -Debug
param (
# Require a valid Scala-style package name (e.g., com.example.project)
[Parameter(Mandatory)]
[ValidatePattern('^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$')]
[string] $BasePackageName
)
function Script:Initialize-ScalaModules {
[CmdletBinding()] # Support for advanced features like -Verbose and -Debug
param (
# Base directory in path format (e.g., com/example/project)
[string] $BaseDir
)
# Define initial file paths for app and library sources
$paths = @(
"app/src/main/scala/$BaseDir/echo/App.scala",
"lib/src/main/scala/$BaseDir/echo/EchoMessage.scala"
)
# Create necessary directories and files
foreach ($path in $paths) {
New-Item -Path $path `
-Force `
-ItemType File `
-Verbose:$VerbosePreference `
-Debug:$DebugPreference
# Write the package declaration to the file
Set-Content -Path $path `
-Value "package $BasePackageName.echo`n" `
-Verbose:$VerbosePreference `
-Debug:$DebugPreference
}
}
# Replace dots in package name with slashes to match directory structure
$Script:baseDir = $BasePackageName -replace '\.', '/'
# Call the function to create Scala module structure
Initialize-ScalaModules -BaseDir $baseDir
Luego puedes ejecutar el script con:
.\scripts\windows\Initialize-ScalaModules.ps1 -BasePackageName "com.github.username"
#!/bin/bash
# Exit immediately on error, undefined variable, or failed pipeline command
set -euo pipefail
# Get the base package name from the first argument
BASE_PACKAGE_NAME="$1"
# Validate the package name using a regex: must follow Scala package conventions
if [[ ! "$BASE_PACKAGE_NAME" =~ ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$ ]]; then
echo "❌ Invalid package name: '$BASE_PACKAGE_NAME'"
echo "Use a valid Scala package name (e.g., com.example.project)"
exit 1
fi
# Convert the dot-separated package name into a path (e.g., com.example → com/example)
BASE_DIR="${BASE_PACKAGE_NAME//./\/}"
# Define the files to be created
FILES=(
"app/src/main/scala/$BASE_DIR/echo/App.scala"
"lib/src/main/scala/$BASE_DIR/echo/EchoMessage.scala"
)
# Create each file with a corresponding package declaration
for FILE in "${FILES[@]}"; do
mkdir -p "$(dirname "$FILE")" # Ensure parent directories exist
echo -e "package ${BASE_PACKAGE_NAME}.echo\n" > "$FILE" # Write package declaration
echo "✅ Created $FILE"
done
Luego puedes ejecutar el script con:
bash scripts/unix/initialize_scala_modules.sh com.github.username
#!/bin/bash
# Exit immediately on error, undefined variable, or failed pipeline command
set -euo pipefail
# Get the base package name from the first argument
BASE_PACKAGE_NAME="$1"
# Validate the package name using a regex: must follow Scala package conventions
if [[ ! "$BASE_PACKAGE_NAME" =~ ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$ ]]; then
echo "❌ Invalid package name: '$BASE_PACKAGE_NAME'"
echo "Use a valid Scala package name (e.g., com.example.project)"
exit 1
fi
# Convert the dot-separated package name into a path (e.g., com.example → com/example)
BASE_DIR="${BASE_PACKAGE_NAME//./\/}"
# Define the files to be created
FILES=(
"app/src/main/scala/$BASE_DIR/echo/App.scala"
"lib/src/main/scala/$BASE_DIR/echo/EchoMessage.scala"
)
# Create each file with a corresponding package declaration
for FILE in "${FILES[@]}"; do
mkdir -p "$(dirname "$FILE")" # Ensure parent directories exist
echo -e "package ${BASE_PACKAGE_NAME}.echo\n" > "$FILE" # Write package declaration
echo "✅ Created $FILE"
done
Luego puedes ejecutar el script con:
bash scripts/unix/initialize_scala_modules.sh com.github.username
🧱 Paso 2: Declarar los módulos del proyecto
Para transformar nuestro proyecto en una estructura multi-módulo, comenzamos por definir sus componentes principales dentro del archivo build.sbt
raíz:
ThisBuild / scalaVersion := "3.7.1"
ThisBuild / organization := "com.github.username"
ThisBuild / name := "echo-app"
lazy val commonSettings = Seq(
scalacOptions ++= Seq(
"-deprecation",
"-unchecked",
"-feature"
)
)
lazy val lib = project
.in(file("lib"))
.settings(commonSettings *)
lazy val app = project
.in(file("app"))
.dependsOn(lib)
.settings(commonSettings *)
lazy val root = project
.in(file("."))
.aggregate(lib, app)
.settings(commonSettings *)
En este paso transformamos nuestro proyecto en una estructura multi-módulo, lo que nos permite separar responsabilidades entre distintos componentes (por ejemplo, una biblioteca reutilizable y una aplicación principal).
- Primero definimos
scala3Version
como una variable para centralizar la versión del compilador. - Luego creamos una lista llamada
commonSettings
que contiene configuraciones compartidas, comoscalaVersion
. - A continuación declaramos dos módulos (
lib
yapp
) usandolazy val
:lib
se encuentra en el subdirectoriolib/
y recibe las configuraciones comunes.app
se encuentra enapp/
, también hereda las configuraciones comunes y depende explícitamente delib
usando.dependsOn(lib)
.
Usamos lazy val
porque sbt
necesita construir la estructura del proyecto de forma perezosa (lazy): permite que las referencias entre proyectos (como app.dependsOn(lib)
) se resuelvan sin problemas incluso si aún no se han evaluado por completo.
Esto evita errores de orden de inicialización y permite que sbt
maneje correctamente las dependencias entre módulos.
Esta estructura modular es especialmente útil en proyectos de bibliotecas, ya que permite mantener el código reutilizable separado del código específico de una aplicación o herramienta.
📦 Paso 3: Crear el módulo de biblioteca
Una vez declarado el subproyecto lib
, es momento de comenzar a escribir la lógica de negocio que deseamos reutilizar. Empezaremos con una función sencilla, pensada para ser consumida desde otros módulos:
package com.github.username
package echo
def echoMessage(message: String): String = message
Este archivo define un componente reutilizable dentro del subproyecto lib
.
- El paquete
com.github.username.echo
sigue la convención de dominios invertidos, facilitando la organización del código en proyectos más grandes. - La función
echoMessage
simplemente devuelve el mismo mensaje que recibe. Aunque su comportamiento es sencillo, nos servirá para verificar que otros módulos pueden importar y utilizar funcionalidades definidas en esta biblioteca.
Este módulo marca el punto de partida para construir una biblioteca bien estructurada, que podrá crecer y evolucionar conforme avancemos en el curso.
🚀 Paso 4: Crear el módulo de aplicación
Ahora que tenemos una biblioteca reutilizable en lib
, es momento de crear el subproyecto app
, encargado de ejecutar la lógica principal del programa. Esta aplicación imprimirá en consola los mensajes recibidos como argumentos, utilizando la función echoMessage
definida previamente.
package com.github.username
package echo
@main def app(args: String*): Unit =
for arg <- args do
println(echoMessage(arg))
Este archivo define la aplicación principal del proyecto. Su objetivo es utilizar la funcionalidad proporcionada por la biblioteca lib
.
- La anotación
@main
indica que esta es la función de entrada del programa. Scala 3 permite definir puntos de entrada sin necesidad de declarar una clase oobject
. - La función recibe los argumentos de línea de comandos como una secuencia variable (
String*
) y los recorre con un buclefor
. - Cada argumento se imprime utilizando la función
echoMessage
, definida en el subproyectolib
.
Gracias a esta integración, podemos verificar que app
depende correctamente de lib
, y que los módulos se comunican de forma efectiva dentro del mismo proyecto multi-módulo.
Este paso demuestra cómo separar la lógica de ejecución (aplicación) de la lógica reutilizable (biblioteca), una práctica esencial para construir proyectos bien organizados y escalables.
🧪 Paso 5: Ejecutar la aplicación
Con ambos módulos ya configurados y conectados, es momento de ejecutar app
desde la raíz del proyecto para comprobar que la integración entre módulos funciona correctamente.
sbt "app/run Alex Dim Nah Dim"
Deberías ver una salida como esta:
Alex
Dim
Nah
Dim
En este paso usamos el comando sbt "app/run"
para compilar y ejecutar el subproyecto app
.
Los argumentos que siguen (Alex Dim Nah Dim
) se envían directamente a la función @main
definida en App.scala
.
- Scala ejecuta la función principal con los argumentos indicados.
- Cada uno se procesa mediante
echoMessage
, definida en el subproyectolib
, y se imprime por separado. - Elegimos nombres de personajes de A Clockwork Orange como una forma divertida de verificar el comportamiento del programa.
Este paso valida que la estructura modular del proyecto está funcionando: app
puede usar sin problemas la lógica definida en lib
, y sbt maneja correctamente la compilación y ejecución de ambos módulos.
Con esta ejecución completamos la primera prueba de integración de nuestro proyecto multi-módulo. A partir de aquí, podemos escalar la aplicación o la biblioteca de forma independiente, manteniendo una separación clara de responsabilidades.
🎯 Conclusiones
Dividir un proyecto en múltiples módulos no solo mejora la organización del código, sino que sienta las bases para desarrollar software más escalable, reutilizable y mantenible. En esta lección aprendimos a estructurar un proyecto multi-módulo con sbt, separando la lógica principal de la aplicación (app
) de una biblioteca reutilizable (lib
), todo dentro de una configuración común y coherente.
También exploramos cómo ejecutar la aplicación con argumentos personalizados, lo que nos permitió validar la integración entre módulos y ver en acción una arquitectura modular.