Skip to content

Programación Orientada a Objetos (POO)

La POO es un paradigma de programación, el cual nos genera el concepto de como visualizar el mundo real y abstraerlo al código de programación y aplicarlo al software.

Note

Una clase es conocida como el esqueleto o plantilla de un objeto, lo que define qué contiene un clase y cómo lo va hacer.

Note

Cuando creamos un objeto a partir de una clase, se dice instancia.

Note

En Dart todo es un Objeto, eso lo primero que nos tiene que quedar muy claro.

Características de las clases

  • Puede contener campos y métodos.
  • En un archivo se puede contener mas de una clase
  • En archivo pueden existir clases y funciones
  • Lo ideal es por archivo existan solo las clases necesarias
  • Las clases tienen campos (atributos) y métodos (comportamientos)
  • Puede contener campos y métodos privados
  • Pueden existir campos y métodos estáticos.
  • Por default los campos y métodos son públicos.

Definiendo Clases

class NombreClase{
    // cuerpo de la clase
}

Campos

  • Los campos son las propiedades definidas en un clase.
  • Estos por buenas practicas se colocan al principio de la clase.
  • Estas propiedades equivalente a tener variables o constantes globales de la clase.
  • Estas quedan dentro del scope de la propia clase, para que sean manipuladas por los métodos, en caso que lo requiera.
  • Los campos globales de una clase pueden ser accedidos por otras clases o funciones que estén en el mismo archivo.
class NombreClase{

    // CAMPOS
    int     campo1 = 3;
    String  campo2 = "hola";

}

Métodos

Conocimos el termino de función; sin embargo, cuando hablamos de una clase se dice método.

  • Es un comportamiento (acción) que realiza un objeto (cosa).
  • Es un bloque o secuencia de código que se repite continuamente.
  • Hace una sola tarea, y lo hace muy bien.
  • Su nombre se define con un verbo (acción).
  • Funciones de un objeto.
  • Modifica estados.
class NombreClase{
    // MÉTODOS
    /// Definición de un método
    String metodo1(){
        return "";
    }

    int? metodo2(double arg1){
        return null;
    }
}

Integrando una clase

Hemos visto las partes básicas de una clase, vamos a crear una completa de ejemplo. Una clase por si sola no hace nada, se debe crear una instancia y usar sus propiedades.

class Persona {
  int dedos = 10;
  String nombre = "Mario";

  void correr() {
    print("Corro!!!!");
  }

  void dormir(int horas) {
    print("Duermo por $horas horas");
  }
}

void main() {
  Persona persona1 = Persona(); // Creamos una instancia de la clase Persona

  print(persona1.dedos);
  print(persona1.nombre);

  persona1.correr();
  persona1.dormir(28);
}

Encapsulamiento (private)

Dentro de los conceptos básicos de la POO, tenemos un termino llamado encapsulamiento, ya vimos que cuando definimos campos y métodos, y creamos una instancia, podemos acceder a todos ellos; sin embargo, normalmente creamos métodos o campos que solo queremos usar dentro de la clase, no exponerlos para que nadie los use. Para esto existe una forma para convertirlos en privado, esto quiere decir que solo pueden ser accedidos solo por miembros de la propia clase.

Note

Esto lo logramos anteponiendo en el nombre un guion bajo (_)

class Estudiante {
  String _nombre = "";
  String _especialidad = "";

  void asignarNombre(String nombre) {
    _nombre = nombre;
  }

  String obtenerNombre() {
    return _nombre;
  }

  void asignarEspecialidad(String especialidad) {
    _especialidad = especialidad;
  }

  String obtenerEspecialidad() {
    return _especialidad;
  }
}

void main() {
  final estudiante = Estudiante();// se crea una instancia final, porque esta no cambiara

  estudiante.asignarNombre("Carlos");
  estudiante.asignarEspecialidad("Sistemas");
  print(estudiante.obtenerNombre());
  print(estudiante.obtenerEspecialidad());

}

Constructor

La función o método constructor es lo primero que se manda a llamar cuando creamos un instancia. El constructor se debe llamar igual que la clase.

Constructor vació

