Scala 3. Dotty II: tipo unión

En la presente entrada, Scala 3. Dotty II: tipo unión, describiré el tipo Unión. El tipo Unión es parecido al tipo intersección descrito en la entrada anterior. El tipo Unión permite que una determinada instancia sea de un tipo determinado o bien de otro. El tipo unión se representa por el símbolo | y cumple la propiedad conmutativa.

En el siguiente ejemplo, se muestra una función con un argumento de tipo unión.

trait TypeA{
  val elemA: String
}
trait TypeB{
  val elemB: String
}
case class ClassTypeA(elemA: String) extends TypeA
case class ClassTypeB(elemB: String) extends TypeB

def printPretty(arg: ClassTypeA | ClassTypeB): Unit = {
  val value = arg match{
    case ClassTypeA(eA) => eA
    case ClassTypeB(eB) => eB
  }
  println(s" Value argument=$value")
}
object Main{
  def main(args: Array[String]): Unit = {
    val a = ClassTypeA("aa")
    val b = ClassTypeB("bb")
    printPretty(a)
    printPretty(b)
  }
}

El código define lo siguiente: se define dos case class de tipo TypeA y TypeB implementadas en las clases ClassTypeA y ClassTypeB; se define una función printPretty cuyo parámetro puede ser del tipo ClassTypeA o bien ClassTypeB; y, para finalizar, se define un objeto Main cuya función main realiza dos llamadas a la función printPretty con dos parámetros con los posibles tipos definidos.

La salida por consola del código es la siguiente:

Value argument=aa
Value argument=bb

Lo más destacado del código anterior es la función printPretty. La función recibe un argumento cuyo tipo puede ser de los tipos definidos ClassTypeA, o bien, ClassTypeB, en función del tipo de entrada, la función escribirá por consola diferentes valores.

En la siguiente entrada, Dotty III: enumeraciones, describiré el tipo enumeración.

AWS Lambda en Scala. Operaciones con AWS S3

Las tres grandes soluciones utilizadas en el mundo empresarial para definir sistemas cloud son Amazon AWS, Microsoft Azure y Google Cloud. Las tres soluciones permiten la posibilidad de desarrollar arquitecturas serverless la cual se implementan con funciones lambda. En la presente entrada, AWS Lambda en Scala. Operaciones con AWS S3, describiré cómo definir una función lambda en Amazon AWS.

 

 

 

 

 

Definimos arquitectura Serverless como aquella arquitectura que define sistemas con aplicaciones y servicios, con capacidad de ejecución, así como, la posibilidad de crear nuevas aplicaciones y servicios, sin necesidad de administrar infraestructura.

Definimos una función Lambda de AWS como “un un servicio informático que permite ejecutar código sin aprovisionar ni administrar servidores. AWS Lambda ejecuta el código sólo cuando es necesario, y se escala de manera automática, pasando de pocas solicitudes al día a miles por segundo”.

Las funciones Lambda pueden ser definidas en diferentes lenguajes como pueden ser: Java, Python, Node, Scala,… en las diferentes plataformas. Dada la diversidad de plataformas y lenguajes, las soluciones son amplias y diversas. Para unificar funcionalidad ante las plataformas y lenguajes existen frameworks que ofrecen operaciones para simplificar la tarea al desarrollador. Un ejemplo de este tipo de tecnología es el framework Serverless.

El framework Serverless es una herramienta Open Source la cual permite el desarrollo y despliegue de aplicaciones serverless en AWS, Azure, Google y otras más.

Instalación en entorno Linux/Mac

La instalación del framework en un entorno Linux o Mac es muy sencilla, simplemente es necesario ejecutar el siguiente comando desde la línea de comando:

curl -o- -L https://slss.io/install | bash

Para la verificación de la instalación, se ejecuta el siguiente comando:

serverless -h

El resultado del comando anterior deberá de mostrar la información de los comandos del framework.

Descripción funcional de la función de ejemplo

Definiremos una función que opere sobre la solución cloud de Amazon. Desde un punto de vista funcional, la función es sencilla, realizará ciertas operaciones con el servicio S3 de AWS descritas en el siguiente listado:

  • Listado de los bucket existentes.
  • Creación de un bucket en S3.
  • Subida de un fichero a S3.
  • Descarga de un fichero a S3.

Creación de la función con Serverless

Para realizar la creación de una función, utilizaremos el comando create del framework Serverless; para ello, en la consola del sistema, crearemos una carpeta (por ejemplo: serverless-scala-aws-s3) y ejecutaremos el comando create de serverless. El snippet copn los comandos son los siguientes:

cd serverless-scala-aws-s3
serverless create --template aws-scala-sbt --path lambda-s3

El comando create emplea la plantilla para un proyecto en Scala con sbt y define el path a la función. Además de la plantilla del lenguaje Scala, se pueden definir funciones en otros lenguajes como Python, Java, kotlin, Go,…

La vista de la estructura creada desde un IDE es el siguiente:

Los componentes del proyecto son los siguientes:

  • build.sbt Fichero sbt para la gestión del ciclo de vida del código de la función. Al tener que operar con S3 se debe de definir la dependencia de la librería AWScala en la referencia libraryDependencies. Las librerías utilizadas en este proyecto son las siguientes:
libraryDependencies ++= Seq(
  "com.amazonaws" % "aws-lambda-java-events" % "2.2.7",
  "com.amazonaws" % "aws-lambda-java-core" % "1.2.0",
  "com.amazonaws" % "aws-lambda-java-log4j2" % "1.1.0",
  "com.github.seratch" %% "awscala" % "0.8.+"
)
  • Componentes de Scala. La plantilla del framework crea automáticamente cuatro componentes, siendo el más importante el Handler de la función.
    • Handler.- El componente handler define dos clases: Handler, para la función lambda a desarrollar; y, ApiGatewayHandler, para definir la clase para el servicio API Gateway; en nuestro caso, nos centraremos en la clase Handler.La clase Handler define un método handleRequest en el cual desarrollaremos la funcionalidad del ejemplo.
import scala.jdk.CollectionConverters._
class Handler extends RequestHandler[Request, Response] {
  val logger: Logger = LogManager.getLogger(getClass)
  def handleRequest(input: Request, context: Context): Response = {
    implicit val region = Region.US_EAST_1
    implicit val s3 = S3()
    val buckets: Seq[Bucket] = s3.buckets
    logger.info(s"\n1 buckets: $buckets \n")
    val bucket: Bucket = s3.createBucket("prueba2fromlambdafunction")
    logger.info(s"\n2 bucket: $bucket \n")
    // Upload operation of the file example1-file.txt with name example1-uploaded-file.txt
    bucket.put("example1-uploaded-file.txt", new java.io.File("example1-file.txt"))
    val s3obj: Option[S3Object] = bucket.getObject("example1-uploaded-file.txt")
    logger.info(s"\n3 Uploaded: ${s3obj.getOrElse("Empty")} \n")
    logger.info(s"Received a request: $input")
    Response("Go Serverless v1.0!!!!! Your function executed successfully!!", input)
  }
}
    • Request.- Define la clase Request con los parámetros del evento de entrada.
