Skip to main content

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 y app) dentro de un mismo build.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í:

Explicación de la estructura

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 paquete com.github.username.echo.
  • app/ define la aplicación que importa y utiliza la funcionalidad de lib, 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 sobre lib, 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.

¿Y la carpeta src/ que habíamos creado antes?

Puedes eliminar el directorio src/ que creaste en la lección anterior, o guardarlo como recuerdo.

scripts/windows/Remove-SrcDirectory.ps1
# 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:

En la terminal de PowerShell
.\scripts\windows\Remove-SrcDirectory.ps1 -Verbose

📦 Paso 1: Crear la estructura de carpetas

scripts/windows/Initialize-ScalaModules.ps1
[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:

En PowerShell
.\scripts\windows\Initialize-ScalaModules.ps1 -BasePackageName "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:

build.sbt
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 *)
¿Qué acabamos de hacer?

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, como scalaVersion.
  • A continuación declaramos dos módulos (lib y app) usando lazy val:
    • lib se encuentra en el subdirectorio lib/ y recibe las configuraciones comunes.
    • app se encuentra en app/, también hereda las configuraciones comunes y depende explícitamente de lib 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:

lib/src/main/scala/com/github/username/echo/EchoMessage.scala
package com.github.username
package echo

def echoMessage(message: String): String = message
¿Qué acabamos de hacer?

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.

app/src/main/scala/com/github/username/echo/App.scala
package com.github.username
package echo

@main def app(args: String*): Unit =
for arg <- args do
println(echoMessage(arg))
¿Qué acabamos de hacer?

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 o object.
  • La función recibe los argumentos de línea de comandos como una secuencia variable (String*) y los recorre con un bucle for.
  • Cada argumento se imprime utilizando la función echoMessage, definida en el subproyecto lib.

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
¿Qué acabamos de hacer?

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 subproyecto lib, 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.

🔑 Puntos clave

  • sbt permite declarar múltiples subproyectos con lazy val dentro de un único build.sbt.
  • Compartir commonSettings asegura coherencia entre módulos y simplifica la configuración global.
  • La directiva .dependsOn(...) conecta módulos para compartir funcionalidades de forma explícita.
  • Scala 3 permite usar @main para definir puntos de entrada sin necesidad de object o App.
  • Para ejecutar un subproyecto, usamos sbt "nombreModulo/run" desde la raíz del proyecto.

🧰 ¿Qué nos llevamos?

Esta lección no solo mostró cómo configurar un proyecto multi-módulo, sino por qué conviene hacerlo desde el inicio.
Aprendimos a separar responsabilidades, reducir el acoplamiento y preparar la base del proyecto para escalar con claridad.

Con esta estructura podemos:

  • Añadir nuevas funcionalidades en lib sin afectar directamente a app.
  • Reutilizar la biblioteca en otros proyectos si es necesario.
  • Incorporar más módulos (como pruebas, documentación o herramientas internas) sin perder orden ni coherencia.

Empezamos con un simple "echo" y terminamos con una arquitectura preparada para crecer. En el desarrollo de bibliotecas, modularizar desde el principio es una decisión clave para crear proyectos sostenibles y profesionales.

📖 Referencias

🔥 Recomendadas

  • 🌐 "Multi-project builds" en la documentación oficial de sbt: Introduce cómo estructurar múltiples subproyectos en un mismo build.sbt. Explica cómo definir módulos (lazy val), compartir configuraciones y establecer dependencias con .dependsOn(...). Es clave para entender la arquitectura modular en sbt.

🔹 Adicionales