imagen

Programación Orientada a Objetos en TS

Ejemplo de clases provistas por JS

// Clases provistas por JS
const date = new Date();

// Usando métodos de la clase
date.getHours();
date.getTime();
date.toISOString();

// Creando una instanciona usando argumentos
const date2 = new Date(1993, 1, 12); // 0 enero 11 dic

Creando tus clases

export class MyDate {
  // Atributos públicos
  year: number;
  month: number;
  day: number;

  // Atributos privados
  private counter: number;

  // Inicializando valores desde el constructor
  constructor(year: number, month: number, day: number) {
    this.year = year;
    this.month = month;
    this.day = day;
  }

  // Método público
  printFormat(): string {
    const day = this.addPadding(this.day);
    const month = this.addPadding(this.month);
    return `${day}/${month}/${this.year}`;
  }

  // Método público que nos permite agregar tiempo
  add(amount: number, type: 'days' | 'months' | 'years') {
    if (type === 'days') {
      this.day += amount;
    }
    if (type === 'months') {
      this.month += amount;
    }
    if (type === 'years') {
      this.year += amount;
    }
  }

  // Un atributo private no puede ser leido ni modificado desde 
  // el exterior, pero a veces necesitamos que se pueda leer y no modificar
  // para este caso podemo crear un método publico que nos permita exponer
  // el valor de la variable privada counter
  getCounter() {
    return this.counter;
  }

  // Método Privado
  private addPadding(value: number) {
    if (value < 10) {
      return `0${value}`;
    }
    return `${value}`;
  }
}

const myDate = new MyDate(2021, 3, 13);
console.log(myDate);
console.log(myDate.printFormat());

myDate.add(3, 'days');

Costructor y atributos de forma abreviada

Podemos definir los atributos de la clase al mismo tiempo que definimos el constructor

// Forma Original
export class MyDate {
  // Atributos públicos
  year: number;
  month: number;
  private day: number;

  // Atributos privados
  private counter: number;

  // Inicializando valores desde el constructor
  constructor(year: number, month: number, day: number) {
    this.year = year;
    this.month = month;
    this.day = day;
  }
}

// Forma Abreviada 
export class MyDate {
  constructor(
    // se debe indicar public o private siempre
    public year: number = 1993,
    public month: number = 7,
    private day: number = 9
  ) {}
}

Getters

Nos permiten obtener datos, cuando solo necesitamos "leer" los valores de los atributos, no es necesario definir getters para cada atributo, pero en algunos casos se puede requerir realizar algunas acciones antes de retornar el valor, y es ahi donde los getters cobran especial importancia.

export class MyDate {

  //... class definition


  // Definiendo el getter
  get day() {
    // code
    return this._day;
  }

  //  Definiendo un método
  get isLeapYear(): boolean {
    if (this.year % 400 === 0) return true;
    if (this.year % 100 === 0) return false;
    return this.year % 4 === 0;
  }
}

// Usamos nuestros getters 
const myDate = new MyDate(2001, 7, 10);
console.log(myDate.day);
console.log(myDate.isLeapYear);

Setters

No permite definir valores de los atributos, cuando el proceso de setear esos valores es solo la asignación, no se justifica el uso de setters, pero al igual que los getters, a veces se requieres implementar algunas condiciones extras antes de almacenar el valor (como el ejemplo de set month mas abajo) y ahi los setters nos ayudan.

export class MyDate {

  //... class definition

  // Getter del atributo
  get month() {
    return this._month;
  }

  // Setter del atributo
  set month(newValue: number) {
    if (newValue >= 1 && newValue <= 12) {
      this._month = newValue;
    } else {
      throw new Error('month out of range');
    }
  }
}

const myDate = new MyDate(1993, 7, 10);
console.log(myDate.printFormat());
myDate.month = 4;
console.log('run', myDate.month);
myDate.month = 78;
console.log('esto no debe aparecer', myDate.month);

Herencia

// Definicmos clase base
export class Animal {
  constructor(public name: string) {}

  move() {
    console.log('Moving along!');
  }

  greeting() {
    return `Hello, I'm ${this.name}`;
  }
}

export class Dog extends Animal {

  constructor(
    // Recibes el atributo de la clase padre
    name: string,
    // Defines nuevos atributos propios de esta clase
    public owner: string
  ) {
    // Le pasamos los atributos al constructor del padre (heredado)
    super(name);
  }

  // Definimos un método exclusivo de esta clase
  woof(times: number): void {
    for (let index = 0; index < times; index++) {
      console.log('woof!');
    }
  }
}

const fifi = new Animal('fifi');
fifi.move();
console.log(fifi.greeting());

const cheis = new Dog('cheis', 'nico');
cheis.move();
console.log(cheis.greeting());
cheis.woof(5);
console.log(cheis.owner);

Atributos y métodos protegidos

Hasta el momento tenemos métodos public y private que nos permite definir el acceso de un atributo o método a nivel de clase y un método o atributo private no puede ser usado desde sus clases hijas, pero en el caso que se requiera tener un método o atributo que solo pueda ser leído o modificado de forma interna (tal como los privados) pero tanto por su clase padre como sus clases hijas, podemos definirlos como protected.

export class Animal {
  // Definido como protegido para que sea leido por sus hijos
  constructor(protected name: string) {}

  move() {
    console.log('Moving along!');
  }

  greeting() {
    // Podemos acceder a los atributos protected
    return `Hello, I'm ${this.name}`;
  }

  // Podemos crear un método que se pueda acceder de forma interna por los hijos
  protected doSomething() {
    console.log('dooo');
  }
}

export class Dog extends Animal {