import scala.beans.BeanProperty
class Request(@BeanProperty var key1: String, @BeanProperty var key2: String, @BeanProperty var key3: String) {
  def this() = this("", "", "")
  override def toString: String = s"Request($key1, $key2, $key3)"
}
    • Response.- Define la clase respuesta del tipo de retorno del Handler.
import scala.beans.BeanProperty
case class Response(@BeanProperty message: String, @BeanProperty request: Request)
    • ApiGatewayResponse.- Define la clase de respuesta para el caso de APIGateway.
case class ApiGatewayResponse(@BeanProperty statusCode: Integer, @BeanProperty body: String,
@BeanProperty headers: java.util.Map[String, String], @BeanProperty base64Encoded: Boolean = false)
  • Serverless.yml. El fichero serverless.yml es aquel lugar donde se configura la función para que sea desplegada en AWS. El fichero está compuesto por varias secciones en donde se define las variables, la función, o bien, aquellos recursos necesarios de AWS. Este fichero es el que empleará el framework Serverless para definir la plantilla de CloudFormation para su despliegue en AWS. La secciones son:
    • Service.- Definición del nombre del servicio de la función en AWS.
    • Provider.- definición de las variables internas a AWS.
    • Custom.- Definición de las variables específicas para la función como por ejemplo: nombre del proyecto, región,… proporcionada por los valores definidos en Provider, o bien, desde la línea de comando.
    • Environment.- Definición de las variables de entorno globales.
    • Package.- configuración del paquete a crear para realizar la subida a AWS. Se puede definir qué ficheros incluir o excluir, o bien, el nombre del jar con el que se trabaja.
    • Functions.- definición de la función Scala, definición de la referencia del rol, variables de entorno,…
    • Resources.- definición de los recursos empleados por la función en AWS; en nuestro caso, definición del role y las políticas de seguridad.

El contenido del fichero es el siguiente:

service: lambda-s3
provider:
  name: aws
  project: scalaproject
  runtime: java8
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  timeout: 900
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:GetObject
        - s3:PutObject
      Resource:
        - "arn:aws:s3:::prueba2fromlambdafunction/*"
custom:
  currentStage: ${opt:stage, self:provider.stage}
  currentProject: ${self:provider.project}
  currentRegion: ${opt:region, self:provider.region}
environment:

package:
  individually: true
  artifact: target/scala-2.13/lambda-s3.jar

functions:
  lambda-s3:
    handler: app.Handler
    role: LambdaRole
    environment:
      ENV: ${self:custom.currentStage}
resources:
  Resources:
    LambdaRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: ${self:custom.currentProject}-lambda-s3-${self:custom.currentStage}
          AssumeRolePolicyDocument:
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: ${self:custom.currentProject}-lambda-s3-${self:custom.currentStage}
            PolicyDocument:
              Statement:
                - Effect: Allow
                  Action:
                    - logs:CreateLogGroup
                    - logs:CreateLogStream
                    - logs:PutLogEvents
                    - s3:*
                  # - ec2:DescribeNetworkInterfaces
                  # - ec2:CreateNetworkInterface
                  # - ec2:DeleteNetworkInterface
                  # - ec2:DescribeInstances
                  # - ec2:AttachNetworkInterface
                 Resource: "*"

De snippet anterior resaltar las líneas comentadas en la definición de los permisos; éstas líneas, corresponden a los permisos que se deben de añadir si se desea que la función Lambda se ejecute en una subred de una VPC determinada.

Ciclo de vida

Configuración del profile de AWS. Para trabajar con AWS es necesario instalar el cliente de AWS y definir las credenciales del usuario para poder realizar las operaciones de despliegue en la cuenta de Amazon.

  • Creación del artefacto. Para realizar el despliegue, es necesario construir el artefacto con los componentes Scala y su ensamblado con las librerías necesarias. El comando SBT a ejecutar en la carpeta de la función es el siguiente:
sbt assembly
  • Despliegue de la función en Amazon AWS. El proceso de despliegue consiste en crear o modificar los recursos en AWS o el código de la función utilizando el stack de cloudformation asociado al fichero serverless.yml. El comando a ejecutar en la carpeta de la función es el siguiente:
serverless deploy -r us-east-1
  • Eliminación de la función. Si se desea eliminar la función se puede eliminar la función y sus recursos asociados con el siguiente:
serverless remove

Librería AWScala

La librería AWScala es aquella librería que permite realizar las operaciones con S3 u otros servicios de AWS. En nuestro caso, nos centraremos en definir las operaciones en S3.

  • Instancia del cliente S3. La creación de un cliente para realizar operaciones con S3 se realiza creando un componente de tipo S3. Dado que la función tiene asociado un role con los permisos de acceso, no es necesario asignar las credenciales. El snippet de ejemplo es el siguiente:
import awscala._, s3._
import awscala.s3._
import awscala.Region
implicit val region = Region.US_EAST_1
implicit val s3 = S3()
  • Listado de los buckets existentes. Una vez creado el cliente, se realiza la conexión al servicio S3 y, con la función buckets, obtenemos una lista con los bucket existentes. El snippet de ejemplo es el siguiente:
val buckets: Seq[Bucket] = s3.buckets
  • Creación de un bucket. De la misma manera que el caso anterior, el cliente S3 tiene una función de creación de bucket cuyo nombre es createBucket al cual se le pasa un nombre único del bucket a crear. El snippet de ejemplo es el siguiente:
val bucket: Bucket = s3.createBucket("prueba2fromlambdafunction")
  • Subir un fichero a S3. Para subir un fichero a S3, el cliente S3 emplea la función put al cual, como primer parámetro, se le pasa el nombre que tendrá en el bucket; y, como segundo parámetro, se le pasa un objeto File con la referencia del fichero. En nuestro caso, existe un fichero de texto example1-file.txt en el proyecto. El snippet de ejemplo es el siguiente:
bucket.put("example1-uploaded-file.txt", new java.io.File("example1-file.txt"))
  • Descarga de un fichero de S3. Para descargar un fichero de S3, el cliente emplea la función getObject a la cual se le pasa como parámetro el nombre del elemento a descargar. El snippet de ejemplo es el siguiente:

val s3obj: Option[S3Object] = bucket.getObject(“example1-uploaded-file.txt”)

Ejecución

Una vez desplegada la función con el comando de serverless deploy, hay que entrar en la consola de AWS y navegaremos hasta la consola de funciones lambda;y,
una vez en la consola, tendremos la referencia a la función. Para las pruebas, crearemos un evento con unos datos de pruebas como los siguientes:

{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}

Para ejecutar la función, pulsaremos el botón de Test en la parte superior derecha; tras la ejecución, se reportará el resultado de la función y se crearán en el cloudwatch las trazas de la función. El aspecto de la consola de AWS con la información de la función es el siguiente:

El resultado de la función en S3 es la creación de un fichero en el bucket prueba2fromlambdafunction. La vista de la consola S3 tras la ejecución de la función es la siguiente:

 

Si el lector está interesado en el código puede acceder al siguiente repositorio de GitHub.

Conclusiones

La selección del lenguaje con el que se opera con AWS depende del equipo de desarrollo ya que, en función del conocimiento de los posibles lenguajes, se seleccionará uno u otro. Desde mi experiencia en los equipos en lod que he trabajado, siempre se ha elegido el lenguaje Python por su sencillez de uso utilizando la librería boto3. Con el presente ejemplo, quiero poner de manifiesto la sencillez con el lenguaje Scala y, dado que estamos construyendo funciones lambda sin infraestructura, utilizar un lenguaje con paradigma funcional permite construir componentes software orientados a la funcionalidad a desarrollar.

Scala 3. Dotty I: Tipos intersección

Inicio una serie de entradas sobre las nuevas características del compilador Dotty el cual representará la versión 3 de Scala que será lanzado en los próximos meses.

Para la realización de los ejemplo, utilizaré el editor de código Scatie. Si el lector está interesado en la descripción del editor, puede acceder al siguiente enlace.

En la presente entrada, me centraré en el operador de intersección de tipos. El operador se representa con el carácter &. La intersección de tipos permite determinar que una instancia puede ser de dos los tipos determinados, es decir, sean dos tipos definidos A y B y una instancia cuya definición de tipo es del tipo A & B, determina que dicha instancia es del tipo A y del tipo A. El operador & es conmutativo con los cual A & B es igual a B & A. A continuación, se muestra el siguiente ejemplo descriptivo:

trait TypeA{
  val elemA: String
}
trait TypeB{
  val elemB: String
}
case class ClassTypeA(elemA: String) extends TypeA
case class ClassTypeB(elemB: String) extends TypeB
case class IntersectionAB(elemA:String, elemB:String) extends TypeA with TypeB

def printPretty(arg: TypeA & TypeB): Unit = println("printPretty:" + arg.elemA + " & " + arg.elemB)
def printPretty2(arg: IntersectionAB): Unit = println("printPretty2:" + arg.elemA + " & " + arg.elemB)

object Main{
  def main(args: Array[String]): Unit = {
    val x = IntersectionAB("aa", "bb")
    printPretty(x)
    println(x)
    val x2 = new IntersectionAB("aa", "bb")
    printPretty(x2)
    println(x2)
    val x3 = new IntersectionAB("aa", "bb")
    printPretty2(x3)
    println(x3)
  }
}

En el ejemplo anterior, se definen dos tipos definidos en los trait TypeA y TypeB; se definen dos case clases de los tipos anteriores cuyos nombres respectivos son ClassTypeA y ClassTypeB; se define una case class IntersectionAB que implementa los tipos anteriores; se define una función printPretty cuyo parámetro es un tipo intersección de los tipos TypeA y TypeB; se define una función printPretty2 cuyo parámetro es del tipo IntersectionAB; y, para finalizar, se define un objeto Main con la función main en donde se instancia objetos intersección y se pasan a las dos funciones de impresión. La funcionalidad de las funciones pretty son sencillas, solo imprimen por consola los tipos pasados por parámetro.

La salida por consola del código es la siguiente:

printPretty:aa & bb
IntersectionAB(aa,bb)
printPretty:aa & bb
IntersectionAB(aa,bb)
printPretty2:aa & bb
IntersectionAB(aa,bb)

Lo más destacado del ejemplo es la función printPretty la cual define un argumento cuyo tipo debe de ser del tipo TypeA y TypeB. Si a la función no se le pasa un objeto con dicha condición, el código no compila.

En la siguiente entrada, Scala 3. Dotty II: uniones de tipo, describeré el tipo unión.

Scastie

Scastie  es un editor de código para trabajar con Scala. Scastie puede ser configurado  para poder operar con las diferentes versiones de Scala (2.12.12 y 2.13.3); puede ser configurado con el nuevo compilador de Scala Dotty; puede ser configurado para trsabajar con TypeLevel; o bien, puede ser configurado para utilizar Scala.js.

Scastie es una muy buena herramienta para realizar pequeños snippet de código para realizar pruebas sencillas sin necesidad de instalar un entorno de trabajo. Scastie tiene dos funciones:

  • Editor de código.- En la parte izquierda, existe la opción “Editor” el cual permite activar el editor de código. El aspecto del editor es el siguiente:

editor

  • Configuración.- En la parte izquierda, exista la opción “Build Settings” el cual permite activar la configuración del editor. El aspecto de la configuración es el siguiente:

settings

Scastie utiliza sbt para la gestión del ciclo de vida y permite la definición de las librerías necesarias como las opciones de configuración.

Analizando la imagen del editor de código en la parte superior, destacamos los siguientes botones de acciones:

  • Save.- Guarda y ejecuta el código.
  • New.- Abre un nuevo snippet de código.
  • Format.- Verificación del estilo del código.
  • Clear Messages.- Elimina los mensajes de error de compilación y error de formateo.
  • Download.- Descarga del código.

En la parte inferior del editor, se encuentra la salida de la consola. En el lateral izquierdo de Scastie, se dispone del botón “Help” el cual muestra la ayuda del editor; y, el botón “Light” el cual permite cambiar el fondo del editor con una opción de fondo oscura y otra de forma clara.

Scastie es una buena herramienta para desarrollar y probar pequeñas cantidades de código. Por otro lado, al permitir ejecutar código en las diferentes versiones de los compiladores, es una buena herramienta para tener un entorno de trabajo para aprender nuevas características, como por ejemplo Dotty, sin necesidad de instalar un entorno local.

Ejecución de Ansible en un contenedor Docker

Ansible es una herramienta open source, gestor de configuración, despliegue y orquestación. Su objetivo es automatizar tareas de IT para aumentar la productiviidad en tareas de configuración y despliegue. El mecanismos utilizado es la definición de playbooks con la declaración de las tareas.

Docker es un proyecto open source que permite el despliegue de aplicaciones en contenedores software en múltiples sistemas operativos.

Para trabajar con Ansible desplegando en un contenedor Docker es necesario definir un contenedor con un servicio SSH para que Ansible se pueda conectar y ejecutar los playbooks que se definen. Para realiizar esta tarea, necesitamos lo siguiente:

  1. Definir el contendor con el servicio SSH
  2. Configurar y definir el playbook de Ansible.