Por default se crea el constructor vació; es decir, cuando creamos la clase lo escribamos o no, ahi esta. Por eso cuando creamos la instancia se tienen unos paréntesis vacíos.

class Persona {
  Persona() {   // método constructor
    print("El constructor es lo primero que se ejecuta");
  }
}

void main(List<String> args) {
  Persona();
}

Constructor con argumentos

En muchas ocasiones necesitamos que al crear una instancia de un objeto, a este se le deban dar valores por default, para que el objeto realice lo que deba hacer, antes de mandar a llamar a cualquier método o campo de la clase.

class Persona {
  String _nombre = "";

  Persona(String nombre) {
    _nombre = nombre;
  }

  void saludo() {
    print("Hola me llamo $_nombre");
  }
}

void main(List<String> args) {
  final p1 = Persona("Carlos");
  final p2 = Persona("Mario");
  final p3 = Persona("Ana");

  p1.saludo();
  p2.saludo();
  p3.saludo();
}

this

La palabra reservada this nos ayuda a hacer referencia a la propia clase, sus campos y métodos de la misma. Este lo usamos mayormente en el constructor por el modo en podemos llamar los campos para inicializados y el objeto pueda tener la información necesaria cuando se genere una instancia. También lo hacemos cuando la variable local de un método se llama igual al campo de la clase.

class Persona {
  String nombre = "";
  int edad = 0;

  Persona(String nombre, int edad) {
    this.nombre = nombre;
    this.edad = edad;
  }

  void saludo() {
    print("Hola me llamo $nombre");
  }