  constructor(
    name: string,
    public owner: string
  ) {
    super(name);
  }

  woof(times: number): void {
    for (let index = 0; index < times; index++) {
      // el name es atributo protected del padre y lo podemos usar en el hijo
      // Pero no fuera de la instancia
      console.log(`woof! ${this.name}`);
    }
    // El método lo podemos utilizar aca dado que es protected
    // pero no puede ser usado desde afuera de la instancia
    this.doSomething();
  }

  // Aca podemos sobre escribir un método del padre
  move() {
    // code
    console.log('moving as a dog');
    // Si sólo queremos agregar funcionalidad y además mantener
    // el comportamiento definido por el padre, basta con usar el super
    super.move();
  }
}

const cheis = new Dog('cheis', 'nico');
// no podemos usar name desde afura, por que es protected
// cheis.name = 'otro nombre';
cheis.woof(1);
// cheis.doSomething();
cheis.move();

Atributos y métodos estáticos

En algunos casos queremos que nuestras clases tengan atributos o métodos que se puedan utilizar sin la necesidad de crear una instancia de la clase, a estos atributos y métodos se les define como static, en otros lenguajes se les llama tambien metodos o atributos de clase.

class MyMath {
  static readonly PI = 3.14;

  static max(...numbers: number[]) {
    return numbers.reduce((max, item) => max >= item ? max: item, 0);
  }
}

// No es necesario crear una instanciam bastara con llamar directamente a la clase
// para hacer uso de los métodos estaticos.
console.log('MyMath.PI', MyMath.PI);
console.log('MyMath.max', MyMath.max(12,2,1,1112,9));
const numbers = [12,2,1,1112,9, 3000];
console.log('MyMath.max', MyMath.max(...numbers));
console.log('MyMath.max', MyMath.max(-1, -9, -8));

Implementación de interfaces

En POO las Interfaces son usadas para indicar qué métodos debe obligatoriamente implementar (contener) una Clase (aunque no tienen por qué comportarse del mismo modo).

JavaScript no admite interfaces, por lo que, como desarrollador de JavaScript, podría tener o no experiencia con ellas. En TypeScript, puede usar interfaces igual que en la programación tradicional orientada a objetos. (ver mas)

...una interfaz se utiliza para establecer un "contrato de código" que describa las propiedades requeridas de un objeto y sus tipos. Por lo tanto, se puede utilizar una interfaz para asegurar la forma de la instancia de la clase. Las declaraciones de clase pueden hacer referencia a una o varias interfaces en su cláusula implements para validar que proporcionan una implementación de las interfaces. (ver mas)

Importante mencionar que las interfaces:

  1. No tienen constructores
  2. No implementan lógica, son métodos abstractos
  3. Los atributos y métodos definidos en una interfaces serán de tipo publico en la implementación
  4. Para hacer uso de ella utilizamos la palabra reservada implements
// Definimos nuestra interface
export interface InterfaceDriver {
  // Definimos atributos (serán públicos)
  database: string;
  password: string;
  port: number;

  // Definimos métodos (públicos)
  connect(): void;
  disconnect(): void;
  isConnected(name: string): boolean;
}

// Definimos nuestra 1era clase que IMPLEMENTA la interface
class PostgresDriver implements InterfaceDriver{
  constructor(
    public database: string,
    public password: string,
    public port: number,
    // Podemos agregar atributos adionales a la clase
    private host: number,
  ) {}

  // Implementamos (obligatoriamente) los métodos que define nuestra interface
  // Recuerde que la lógica de cada método se define en la implementación, la 
  // interface sólo define los nombres de los métodos.
  disconnect(): void {
    // code
  }

  isConnected(name: string): boolean {
    return true;
  }

  connect(): void {
    // code
  }
}

// Definimos nuestra 1era clase que IMPLEMENTA la interface
class OracleDriver implements InterfaceDriver{
  constructor(
    public database: string,
    public password: string,
    public port: number
  ) {}

  connect(): void {
    // code
  }

  disconnect(): void {
    // code
  }

  isConnected(name: string): boolean {
    return true;
  }
}

Clases abstractas

En algunas situaciones podemos tener multiples clases que comparten comportamientos (métodos con lógica de negocio) y atributos, una forma de agrupar dichos métodos y propiedades es utilizando clases abstractas.

Las clases abstractas no pueden ser instanciadas, debe ser utilizada mediante la herencia (extends) ( no podemos hacer un new ) por lo cual se parecen un poco a las interfaces.

Pueden definir métodos abstractos, sin lógica en ellos y obligando a las clases hijas a implementar el método y definir su comportamiento.

//animal.ts
export abstract class Animal{
  // Definimos nuestro constructor 
  // usamos _ para definir variable local y que no se confunda con 
  // las implementaciones
  constructor(protected _nombre:string){}

  // Indicamos que es necesario implementar este método en los hijos
  abstract desplazar(): void;

  // Definimos un método 
  get nombre(): string{
      this._nombre=nombre;
  }

  set nombre(nombre:string){
      this._nombre=nombre;
  }
    
}

//gato.ts
import {Animal} from "./animal";

// Heredamos de Animal usando extends
export class Gato extends Animal{
  // Definimos el constructor
  constructor(nombre: string, private raza:string){
    // Invocamos al constructor del padre
    super(nombre);
  }

  // Definimos el método abstracto de indicado por nuestra clase padre
  desplazar():void {
    console.log(`${this.nombre} camina sigilosamente`);
  }

  // Implementamos un método que sólo es de nuestra clase hija
  ronronear():void{
    console.log(`${this.nombre} ronronea`);
  }
}