Docker

Lo primero que debemos de realizar es definir una Dockerfile con la configuración de la imagen del contenedor que contenga un servidor SSH. Para ello, nos basaremos en la documentación existente en la documentación de docker.

La modificación a realizar es insertar el valor de la password del usuario root. El resultado del fichero es el siguiente:

FROM ubuntu:16.04
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
ENV NOTVISIBLE "in users profile"
RUN echo "export VISIBLE=now" >> /etc/profile
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

Para realizar la construcción de la imagen, debemos de ejecutar el siguiente comando:

docker image build -t alvaroms/ubuntu-serverssh:v1 .

Para arrancar el contenedor con la imagen creada anteriormente, ejecutamos el siguiente comando:

docker container run -d --name dockerssh -p 50022:22 alvaroms/ubuntu-serverssh:v1

Una vez arrancado el contenedor, debemos de obtener la IP asignada; para ello, debemos de inspeccionar la configuración del contenedor para obtener la IP del Gateway; esta operación, la realizamos con el siguiente comando:

docker container inspect dockerssh | grep Gateway

Suponiendo que la Ip obtenida es la 172.17.0.1, la conexión ssh la realizamos con el siguiente comando:

ssh -p 50022 root@172.17.0.1

Para completar la operación, deberemos de introducir la password definida en el fichero Dockerfile.

Ansible

Para trabajar con Ansible he definido un usuario con nombre Ansible y los permisos adecuados.

En el fichero /home/ansible/inventory inserto las siguiente líneas:

[all:vars]
ansible_connection=ssh
ansible_user=root
ansible_ssh_pass=root
[...]
[dockers]
ubuntu-sshserver ansible_port=50022 ansible_host=172.17.0.1

Con estas modificaciones definimos las credenciales de conexión al contenedor Docker y definimos la configuración del contenedor para utilizarla en los playbook.

Definimos una playbook con el cual nos permite instalar Git. El playbook es el siguiente:

--- # install git on target host
- hosts: ubuntu-sshserver
become: yes
tasks:
- name: install git
yum:
name: git
state: latest

Para la ejecución del playbook ejecutamos el siguiente comando:

ansible-playbook -i /home/ansible/inventory ./git-setup.yml

Para verificar la correcta instalación, nos conectamos al contenedor por SSH y obtenemos la versión de git los comandos son:

ssh -p 50022 root@172.17.0.1
docker>git --version

Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose

Finalizamos la serie de artículos de Kotlin y SpringBoot con el presente artículo con título “Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose”. El objetivo de la entrada es dockerizar la aplicación para hacer funcionar la aplicación desarrollada en un contenedor Docker. Además, definiremos la aplicación para que funcione en una composición de contenedores con un contenedor con MySQL.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La estructura del artículo está compuesto por los siguientes puntos:

  1. Docker
    • Propuesta 1
    • Propuesta 2
    • Propuesta 3
  2. DockerCompose

1.- Docker

La aplicación la podemos dockerizar de tres formas posibles. Las diferencia reside en el tamaño de la imagen resultante.

Propuesta 1

La estrategia consiste en copiar el contenido de la aplicación en el interior de la imagen y posteriormente realizar la creación del artefacto. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
RUN mv /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v1 .

Para visualizar las imágenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de la ejecución del comando muestra una imagen con un tamaño aproximado a 686MB.

Propuesta 2

La segunda estrategía consigue reducir el tamaño de la imagen. Para ello, se realiza la creación del artefacto fuera de la imagen y se copia al interior de la imagen. Es decir, respecto a la solución anterior, se elimina el paso de construcción del artefacto. Para realizar la creación del artefacto, es necesario definir una tarea de creación del fichero jar mediante una tarea en Gradle. La tarea Gradle es la siguiente:

tasks.register("buildDocker"){
  dependsOn("build")
  doLast{
    fileList("./build/libs").forEach { file ->
      file.copyTo(File("./ejem1kotlindocker.jar"))
    }
  }
}

Como se observa en el snippet anterior, la tarea “buildDocker” tiene una dependencia con la tarea build; una vez ejecutado build, su resultado, generado en la carpeta “./build/libs” del proyecto, se copia a la carpeta raíz del proyecto. Esta tarea debe de ser ejecutada previamente a la construcción de la imagen.

La definición de la imagen en el Dockerfile es la siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
WORKDIR /app
EXPOSE 8080
ADD ./ejem1kotlindocker.jar /app/ejem1kotlindocker.jar
ENTRYPOINT ["java", "-jar", "/app/ejem1kotlindocker.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v2 .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable.

Propuesta 3

La tercera estrategia consiste en fusionar los objetivos de las anteriores. Para ello, primeramente, realizamos la creación del artefacto en una primera capa; posteriormente,
con el artefacto creado, construimos la imagen. Todo ello en el paso de construcción de la imagen y en elmomento de crear la imagen Docker. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk AS build
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
FROM openjdk:8-jdk-alpine

ARG APP_VERSION=1.0.0-SNAPSHOT
ENV database__client=mysql
ENV database__connection__host=mysql
ENV database__connection__user=root
ENV database__connection__password=password
ENV database__connection__database=prueba
ENV SPRING_PROFILES_ACTIVE=dev
LABEL org.label-schema.version=$APP_VERSION
COPY --from=build /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar" ]

Con esta opción, definimos las variables de entorno con la configuración por defecto a la base de datos cuyos valores pueden ser pasados por parámetro en la creación de la imagen. De la misma manera que los argumentos necesarios.

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v3 --build-arg APP_VERSION=1.0.3-SNAPSHOT .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable;y, respecto a la segunda, no es necesario realizar ninguna operación intermedia. Así, de las tres definiciones de Dockerfile, esta tercera, es la solución más eficiente.

2.- DockerCompose

DockerCompose permite realizar la composición de contenedores interconectados. La definición de la composición se realiza en el fichero docker-compose.yml, ubicado en la raíz del proyecto. La composición se define en base a dos servicios: el primero, mysql, corresponde con un contenedor de una imagen de MySQL; el segundo, kotlindocker, corresponde con la aplicación desarrollada.

Cada contenedor está definida en dos redes: kotlinnetwork, para el contenedor de la aplicación; y, mysqlnetwork, para el contenedor de la base de datos MySQL. La conexión la realiza el servicio de kotlin al tener conexión a las dos redes. Para los volúmenes, se definen un volumen para cada servicio.

Otro detalle importante, reside en qué imagen de la aplicación seleccionamos, o bien, creamos la imagen a partir del código existente: opción build, creamos la imagen a partir del código; e, image, creamos el contenedor a partir de una imagen definida.

El contenido del fichero docker-compose es el siguiente:

version: '3'
services:
  mysql:
    container_name: mysql
    image: mysql:5.7
    environment:
      - MYSQL_DATABASE=prueba
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - 3306:3306
    volumes:
      - mysqlvolume:/var/lib/mysql
    networks:
      - mysqlnetwork
  kotlindocker:
    depends_on:
      - mysql
    container_name: kotlindocker
      # image: alvaroms/ejem1kotlindocker:v4
      build: ./
    environment:
      - SPRING_PROFILES_ACTIVE=dev
    ports:
      - 8087:8080
    volumes:
      - kotlinvolume:/var/lib/kotlin
    networks:
      - kotlinnetwork
      - mysqlnetwork
    volumes:
      kotlinvolume:
      mysqlvolume:
    networks:
      kotlinnetwork:
        driver_opts:
          com.docker.network.bridge.name: kotlinNetwork
      mysqlnetwork:
        driver_opts:
          com.docker.network.bridge.name: MySQLNetwork

Para el lector interesado en el código del proyecto puede acceder en el siguiente enlace

Finalizamos con una reflexión sobre la serie sobre kotlin, Spring Boot y Docker: un punto a tener en cuenta es que el código Kotlin presentado es muy mejorable al no haber aplicado patrones funcionales como los que se pueden presentar con la librería Arrow; con ella, la mejora del código es notable, por ejemplo, utilizando contenedores binarios.

Kotlin, SpringBoot, Docker y DockerCompose II: test unitarios

Continuando con el segundo artículo de la serie con título Kotlin “SpringBoot, Docker y DockerCompose II: test unitarios” en el cual me centraré en cómo definir pruebas unitarias. En la anterior entrada, “Kotlin, SpringBoot, Docker y DockerCompose I”, realicé una descripción de una aplicación básica de ejemplo.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La estructura del artículo está compuesto por los siguientes puntos:

  1. Test unitarios de controladores
  2. Test unitarios de servicios
  3. Test unitarios de repositorios

Para la realización de pruebas unitarias es necesario definir un conjunto de dependencias en el fichero de configuración de Gradle build.gradle.kts. Las dependencias necesarias son las siguientes:

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-config")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0")