  void decirEdad() {
    print("La edad de ${this.nombre} es ${this.edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona("Carlos", 29);
  final p2 = Persona("Mario", 32);
  final p3 = Persona("Ana", 12);

  p1.saludo();
  p1.decirEdad();
  p2.saludo();
  p2.decirEdad();
  p3.saludo();
  p3.decirEdad();
}

Constructor con parámetros nombrados

Al final el método constructor es un método; por ende, de igual manera que cualquier función puede recibir argumentos nombrados. De la misma forma que lo hacemos en las funciones, podemos tener parámetros nombrados requeridos y con valores po default.

class Persona {
  String nombre = "";
  int edad = 0;

  Persona({required String nombre, int edad = 0}) {
    this.nombre = nombre;
    this.edad = edad;
  }

  void saludo() {
    print("Hola me llamo $nombre");
  }

  void decirEdad() {
    print("La edad de ${this.nombre} es ${this.edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona(nombre: "Carlos", edad: 23);

  p1.saludo();
  p1.decirEdad();
}

Constructor reducido

En ocasiones solo se usa el constructor solo para inicializar los campos de la clase, sin ninguna otra acción; dado esto, con Dart nos da una manera mas corta de hacer.

En este primer ejemplo los campos son privados.

class Persona {
  String _nombre = "";
  int _edad = 0;

  Persona(String nombre, int edad) : _nombre = nombre, _edad = edad;

  void saludo() {
    print("Hola me llamo $_nombre");
  }

  void decirEdad() {
    print("La edad de ${this._nombre} es ${this._edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona("Mario", 22);

  p1.saludo();
  p1.decirEdad();
}

En este ejemplo los campos son públicos.

class Persona {
  String nombre;
  int edad;

  Persona(this.nombre, this.edad);

  void saludo() {
    print("Hola me llamo $nombre");
  }

  void decirEdad() {
    print("La edad de ${this.nombre} es ${this.edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona("Mario", 22);

  p1.saludo();
  p1.decirEdad();
}

En este ejemplo tenemos parámetros nombrados, y como queremos que se inicialicen cuando se genere la instancia, los colocamos como requeridos.

class Persona {
  String nombre;
  int edad;

  Persona({required this.nombre, required this.edad});

  void saludo() {
    print("Hola me llamo $nombre");
  }

  void decirEdad() {
    print("La edad de ${this.nombre} es ${this.edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona(nombre: "Mario", edad: 22);

  p1.saludo();
  p1.decirEdad();
}

Constructor nombrado

Dart tiene una característica curiosa con respecto a constructores, podemos personalizar el constructor con otro nombre, al final la finalidad es la misma, ser lo primero que se llama al crear una instancia, pero con nombre diferente.

class Persona {
  String nombre;
  int edad;

  // constructor "normal"
  Persona({required this.nombre, required this.edad});

  // Este constructor no recibe parámetros, pero se inicializan, no tiene cuerpo pero se pudiera hacer de la misma forma.
  Persona.anonima() : nombre = "anónimo", edad = 0;

  // aquí mando a llamar al constructor con parámetros y paso los datos por default
  // con this puedo llamar cualquier constructor
  Persona.joven() : this(nombre: "joven", edad: 15);

  void saludo() {
    print("Hola me llamo $nombre");
  }

  void decirEdad() {
    print("La edad de ${this.nombre} es ${this.edad}");
  }
}

void main(List<String> args) {
  final p1 = Persona(nombre: "Mario", edad: 22);
  final p2 = Persona.anonima();
  final p3 = Persona.joven();

  p1.saludo();
  p1.decirEdad();
  p2.saludo();
  p2.decirEdad();
  p3.saludo();
  p3.decirEdad();
}

Getters y Setters

De los lenguajes programados a objetos, por ejemplo JAVA, surgió una buena practica para la manipulación del estado de los campos de una clase, llamados setter y getters, como el nombre lo indica eran para asignar un dato o para obtenerlo respectivamente. Aquí se mantiene ese mismo concepto pero, con algunos cambios.

Manera tradicional, ya no se usa en Dart

class Auto {
  int _velocidad = 0; //campo privado, que se cambia su valor cuando se crea una instancia

  Auto(int velocidad){
    this._velocidad = velocidad;
  }

  void setVelocidad(int velocidad) {
    this._velocidad = velocidad;
  }

  int getVelocidad() {
    return this._velocidad;
  }
}

void main(List<String> args) {
  final Auto auto1 = Auto(100);

  auto1.setVelocidad(50); // asigno el valor

  int velocidad = auto1.getVelocidad(); // obteniendo el valor del campo

  print("La velocidad es $velocidad km/h");
}

Si vamos a necesitar generar un método set y uno get básico, ya no es necesario crearlos, simplemente se hace el campo publico y ya. Siempre y cuando solo sea modificar y obtener el valor, como en el ejemplo tradicional.

class Auto {

  int velocidad = 0;

}

void main(List<String> args) {
  final Auto auto1 = Auto();
  auto1.velocidad = 100; // asigno el valor es equivalente a un set, asignando o cambiando el estado del campo

  int velocidad =
      auto1.velocidad; // equivalente a un get, obteniendo el valor del campo

  print("La velocidad es $velocidad km/h");
}

Sin embargo, si queremos continuar como la manera tradicional, Dart no da una nueva forma con palabras reservadas específicamente para este propósito set y get. No es estrictamente necesario usar ambos.

Características set

  • Se usa la palabra reservada set
  • Se declara como una función
  • Por default set es como función void
  • Podemos usar una función flecha o una función normal con cuerpo

Características get

  • Se usa la palabra reservada get
  • Se declara como una función, y se antepone el tipo que retorna
  • Por default get debe retornar algo
  • Se usa función flecha, dado que solo retornar un valor o podemos hacer algún cambio en una acción
class Auto {
  //campo privado, que se cambia su valor cuando se crea una instancia
  int _velocidad = 0;
  String _nombre = "";
  int _modelo = 0;

  Auto(String nombre, int velocidad, {required int modelo}) {
    this._velocidad = velocidad;
    this._nombre = nombre;
    _modelo = modelo;
  }

  set velocidad(int velocidad) =>
      this._velocidad = velocidad; //podemos usar una función flecha

  int get velocidad =>
      this._velocidad; //usamos una función flecha, no se usan paréntesis

  /**
  * Si necesitamos modificar el valor recibido, en el cuerpo de la función lo podemos hacer
  */
  set nombre(String nombre) {
    String nuevoNombre = "Un super $nombre";
    _nombre = nuevoNombre;
  }

  String get nombre => _nombre;

  // como el campo de modelo no quiero que sea modificado nunca, solo se puede obtener su valor de cuando se crea la instancia
  int get modelo => _modelo;
}

void main(List<String> args) {
  final Auto auto1 = Auto("", 0, modelo: 2004);

  auto1.velocidad = 60; // asigno el valor
  int velocidad = auto1.velocidad; // obteniendo el valor del campo
  auto1.nombre = "Ferrari";
  String nombreAuto = auto1.nombre;

  print("Este auto es un $nombreAuto");
  print("La velocidad es $velocidad km/h");
  print("Es modelo es ${auto1.modelo}");
}

late

Dart nos da un palabra reservada para inicializar campos un poco después de la inicializaron de una instancia. Esto nos da ciertas libertades y nos ayuda a hacer los campos final. Esto también nos permite usar variables que son non-null pero, sin hacer la inicialización, pues late permite esta acción. late es como un contrato con Dart de decir, "te prometo que cuando lo llame no sera nulo". También se le pueden llamar que son campos con inicialización tardía. Esto nos ayuda a que si la instancia es muy pesada o tarda mucho tiempo, no haga lento el arranque de la inicialización de la clase que lo contiene.

!!! warning Ten cuidad con late Si mandamos a llamar una instancia declarada como late y esta sigue siendo null, la culpa es nuestra. Por eso Dart es non-null por default.

Sintaxis de late

late tipo nombreVariable; // no se asigna nada porque posteriormente se le asigna un valor

En este ejemplo no tenemos el peligro de caer en un caso nulo.

class Persona {
  late final String _nombre;
  late final int _edad;

  // Al tener los campos como late, no me obliga darles valores por default en el constructor
  Persona(String nombre, {required int edad})
      : _nombre = nombre,
        _edad = edad;

  String get nombre => _nombre;

  int get edad => _edad;
}

void main(List<String> args) {
  final Persona persona = Persona("Carlos", edad: 10);
  print("${persona.nombre} tiene ${persona.edad} primaveras");
}

static (Pendiente)

La palabra reservada static para crear campos y métodos dentro de una clase; que pueden ser llamadas sin la necesidad de crear una instancia de una clase.

Estructura para usar static, deben estar dentro de una clase:

// field
static type nameVariable = value;

// function method

static type nameFunction(){
    // body of methods
}
class Math {
  static double PI_SHORT = 3.1416;

  static String cutNumber(num number) {
    return number.toStringAsFixed(2);
  }
}

void main(List<String> args) {
  print(Math.PI_SHORT);
  print(Math.cutNumber(25.3636464));
}

Ejemplos

  • Crear una clase Auto:
    • Campos:
    • noPuertas:int
    • color:String
    • Métodos:
    • acelerar:void
    • arrancar:void
    • cambioColor(String):void

Ejercicios

  • Crear clase Motor
    • campos:
      • voltaje:double
      • tipo:String (DC, AC, etc.)
    • Comportamientos
      • arrancar:void -> mensaje que gira
      • cambioGiro(int giro):void -> mandar mensaje de hacia donde esta girando, en función del giro que pasaron. giro = 1, derecha. giro = 2, izquierda
      • apagar:void -> mensaje de se apago el motor
      • cambiarVoltaje(double voltajeNuevo):double -> retorna el voltaje actual, si el voltaje es 0, se manda a llamar la función apagar.
  • Probar su clase en un método main
  • Crear la clase SensorTemperatura:
    • campos
      • codigoSensor:String
      • voltajeAlimentacion:double
    • comportamientos
      • obtenerTemperatura([String]):double -> devuelve un valor random (double), entre 0 a 50°C. Recibe si quiere el valor en la función es para si retorna el centígrados o Fahrenheit, por default devuelve grados centígrados
      • obtenerNombreSensor:String -> devuelve el nombre del sensor
      • cambiarVoltaje(double voltajeNuevo):void -> Si pasan un voltaje superior a 5 o inferior a 0, lanza un mensaje que diga: "Sensor quemado", en caso que este en el rango correcto, dice "sensor funcionando correctamente"
    • Probar su clase en un método main

Mas información en https://dart.dev/language/classes