====== Programación Orientada a Objetos en Kotlin ====== ===== Clases ===== La **Programación Orientada a Objetos (POO)** es un paradigma de programación basado en la utilización de **objetos**. Un objeto agrupa principalmente: * **Datos**, representados mediante propiedades. * **Comportamiento**, representado mediante funciones. Por ejemplo, si queremos representar un coche podríamos tener: Coche Propiedades: marca modelo velocidad Funciones: acelerar() frenar() mostrarInformacion() Para definir este tipo de objetos utilizaremos **clases**. ==== Clases y objetos ==== Una **clase** es una plantilla que define las características y el comportamiento que tendrán los objetos creados a partir de ella. Por ejemplo: class Coche { var marca = "" var modelo = "" var velocidad = 0 } Esta clase define cómo serán los objetos de tipo `Coche`. Sin embargo, declarar una clase **no crea ningún coche**. Para trabajar con ella debemos crear objetos o **instancias** de la clase. **Clase frente a objeto** Podemos entender una clase como un plano o plantilla y un objeto como una realización concreta de ese plano. Una misma clase puede utilizarse para crear muchos objetos diferentes. ==== Declaración de una clase ==== Para declarar una clase utilizamos la palabra reservada `class` seguida del nombre de la clase: class Persona { } Por convenio, los nombres de las clases comienzan con mayúscula: class Persona class Alumno class Producto class CuentaBancaria Una clase puede estar formada por dos partes: * **Cabecera de la clase**. * **Cuerpo de la clase**. Por ejemplo: class Persona(val nombre: String) { ``` var edad = 0 fun saludar() { println("Hola, soy $nombre") } ``` } La cabecera es: class Persona(val nombre: String) El cuerpo es todo lo que aparece entre `{ }`: { var edad = 0 ``` fun saludar() { println("Hola, soy $nombre") } ``` } El cuerpo puede contener, entre otros elementos: * Propiedades. * Funciones. * Bloques de inicialización. * Constructores secundarios. * Otras clases u objetos. Si una clase no necesita cuerpo podemos omitir las llaves: class Persona(val nombre: String, var edad: Int) ==== Crear objetos ==== Para crear un objeto escribimos el nombre de la clase seguido de paréntesis: val persona = Persona("Ana", 20) A esta operación la denominamos **instanciar una clase**. `persona` es ahora un objeto o instancia de `Persona`. A diferencia de otros lenguajes orientados a objetos como Java, en Kotlin **no se utiliza la palabra `new`** para crear objetos. Por ejemplo: // Kotlin val persona = Persona("Ana", 20) No escribimos: // Incorrecto en Kotlin // val persona = new Persona("Ana", 20) Podemos crear múltiples objetos de una misma clase: val persona1 = Persona("Ana", 20) val persona2 = Persona("Carlos", 25) val persona3 = Persona("Marta", 19) Todos son objetos diferentes, aunque hayan sido creados utilizando la misma clase. ==== Propiedades ==== Las variables que forman parte de una clase reciben normalmente el nombre de **propiedades**. Por ejemplo: class Coche { var marca = "" var modelo = "" var velocidad = 0 } La clase `Coche` tiene tres propiedades: marca modelo velocidad Podemos acceder a ellas utilizando el operador `.`: val coche = Coche() coche.marca = "Toyota" coche.modelo = "Corolla" coche.velocidad = 50 println(coche.marca) println(coche.modelo) println(coche.velocidad) ==== Propiedades val y var ==== Al igual que las variables normales, las propiedades pueden declararse utilizando `val` o `var`. Por ejemplo: class Persona( val nombre: String, var edad: Int ) En este caso: * `nombre` es una propiedad que no puede reasignarse. * `edad` es una propiedad que puede modificarse. val persona = Persona("Ana", 20) persona.edad = 21 Esto es correcto. Sin embargo: // ERROR // persona.nombre = "Marta" no está permitido porque `nombre` fue declarado mediante `val`. Al igual que con las variables normales, es recomendable utilizar `val` cuando una propiedad no necesite ser reasignada. ==== Funciones de una clase ==== Las clases también pueden contener funciones. Estas funciones representan el **comportamiento de los objetos**. Por ejemplo: class Coche( val marca: String, val modelo: String, var velocidad: Int ) { ``` fun acelerar() { velocidad += 10 } fun frenar() { velocidad -= 10 } fun mostrarInformacion() { println("$marca $modelo - $velocidad km/h") } ``` } Podemos utilizar estas funciones mediante el operador `.`: fun main() { ``` val coche = Coche( "Toyota", "Corolla", 0 ) coche.acelerar() coche.acelerar() coche.mostrarInformacion() ``` } Resultado: Toyota Corolla - 20 km/h Las funciones pueden acceder directamente a las propiedades de su objeto: fun acelerar() { velocidad += 10 } No necesitamos pasar `velocidad` como parámetro porque pertenece al propio objeto. ==== Constructor primario ==== Cuando creamos un objeto normalmente queremos proporcionar sus datos iniciales. Para ello utilizamos un **constructor**. La forma más habitual en Kotlin es utilizar un **constructor primario**, que se declara directamente en la cabecera de la clase: class Persona(val nombre: String, var edad: Int) { } Ahora debemos proporcionar esos valores al crear el objeto: val persona = Persona("Ana", 20) Podemos acceder posteriormente a ellos: println(persona.nombre) println(persona.edad) Resultado: Ana 20 La sintaxis completa del constructor podría escribirse utilizando la palabra `constructor`: class Persona constructor(val nombre: String, var edad: Int) { } Sin embargo, normalmente podemos omitirla: class Persona(val nombre: String, var edad: Int) { } Esta es la forma que utilizaremos habitualmente. ==== Parámetros y propiedades del constructor ==== Existe una diferencia importante entre escribir: class Persona(nombre: String) y: class Persona(val nombre: String) En el segundo caso `nombre` se convierte en una **propiedad del objeto**. Por ello podemos hacer: val persona = Persona("Ana") println(persona.nombre) Sin `val` o `var`, el parámetro del constructor no se convierte automáticamente en una propiedad accesible del objeto. Por tanto: class Persona( val nombre: String, var edad: Int ) es una forma muy habitual de declarar simultáneamente: * Los parámetros necesarios para crear el objeto. * Las propiedades que almacenará. ==== Valores por defecto ==== Los parámetros del constructor pueden tener valores por defecto: class Persona( val nombre: String = "Desconocido", var edad: Int = 0 ) Ahora podemos crear objetos de diferentes maneras: val persona1 = Persona() val persona2 = Persona("Ana") val persona3 = Persona("Carlos", 25) Los valores serán: persona1 -> nombre = "Desconocido", edad = 0 persona2 -> nombre = "Ana", edad = 0 persona3 -> nombre = "Carlos", edad = 25 También podemos utilizar argumentos con nombre: val persona = Persona( edad = 25, nombre = "Carlos" ) ==== Propiedades adicionales ==== No todas las propiedades tienen que declararse en el constructor. Podemos declarar otras propiedades dentro del cuerpo de la clase: class Persona( val nombre: String, val anioNacimiento: Int ) { ``` val mayoriaEdad = 18 var activo = true ``` } También podemos calcular una propiedad utilizando los datos recibidos por el constructor: class Rectangulo( val base: Double, val altura: Double ) { ``` val area = base * altura ``` } Uso: val rectangulo = Rectangulo(10.0, 5.0) println(rectangulo.area) Resultado: 50.0 ==== Bloque init ==== En ocasiones necesitamos ejecutar instrucciones cuando se crea un objeto. Para ello podemos utilizar un bloque `init`. class Persona( val nombre: String, var edad: Int ) { ``` init { println("Creando una nueva persona") } ``` } Cuando hacemos: val persona = Persona("Ana", 20) se ejecutará automáticamente el bloque `init`. Podemos acceder desde él a los parámetros y propiedades: class Persona( val nombre: String, var edad: Int ) { ``` init { println("Creando a $nombre") println("Edad inicial: $edad") } ``` } ==== Validación durante la inicialización ==== Uno de los usos del bloque `init` es comprobar que los datos utilizados para crear un objeto sean correctos. Por ejemplo: class Persona( val nombre: String, var edad: Int ) { ``` init { require(edad >= 0) { "La edad no puede ser negativa" } } ``` } Esto sería válido: val persona = Persona("Ana", 20) Pero: val persona = Persona("Ana", -5) provocaría un error porque no se cumple la condición establecida mediante `require()`. Otro ejemplo: class Producto( val nombre: String, val precio: Double ) { ``` init { require(nombre.isNotBlank()) { "El nombre no puede estar vacío" } require(precio >= 0) { "El precio no puede ser negativo" } } ``` } `require()` permite indicar una condición que debe cumplirse. Si la condición es falsa, Kotlin lanza una excepción indicando que el argumento proporcionado no es válido. No es necesario profundizar todavía en las excepciones para poder utilizar esta validación básica. ==== Constructores secundarios ==== Una clase puede tener además **constructores secundarios**. Se declaran mediante `constructor` dentro del cuerpo de la clase. Por ejemplo: class Persona( val nombre: String, var edad: Int ) { ``` constructor(nombre: String) : this(nombre, 0) ``` } Ahora podemos crear: val persona1 = Persona("Ana", 20) val persona2 = Persona("Carlos") En el segundo caso se utiliza el constructor secundario: constructor(nombre: String) : this(nombre, 0) que llama al constructor primario proporcionando: nombre -> valor recibido edad -> 0 Si existe un constructor primario, los constructores secundarios deben terminar delegando en él mediante `this(...)`. No obstante, Kotlin permite solucionar muchas situaciones mediante **valores por defecto**, por lo que no necesitaremos constructores secundarios con tanta frecuencia como en otros lenguajes. Por ejemplo, en el caso anterior probablemente sea más sencillo escribir: class Persona( val nombre: String, var edad: Int = 0 ) ===== Ejemplo completo: clase Producto ===== Vamos a representar un producto de una tienda. Cada producto tendrá: * Nombre. * Precio. * Stock. Además podrá: * Mostrar su información. * Vender unidades. * Reponer unidades. * Calcular el valor total del stock. class Producto( val nombre: String, val precio: Double, var stock: Int = 0 ) { ``` init { require(nombre.isNotBlank()) { "El nombre no puede estar vacío" } require(precio >= 0) { "El precio no puede ser negativo" } require(stock >= 0) { "El stock no puede ser negativo" } } fun vender(cantidad: Int) { if (cantidad <= 0) { println("La cantidad debe ser positiva") } else if (cantidad > stock) { println("No existe stock suficiente") } else { stock -= cantidad println("Venta realizada") } } fun reponer(cantidad: Int) { if (cantidad > 0) { stock += cantidad } } fun calcularValorStock(): Double { return precio * stock } fun mostrarInformacion() { println("Producto: $nombre") println("Precio: $precio €") println("Stock: $stock") println("Valor del stock: ${calcularValorStock()} €") } ``` } Podemos utilizarla: fun main() { ``` val teclado = Producto( nombre = "Teclado mecánico", precio = 59.95, stock = 10 ) teclado.mostrarInformacion() teclado.vender(3) teclado.reponer(5) teclado.mostrarInformacion() ``` } En este ejemplo podemos identificar claramente los dos elementos principales de un objeto. **Estado del objeto:** nombre precio stock **Comportamiento del objeto:** vender() reponer() calcularValorStock() mostrarInformacion() ===== Referencias entre objetos ===== Una variable que contiene un objeto contiene una **referencia a ese objeto**. Por ejemplo: val producto1 = Producto( "Teclado", 50.0, 10 ) val producto2 = producto1 En este caso no hemos creado dos productos independientes. `producto1` y `producto2` hacen referencia al **mismo objeto**. Por tanto: producto2.stock = 5 println(producto1.stock) mostrará: 5 Esto es diferente de lo que ocurre con tipos sencillos como `Int`: var numero1 = 10 var numero2 = numero1 numero2 = 20 println(numero1) El resultado continúa siendo: 10 Comprender que los objetos se manejan mediante referencias será importante posteriormente cuando trabajemos con objetos, colecciones y aplicaciones Android. ===== Ejercicio — Gestión de vehículos ===== Crea una clase denominada: Vehiculo Cada vehículo debe almacenar: * Matrícula. * Marca. * Modelo. * Kilómetros. * Nivel de combustible. * Capacidad máxima del depósito. Por ejemplo: val coche = Vehiculo( matricula = "1234 ABC", marca = "Toyota", modelo = "Corolla", kilometros = 50000, combustible = 30.0, capacidadDeposito = 50.0 ) La clase debe proporcionar las siguientes funciones: mostrarInformacion() repostar(litros) realizarViaje(kilometros, consumo) La función `repostar()` debe comprobar: * Que la cantidad sea positiva. * Que el depósito no supere su capacidad máxima. Por ejemplo, si quedan 40 litros en un depósito de 50 y hacemos: coche.repostar(20.0) el depósito deberá quedar en: 50 litros La función: realizarViaje(kilometros, consumo) recibirá: * Distancia que se quiere recorrer. * Consumo del vehículo en litros cada 100 km. Por ejemplo: coche.realizarViaje( kilometros = 200, consumo = 6.5 ) El programa deberá calcular el combustible necesario: combustibleNecesario = kilometros * consumo / 100 Si existe combustible suficiente: * Restar el combustible consumido. * Aumentar el kilometraje del vehículo. * Mostrar un mensaje indicando que el viaje se ha realizado. Si no existe combustible suficiente: * No modificar el kilometraje. * No modificar el combustible. * Mostrar un mensaje indicando que no existe combustible suficiente. Además, utiliza un bloque `init` para comprobar que: * Los kilómetros iniciales no sean negativos. * El combustible no sea negativo. * La capacidad del depósito sea mayor que cero. Finalmente, crea al menos **tres vehículos diferentes** y realiza distintas operaciones sobre ellos. ===== Ejercicio de ampliación ===== Crea una clase `Alumno` que almacene: nombre apellidos edad notas Las notas pueden almacenarse en: val notas = mutableListOf() La clase deberá proporcionar las funciones: agregarNota(nota) calcularMedia() estaAprobado() mostrarInformacion() `agregarNota()` solamente deberá aceptar valores entre `0` y `10`. `calcularMedia()` devolverá la media de las notas del alumno. `estaAprobado()` devolverá un `Boolean` indicando si la media es igual o superior a `5`. `mostrarInformacion()` deberá producir una salida similar a: Alumno: Ana García Edad: 20 Notas: 7.5, 8.0, 6.5, 9.0 Media: 7.75 Resultado: APROBADO Crea posteriormente varios objetos `Alumno`, añade diferentes notas y muestra sus resultados. ---- (c) {{date> %Y}} Fernando Valdeón