Para realizar las pruebas unitarias es necesario tener definido el fichero con las propiedades para el entorno de pruebas; en el ejemplo, el contenido necesario del fichero application.properties es el siguiente:

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=
kotlindocker.title=ExampleKotlinDocker
kotlindocker.banner.title=Warning
kotlindocker.banner.content=Kotlin application witth docker.

Las pruebas unitarias consiste en realizar test sobre un componente determinado. Si el componente tiene relaciones de asociación, o bien, usa elementos con una funcionalidad concreta, se puede declarar su resultado para realizar la prueba; estos casos, se emplean objetos mock para dichas pruebas.

1.- Test unitarios de controladores

Para definir una prueba unitaria de un controlador, en nuestro caso AppController, definiremos una clase de Test. La clase se test -AppControllerTest- debe de estar declarada con las siguientes anotaciones: @RunWith, @AutoConfigureMockMvc y @SpringBootTest.

La clase AppControllerTest dede tener definida la inyección de las entidades WebApplicationContext y MockMvc. Además, se definen la declaración de los elementos necesarios para moquear, en nuestro caso, la clase BusinessService mediante la anotación @MockBean.

La definición de un test se realiza definiendo funciones con la anotación @Test. En el cuerpo de la función, se declara el comportamiento del objeto moquedo, se lanza la petición al objeto a probar, en nuestro caso, una petición HTTP al enppoint /business/operation1 con los datos necesarios; y, para finalizar, se realiza el chequeo del resultado con las funciones del objeto MarcherAssert. El snippet del código es el siguiente:

@RunWith(SpringRunner::class)
@AutoConfigureMockMvc
@SpringBootTest
class AppControllerTest {
  @Autowired
  private lateinit var webApplicationContext: WebApplicationContext
  @Autowired
  private lateinit var mockMvc: MockMvc
  @MockBean
  private lateinit var businessService: BusinessService
  @org.junit.Before
  fun setup() {
    mockMvc = MockMvcBuilders
      .webAppContextSetup(webApplicationContext)
      .build()
  }
  @Test
  fun `AppControllerTest operation1`(){
    val responseOperation1 = BusinessServiceResponse(message = "MessageTest")
    whenever(businessService.operationBusiness1( any() )).thenReturn( responseOperation1 )
    val result = mockMvc.perform( MockMvcRequestBuilders.get("/business/operation1").accept(MediaType.APPLICATION_JSON) )
      .andExpect(MockMvcResultMatchers.status().isOk)
      .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
      .andReturn()
    MatcherAssert.assertThat(result.response.status, CoreMatchers.`is`(HttpStatus.OK.value()))
    verify(businessService).operationBusiness1( any() )
  }
}

2.- Test unitarios de servicios

La declaración de las pruebas unitarias de un servicio de negocio, sigue la misma línea que el de un controlador. En el ejemplo, definiré una prueba unitaria de la operación definida en el
servicio de negocio.

La diferencia radica en lo siguiente: los elementos que deben de ser moquedos, se declaran con la anotación @Mock; y, para el elemento a probar, se emplea la anotación @InjectMocks. La definición de la clase de test está definida exclusivamente con la anotación @RunWith(MockitoJUnitRunner::class). El snippet de la clase es el siguiente:

@RunWith(MockitoJUnitRunner::class)
class BusinessServiceTest {
  @Mock
  private lateinit var authorRepository: AuthorRepository
  @InjectMocks
  private lateinit var businessService: BusinessService
  @Test
  fun `operation1`(){
    val authorMock = Author(login = "loginTest", firstname = "firstNameTest", lastname = "lastnameTest")
    whenever(authorRepository.findByLogin( any() )).thenReturn(
      authorMock
    )
    val request = BusinessServiceRequest(login = "Param1")
    val result = businessService.operationBusiness1(request)
    MatcherAssert.assertThat(result.message, CoreMatchers.`is`( "Param1-" + authorMock.login ))
    verify(authorRepository).findByLogin( any() )
  }
}

3.- Test unitarios de repositorios

La definición de las clases de test de un repositorio deben de tener la anotación @DataJpaTest y, como atributo, la definición de inyección de dependecia de los repositorios a probar. En nuestro caso, definiremos la referencia al repositorio ArticleRepository y, por otro lado, la referencia al EntityManager de test. La prueba de una operación se define en una función creando los datos de entrada, consulta al repositorio y verificación del resultado. El snippet con el código de ejemplo es el siguiente:

@DataJpaTest
class ArticleRepositoryTest {
  @Autowired
  private lateinit var entityManager: TestEntityManager
  @Autowired
  private lateinit var articleRepository: ArticleRepository
  @Test
  fun `When findByIdOrNull then return Article`() {
    val juergen = Author("loginUserTest", "firstnameTest", "lastnameTest")
    val juergenInserted = entityManager.persist(juergen)
    assertThat(juergenInserted).isNotNull
    val article = Article("titleTest", "headLineTest", "ContentTest", juergen)
    val articleInserted = entityManager.persist(article)
    assertThat(articleInserted).isNotNull
      entityManager.flush()
    val found = articleRepository.findByIdOrNull(article.id!!)
    assertThat(found).isEqualTo(article)
  }
}

Para el lector interesado en el código del proyeceto puede acceder al siguiente enlace

En la siguiente entrada, “Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose”, describiré como realizar test unitarios de los elementos de la aplicación.

Kotlin, SpringBoot, Docker y DockerCompose I

Inicio una serie de entradas cuya finalidad es la creación de una aplicación sencilla con SpringBoot desarrollada en lenguaje Kotlin, además, pretendo dockerizar la aplicación con Docker y Docker DockerCompose.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La aplicación define un servicio REST basada en un dominio para la gestión de artículos y sus autores. Se define una operación ficticia para integrar los componentes de la arquitectura software.

La estructura del artículo está compuesto por los siguientes puntos:

  1. Definición de dependencias y creación del proyecto
  2. Modelo de entidad
  3. Capa de persistencia
  4. Capa de servicio
  5. Capa controladora
  6. Aplicación
  7. Configuración de base de datos
  8. Arranque

1.-Definición de dependencias y creación del proyecto

La gestión de dependencias del proyecto la realiza Gradle. Las dependencias más importantes son: spring-boot-starter-data-jpa, spring-boot-starter-web, spring-boot-devtools, spring-boot-configuration-processor, h2 y mysql-connector-java

La versión de SpringBoot es la 2.2.7.RELEASE y el dependency-manager es la 1.0.9.RELEASE

2.-Modelo de entidad

La aplicación define dos entidades: Artículos y Autor. Un Autor está compuesto de los siguientes atributos: login, firstname, lastname. Un Artículo está compuesto por los siguientes atributos: title, headline, content, autor, slug y addedAt. Un Artículo está escrito por un Autor y, éste, puede escribir varios Artículos.

El snippet con las definición de las entidades es el siguiente:

@Entity
class Author(
  var login: String,
  var firstname: String,
  var lastname: String,
@Id @GeneratedValue var id: Long? = null)
@Entity
class Article(
  var title: String,
  var headline: String,
  var content: String,
  @ManyToOne var author: Author,
  var slug: String = title.toSlug(),
  var addedAt: LocalDateTime = LocalDateTime.now(),
  @Id @GeneratedValue var id: Long? = null)

La definición de las relaciones entre entidades se utilizan las siguientes anotaciones: @Entity, para definir las clases que definen una entidad; @ManyToOne, para definir las relaciones uno a muchos; y, @Id y @GeneratedValue, para definir un identificador único.

3.- Capa de persistencia

Los componentes de persistencia están basados en el interfaz CrudRepository de Spring JPA. Defino dos repositorios: ArticleRepository, para las operaciones sobre la entidad Article; y,
AuthorRepository, para las operaciones sobre la entidad Author. El snippet con los repositorios es el siguiente:

import com.example.ejem1kotlindocker.entity.Article
import com.example.ejem1kotlindocker.entity.Author
import org.springframework.data.repository.CrudRepository
interface ArticleRepository: CrudRepository<Article, Long> {
  fun findBySlug(slug: String): Article?
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}
interface AuthorRepository: CrudRepository<Author, Long> {
  fun findByLogin(login: String): Author?
}

Las operaciones sobre las entidades son operaciones de búsqueda basadas en los atributos de cada entidad.

4.- Capa de servicio

La capa de servicios es aquella que define los componentes con las operaciones de negocio las cuáles utilizan los repositorios o cualquier otro componente. En el ejemplo, se define un único servicio con una operación y los DTO para la funcionalidad requerida. El snippet del servicio es el siguiente:

@Service
class BusinessService {
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }

  @Autowired
  private lateinit var authorRepository: AuthorRepository

  @Transactional
  fun operationBusiness1(request: BusinessServiceRequest): BusinessServiceResponse {
    logger.info("[**] BusinessService.operation1. Request=${request}")
    val loginRequest = authorRepository.findByLogin(request.login)
    val stringResponse = StringBuilder()
    stringResponse.append(request.login)
    stringResponse.append("-")
    stringResponse.append(loginRequest!!.login)
    val result = BusinessServiceResponse(message = stringResponse.toString())
    logger.info("[**] BusinessService.operation1. result=${result}")
    return result
  }
}
data class BusinessServiceRequest(val login: String)
data class BusinessServiceResponse(val message: String)

En el snippet anterior, se muestra el servicio BusinessService que define una inyección de dependencia con el repositorio AuthorRepository, empleando la anotación @Autowired; se define una función de negocio operationBusiness1 transaccional al emplear la anotación @Transactional; se define un elemento de logger y los DTO de entrada y salida de de la operación de servicio operationBusiness1.

5.- Capa controladora

La capa controladora es aquella capa en la cual definimos los controladores REST de la aplicación. La capa controladora debe de definir una inyección de dependencia del servicio necesario para la ejecución de la operación de negocio. En nuestro caso, se define el controlador AppController con una dependecia al servicio BusinessService.

El snippet del controlador AppController es el siguiente:

import com.example.ejem1kotlindocker.service.BusinessService
import com.example.ejem1kotlindocker.service.BusinessServiceRequest
import com.example.ejem1kotlindocker.service.BusinessServiceResponse
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("business")
class AppController {
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }

  @Autowired
  private lateinit var businessService: BusinessService

  @GetMapping("operation1")
  fun operation1(): BusinessServiceResponse {
    logger.info("[*] AppController.operation1.")
    val request = BusinessServiceRequest("ParamTest")
    return businessService.operationBusiness1(request)
  }
}

El controlador REST queda definido en la clase AppController por las anotaciones @RestController y @RequestMapping con el String “business”. La funcionalidad del servicio de negocio se define con la anotación @GetMapping con la cual definimos una operación REST de tipo GET. Además, la clase define el atributo businessService con la inyección de dependencia utilizando la anotación @Autowired. Para finalizar e igual que el servicio, se utiliza un elemento para escribir trazas de log.

6.- Aplicación

La aplicación SpringBoot queda definida por la clase Ejem1kotlindockerApplication la cual queda definida con la apnotación @SpringBootApplication. La anotación @EnableConfigurationProperties define la relación de la clase con las propiedades de la misma, referenciando a la clase Ejem1kotlindockerProperties. La clase Ejem1kotlindockerProperties realiza la carga de las propiedades del fichero application.properties. El snippet de la clase es la siguiente:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication

@SpringBootApplication
@EnableConfigurationProperties(Ejem1kotlindockerProperties::class)
class Ejem1kotlindockerApplication{}
fun main(args: Array<String>) {
  runApplication<Ejem1kotlindockerApplication>(*args){}
}

7.- Configuración de base de datos

La configuración de base de datos la realizamos en la clase Ejem1kotlindockerConfiguration. La clase define la carga de los datos de configuración desde el fichero application.properties y la definición de inicialización de unos datos iniciales en la base de datos en la función databaseInitializer.

Por otra parte, se definen dos Profile de trabajo: local, para la ejecución de la aplicación con una base de datos MySQL;y, dev, para definir la configuración a una base de datos al trabajar con el docker-compose. El snippet de la clase configuración es el siguiente:

@Configuration
@ConfigurationProperties("spring.datasource")
@SuppressWarnings("unused")
class Ejem1kotlindockerConfiguration{
  companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = LoggerFactory.getLogger(javaClass.enclosingClass)
  }
  @Value("\${spring.datasource.driver-class-name}")
  private lateinit var driverClassName: String
  @Value("\${spring.datasource.url}")
  private lateinit var url: String
  @Value("\${spring.datasource.username}")
  private lateinit var username: String
  @Value("\${spring.datasource.password}")
  private lateinit var password: String
  @Profile("local")
  @Bean
  fun localDatabaseConnection(): String {
    println("DB connection form Local - MySQL")
    logger.info("[*] DriverClassName=${driverClassName}")
    logger.info("[*] URL=${url}")
    logger.info("[*] UserName=${username}")
    return "DB Connection for Local - MySQL"
  }
  @Profile("dev")
  @Bean
  fun devDatabaseConnection(): String {
    println("DB connection form Local - MySQL")
    logger.info("[*] DriverClassName=${driverClassName}")
    logger.info("[*] URL=${url}")
    logger.info("[*] UserName=${username}")
    return "DB Connection for Local - MySQL"
  }
  @Bean
  fun databaseInitializer(authorRepository: AuthorRepository,
    articleRepository: ArticleRepository) = ApplicationRunner {
    val smaldini = authorRepository.save(Author("ParamTest", "AuthorTest", "lastNameTest"))
    articleRepository.save(Article(
      title = "Articule1",
      headline = "Headline1",
      content = "Content1",
      author = smaldini
   ))
   articleRepository.save(Article(
     title = "Articule2",
     headline = "Headline2",
     content = "Content3",
     author = smaldini
   ))
  }
}

Los datos iniciales que se crean en la base de datos corresponden con dos elementos de la entidad Article.

8.- Arranque

Para arrancar la aplicación en un entorno local, es decir utilizando una base de datos H2, se ejecuta el siguiente comando ubicados en
la carpeta principal del proyecto:

./gradlew bootRun

Si queremos ejecutar la aplicación utilizando la configuración existente en el profile local, ejecutamos
el siguiente comando unicados en la carpeta principal del proyecto:

SPRING_PROFILES_ACTIVE=local ./gradlew bootRun

Para verificar el correcto funcionamiento del servicio, ejecutamos el siguiente comando curl:

curl http://localhost:8080/kotlindocker/business/operation1

La salida pos consola es la siguiente:

{"message":"ParamTest-ParamTest"}

 

Para el lector interesado en el código del proyeceto puede acceder al siguiente enlace

En la siguiente entrada, Kotlin, SpringBoot, Docker y DockerCompose II: test unitarios, describiré como realizar test unitarios de los elementos de la aplicación.

Funciones lambda con receptores

Las funciones lambdas son funciones que permiten recibir funciones como parámetros, o bien, retornar una función. Este tipo de función cuya utilización es común en lenguajes con paradigma funcional como son los lenguajes Kotlin, Scala o Haskell. En lengueje Kotlin existe una variante de función lambda la cual permite recibir una referencia al objeto al que se le aplica la función; este tipo de función, se conoce como funciones lambda con receptores.

Para comprender las funciones lambda con receptores, mostraré unos ejemplos de funciones lambda: el primero, es un ejemplo de construcción de una cadena de texto; el segundo, realiza una transformación del tipo de entrada.

Ejemplo 1, función lambda.

Se define una función buildString que recibe como parámetro una función lambda cuyo parámetro de entrada es un elemento de tipo StringBuilder y su salida es un tipo Unit. En el siguiente ejemplo, la función buildString realiza la siguiente funcionalidad: instancia un objeto de la clase StringBuilder identificado como sb, aplicamos la función lambda pasada por parámetros con el objeto sb y, por último, retornamos el String resultante. Para finalizar el ejemplo, se define la invocación de la función y la impresión por consola del resultado.

El snippet del código es el siguiente:

fun buildString(
  builderAction: (StringBuilder) -> Unit
):String{
  val sb = StringBuilder()
  builderAction(sb)
  return sb.toString()
}
val s = buildString {
  it.append("Hello, ")
  it.append("World! ")
}
println("1.- Function Lambda=${s}")

La salida por consola es la siguiente:

1.- Function Lambda=Hello, World!

Ejemplo 2, función lambda

Se define una entidad con nombre Account, se define una función calculate30 la cual recibe como parámetro un elemento de tipo Account y una función lambda que transforma un tipo de entrada Account en otro tipo Account. La función calculate30 realiza las siguientes operaciones: creación de una instancia Account a partir del objeto pasado por parámetro de entrada y, por último, aplicación de la función lambda pasada por parámetro con el objeto Account creado previamente. Para finalizar el ejemplo, se realiza la creación del objeto account1, la invocación a la función calculate30 y la impresión por consola del resultado.

El snippet del código es el siguiente:

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30(
  init: Account,
  func: (Account) -> Account): Account{
    val account = init.copy(amount = 30)
    return func(account)
}
val account1 = Account(id = 1, amount = 0, result = 0, status = "INIT")
val CTE = 3
val fLambda = calculate30(account1){
  it.copy( result = (it.amount * 6) * CTE, status = "END" )
}
println("1.- Account30 (lambda function)=${fLambda}")

La salida por consola es la siguiente:

1.- Account30 (lambda function)=Account(id=1, amount=30, result=540, status=END)

En los ejemplos anteriores, se puede apreciar que la definición de la función lambda se utiliza la palabre reservada “it” para poder trabajar con el objeto con el que se opera.

Funciones lambda con receptores

Las funciones lambda con receptores en Kotlin son como las funciones lambda a diferencia que la función trabaja con las funciones del tipo de entrada. La declaración de la función, se realiza como si operase con el objeto del tipo de entrada, pudiendo usar el operador this.

Ejemplo 1, función lambda con receptor.

Se define una función buildStringReceiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente: creación del objeto s de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

fun buildStringReceiver(
  builderAction: StringBuilder.() -> Unit
): String{
    val s = StringBuilder()
    s.builderAction()
    return s.toString()
}
val r = buildStringReceiver{
  this.append("Hello, ")
  this.append("World! ")
}
println("2.- Function Lambda receiver=${r}")

La salida por consola es la siguiente:

2.- Function Lambda receiver=Hello, World!

Ejemplo 2, función lambda con receptor.

Se define una función calculate30Receiver la cual tiene como parámetro de entrada una función lambda con receptor. La función buildStringReceiver realiza lo siguiente:
creación del objeto de tipo StringBuilder, invocación de la función lambda con el objeto creado y retorno del resultado. Para finalizar, se define la invocación a la función buildStringReceiver con la declaración de la función y la impresión del resultado.

data class Account(
  val id: Int,
  val amount: Int,
  val result: Int,
  val status: String
)
fun calculate30Receiver(
  init: Account,
  func: Account.() -> Account
): Account{
    val account = init.copy(amount = 30)
    return account.func()
}
val fLReceiver = calculate30Receiver(account1){
  this.copy(result = (this.amount * 6) * CTE, status = "END" )
}
println("2.- Account30Receiver(lambda with receiver)=${fLReceiver}")

La salida por consola es la siguiente:

2.- Account30Receiver(lambda with receiver)=Account(id=1, amount=30, result=540, status=END)

Un ejemplo de función lambda con receptor en kotlin pueden ser las funciones apply o use.

Para el lector interesado, el código completo del ejemplo se encuentra en el siguiente enlace

Objetos invocables como funciones en lenguaje Kotlin

En lenguaje Kotlin tenemos la posibilidad de definir una clase la cual funciona como un función; es decir, una vez creado el objeto de la clase, operamos con la instancia como si fuera una función. En la presente entrada, Objetos invocables como funciones en lenguaje Kotlin, mostraré unos ejemplos básicos de uso de esta características de clase.

Los objetos invocables en Kotlin me traen como recuerdo las first class en Scala. Las first class son funciones que se definen como  una variable, un argumento de función, o bien, el resultado de una función. El compilador de Scala, realiza la transformación de la función a una clase transparente al desarrollador. Un ejemplo de first class en Scala es el siguiente:

val multiplyBy2 = (elem: Int) => (elem * 2)

Los ejemplos de objetos invocados que presento en la entrada son tres:

  1. Instanciación de una clase invocable básica.
  2. Instanciación de una clase invovable a partir de un interface.
  3. Definición de un predicado en una clase invocable.

Una clase invocable es aquella clase que define una función específica defina en un método con nombre invoke el cual está definida como un operador. La clase define un constructor y, para la invocación de la función invoke, no es necesario especificar el nombre de la función.

Instancia de una clase invocable básica

En el siguiente ejercicio presento un ejemplo básico. Defino una clase Greeter con un único método invoke que recibe un parámetro; la funcionalidad del método, es la escritura por pantalla del atributo de la clase y el parámetro. El snippet del código es el siguiente:

class Greeter(val greeting: String){
  operator fun invoke(name: String){
    println("Greeting=${greeting} Name=${name}")
  }
}
val obj1 = Greeter("Test1")
obj1("ParamInvoke")

La instancia de la clase Greeter se realiza como cualquier clase típica; pero, la invocación del método, no se especifica sino que se emplea la referencia de la instancia con los parámetros requeridos en la firma del método invoke.

La salida por consola es la siguiente:

Greeting=Test1 Name=ParamInvoke

Instanciación de una clase invovable a partir de un interface

Incrementando el nivel de dificultad, presento una clase SpecialFunction con su método invoke. El método invoke tiene una definición especial porque se utiliza un interface donde se definen los tipos de entrada y de salida de forma genérica; en concreto, se definen dos parámetros de entrada: p1 de tipo X y p2 de tipo Y; y, por último, se define un tipo de salida de tipo Z. El snippet del código es el siguiente:

interface MyFuntion2<in X, in Y, out Z>{
  operator fun invoke(p1:X, p2: Y): Z
}
class SpecialFunction(): MyFuntion2<Int, Int, String>{
  override fun invoke(p1: Int, p2: Int): String {
    return StringBuilder()
      .append(p1)
      .append("+")
      .append(p2)
      .append("=")
      .append((p1+p2).toString())
      .toString()
  }
}
val objectFunctionSum = SpecialFunction()
println("Suma=>${objectFunctionSum(p1 =2,p2=3)}")

De la misma manera que el anterior caso, se realiza la instancia de la clase y, con ésta, realizamos la invocación del método invoke con los parámetros requeridos en su firma.

La salida por consola es la siguiente:

Suma=>2+3=5

Definición de un predicado en una clase invocable

El último caso que presento es una clase que representa un predicado semántico dentro de un contexto funcional. El escenario es el siguiente: dado un sistema que trabaja con proyectos en los cuales se presentan problemas o incidencias representadas por la entidad Issue; la entidad Issue está compuesta por los siguientes campos: un identificador, un nombre de proyecto, un tipo, una prioridad y una descripción del problema. La clase pretende definir lo siguiente: para un proyecto determinado, se quiere determinar si una entidad Issue es de tipo project y si es importante o no. Un elemento Issue es importante si es de tipo tiene valor “BUG” y su prioridad es Critical. El snippet del código es el siguiente:

data class Issue(
  val id:String,
  val project: String,
  val type: String,
  val priority: String,
  val description: String
)
class ImportantIssuesPredicative(val project: String): (Issue) -> Boolean{
  override fun invoke(p1: Issue): Boolean {
    return p1.project == project && p1.isImportant()
  }
  private fun Issue.isImportant(): Boolean{
    return this.type == "BUG" && this.priority == "Critical"
  }
}
val issue1 = Issue(id = "1", project = "project1", type = "PBI", priority = "Medium", description = "description1")
val issue2 = Issue(id = "2", project = "project2", type = "PBI", priority = "High", description = "description1")
val issue3 = Issue(id = "3", project = "project1", type = "BUG", priority = "Critical", description = "description1")
val listIssues = listOf(issue1, issue2, issue3)
val predicate = ImportantIssuesPredicative("project1")
val result = listIssues.filter(predicate)
println("Result->${result}")

La definición y la instancia del predicado se realiza de la misma forma que en los ejemplos anteriores, residiendo la diferencia en la funcionalidad que se añade en el mótodo invoke; en ella, se utiliza una extensión de la clase String definida en la misma clase.

La salida por consola es la siguiente:

Result->[Issue(id=3, project=project1, type=BUG, priority=Critical, description=description1)]

Para finalizar, destacar el parecido con las funciones first class de Scala y la verbosidad de código Kotlin en comparación a Scala.

Al lector interesado, puede acceder al código completo del ejemplo en el siguiente enlace.