¿Cómo funcionan las interfaces en la programación orientada a objetos?
Las interfaces son una herramienta esencial para la programación orientada a objetos, ya que permiten la implementación del polimorfismo y diversos patrones de diseño. Consideradas como "contratos", obligan a las clases que deciden utilizarlas a implementar cada una de las funciones o métodos que estas definen. La importancia de las interfaces es evidente en su capacidad de simplificar procesos de codificación y gestionar de manera eficiente el comportamiento de diferentes clases.
¿Qué es una interfaz en programación?
Una interfaz en programación se define como un conjunto de métodos que una clase debe implementar para cumplir con un contrato particular. En este sentido:
Definición de contrato: Una interfaz asegura que cualquier clase que la implemente debe contener los métodos especificados.
Flexibilidad en la implementación: Aunque una interfaz estipula cuáles métodos deben existir, no dicta cómo estos deben ser implementados.
En TypeScript, por ejemplo, se puede encontrar una interfaz denominada printInfo, que requiere una función getMessage que devuelve un string. Sin entrar en detalles de implementación, cualquier clase que quiera "firmar" este contrato deberá asegurarse de que el método getMessage exista dentro de su estructura.
¿Cómo se implementan las interfaces en TypeScript y Go?
Uso de interfaces en TypeScript
Declaración: Utiliza la palabra clave interface para definir qué métodos deben estar presentes.
Implementación: Las clases usan la palabra clave implements para indicar que están adoptando la interfaz.
Un ejemplo en TypeScript muestra cómo una clase puede implementar diferentes interfaces mientras mantiene su propia lógica interna única. Esta flexibilidad permite a distintos desarrolladores implementar la funcionalidad necesaria de forma peculiar a la estructura de su aplicación, apoyando el comportamiento polimórfico.
Implementación implícita en Go
A diferencia de TypeScript, Go utiliza un enfoque implícito para la implementación de interfaces:
Declaración: Similar a otros lenguajes, Go permite definir un tipo de interfaz, pero aquí, las clases que coinciden con la interfaz se consideran que la implementan automáticamente.
Polimorfismo: Mediante métodos comunes en diferentes estructuras (struct), Go permite operaciones polimórficas utilizando las interfaces.
El siguiente código ilustra la creación de una interfaz printInfo en Go:
type printInfo interface{getMessage()string}
Dos structs, TemporaryEmployee y FullTimeEmployee, pueden implementar implícitamente esta interfaz si definen un método getMessage() correctamente.
¿Por qué son importantes las interfaces?
El uso de interfaces en programación no solo fomenta un diseño más limpio y modular, sino que también reduce la duplicación de código. Al utilizar interfaces, no es necesario definir múltiples funciones printMessage para cada clase, sino que se puede trabajar con un solo método genérico que acepte cualquier objeto que implemente la interfaz. Esto:
Reduce la repetición: Evita crear funciones duplicadas para cada clase.
Facilita la extensión: Al añadir nuevas clases, solo se necesita implementar la interfaz sin cambiar el código existente.
Fomenta el polimorfismo: Permite que diferentes clases sean tratadas a través de una misma interfaz, expandiendo las posibilidades de reutilización y adaptación del código.
Reflexiones finales
Las interfaces son, sin duda, una joya en la corona de la programación orientada a objetos y en lenguajes como Go. Aunque su implementación pueda variar entre lenguajes, el concepto básico sigue siendo una pieza inquebrantable en la creación de códigos robustos, adaptables y eficientes. Alentamos a los desarrolladores a profundizar su comprensión y uso de estas potentes herramientas para maximizar la eficacia de sus proyectos. Y recuerda, el verdadero poder de la programación yace en la capacidad de seguir aprendiendo y adaptando diversas técnicas a tus necesidades de desarrollo. ¡Sigue explorando y codificando!
El tipado de Go respecto de las interfaces es una especie de duck typing. Como regla nmemotécnica: si camina como un pato y hace cuác como un pato, entonces debe ser un pato. La diferencia entre duck typing y el tipado en Go, es que en Go el chequeo sucede en tiempo de compilación, es tipado estático. Es por esto que lo llaman "tipado estructural", el chequeo le da importancia a la estructura.
Excelente aporte compañero
En lenguajes como Typescript o Java es ++necesario++ definir los métodos de la interfaz para poder implementarla y la implementación es explícita.
En cambio en Go, es ++suficiente++ definir los métodos para implementar la interfaz y la implementación es implícita.
En el polimorfismo se utiliza una interfaz (o una clase base) para determinar en tiempo de ejecución el método a utilizar, accediéndolo por medio de la interfaz en vez de hacerlo a través de un objeto en particular, porque en este último caso el método se determina en tiempo de compilación (esto no es polimorfismo).
// no se conoce p hasta el momento en que // se ejecuta esta función// podría ser un FullTimeEmployee// o un TemporaryEmployeefunc getMessage(p PrintInfo){ fmt.Println(p.getMessage)}// aquí ya se conoce el método a usar// antes de compilarftEmployee.getMessage()
Entonces con polimorfismo, podrías por ejemplo, estar recibiendo desde una BD o un JSON desde un front, los datos de un empleado en tiempo de ejecución y determinar en ese momento cual es el método apropiado para ese empleado en particular.
Apuntes y código:
Diferentes lenguajes de programación utilizan sintaxis explicita para decir que una clase implementa un interfaz
Go lo hace de manera implícita lo que permite la re utilización de código y polimorfismo
package main
import"fmt"type Person struct { name string
age int
}type Employee struct { id int
}// De esta manera aplicamos la composicion sobre la herenciatype FullTimeEmployee struct {PersonEmployee endDate string
}type TemporatyEmployee struct {PersonEmployee taxRate int
}// Creamos la interfaztype PrintInfointerface{getMessage() string
}// Implementamos la interfazfunc(ftEmployee FullTimeEmployee)getMessage() string {return"Full Time Employee"}func(tEmployee TemporatyEmployee)getMessage() string {return"Temporary Employee"}// Creamos el metodo de la interfazfunc getMessage(p PrintInfo){ fmt.Println(p.getMessage())}func main(){ftEmployee:=FullTimeEmployee{} ftEmployee.name="Benjamin" ftEmployee.age=20 ftEmployee.id=1 fmt.Printf("%v\n", ftEmployee)tEmployee:=TemporatyEmployee{}getMessage(tEmployee)getMessage(ftEmployee)}
yo no conozco typescript, por lo que me perdi en esta explicación.
siento que deberia solo explicar en el lenguaje, ya que hace comparacion de 3 lenguajes a la vez, y uno se enrreda.
Go no implementa interfaces de manera explicita sino de manera implícita.
¿Los structs podrían ser inmutables?
Puedes crear la struct vacía y crear FunctionsReceivers de solo lectura asociadas al struct.
No sé si sea buena practica, pero es la que se me ocurre y creo haría lo que necesitas.
para q los ejemplos de ts? por q no directamente vemos go?
package main
import"fmt"type Person struct { name string
age int
}type Employee struct { id int
}// Inheritance tipo anonimotype FullTimeEmployee struct {PersonEmployee endDate string
}type TemproaryEmployee struct {PersonEmployee taxRate float64
}type PrintInfointerface{getMessage() string
}// Composicion sobre herenciafunc(ft FullTimeEmployee)getMessage() string {return fmt.Sprintf("Hi %s, you are %d years old. And you are a full time employee", ft.name, ft.age)}func(te TemproaryEmployee)getMessage() string {return fmt.Sprintf("Hi %s, you are %d years old. And you are a temprary employee", te.name, te.age)}func GetMessage(pi PrintInfo){ fmt.Println(pi.getMessage())}func main(){f:=FullTimeEmployee{} f.name="John" f.age=30 f.id=1 f.endDate="2019-01-01" fmt.Printf("%+v\n", f)t:=TemproaryEmployee{} t.name="Mary" t.age=25 t.id=2 t.taxRate=0.25 fmt.Printf("%+v\n", t)GetMessage(f)GetMessage(t)}
Comparto mi codigo
package main
import"fmt"type Person struct { name string
age int
}type Employee struct { id int
}// Composicion de herencia, se agrega el tipo de objetotype FullTimeEmployee struct {PersonEmployee yearsExperience int
}type HalfTimeEmployee struct {PersonEmployee internship bool
}//Constructoresfunc NewFullTimeEmployee(name string, age int, id int, yearExperience int)*FullTimeEmployee{return&FullTimeEmployee{Person:Person{name: name,age: age,},Employee:Employee{id: id,},yearsExperience: yearExperience,}}func(ftEmployee *FullTimeEmployee)GetMesagge(){ fmt.Printf("FullTimeEmployee's Name :%s with a age %d and him/her id is %d with %d years of experience\n", ftEmployee.name, ftEmployee.age, ftEmployee.id, ftEmployee.yearsExperience)}func NewHalfTimeEmployee(name string, age int, id int, internship bool)*HalfTimeEmployee{return&HalfTimeEmployee{Person:Person{name: name,age: age,},Employee:Employee{id: id,},internship: internship,}}func(htEmployee *HalfTimeEmployee)GetMesagge(){ fmt.Printf("HalfTimeEmployee's Name: %s, Age: %d, ID: %d, Internship: %t\n", htEmployee.name, htEmployee.age, htEmployee.id, htEmployee.internship)}type PrintInfointerface{GetMesagge()}func PrintData(p PrintInfo){ p.GetMesagge()}func main(){ft:=NewFullTimeEmployee("Axel",22,1,2)ht:=NewHalfTimeEmployee("Alexis",22,2,true)PrintData(ft)PrintData(ht)}
Comparto este pequeño ejemplo un poco más simple pero consistente del polimorfismo. Espero les ayude :)
package main
import"fmt"// Definimos la interfaz con la declaración del método que deben implementarlo// el struct que lo necesitetype MakeSoundinterface{makeSound() string
}// Función que, dependiendo de la interfaz implementada ejecutará un método u otrofunc makeSound(makeSound MakeSound){ fmt.Print("\n", makeSound.makeSound())}type Cat struct { color string
}// Implementa interfaz para struct de Catfunc(cat Cat)makeSound() string {return"Meow"}type Tiger struct { color string
}// Implementa interfaz para struct de Tiger.func(tiger Tiger)makeSound() string {return"Roughh!"}func main(){cat:=Cat{}tiger:=Tiger{}makeSound(cat)// Aquí deberá ejecutar la función de makeSound de Cat.makeSound(tiger)// Aquí deberá ejecutar la función de makeSound de Tiger.}```package main import"fmt"// Definimos la interfaz con la declaración del método que deben implementarlo // el struct que lo necesite type MakeSound interface { makeSound() string } // Función que, dependiendo de la interfaz implementada ejecutará un método u otro func makeSound(makeSound MakeSound) { fmt.Print("\n", makeSound.makeSound()) } type Cat struct { color string } // Implementa interfaz para struct de Cat func (cat Cat) makeSound() string { return "Meow" } type Tiger struct { color string } // Implementa interfaz para struct de Tiger. func (tiger Tiger) makeSound() string { return "Roughh!" } func main() { cat := Cat{} tiger := Tiger{} makeSound(cat) // Aquí deberá ejecutar la función de makeSound de Cat. makeSound(tiger) // Aquí deberá ejecutar la función de makeSound de Tiger. }
Muy buen aporte!
Dado que las interfaces en Go son implicitas, existe alguna recomendación o buena práctica que permita facilitar el mantenimiento de código. Al ser implicitas no es fácil a simple vista determinar qué interfaces implementa una estructura lo cual dificulta el mantenimiento de código (en especial cuando uno realizar mantenimiento de código de un tercero).
Siento muy forzado go para la OOP, pero siento que se puede mantener la limpieza si todo lo llevamos a un archivo aparte.
Al revés. La OOP es muy forzada, he tiene muchas partes ineficientes en el libro de Golang Programming language explican por qué quitaron tantas cosas de OOP y de resultado salió un lenguaje mucho más rápido y eficiente con sintaxis más simple.
En golang puedes usar paquetes para agrupar el código es diferente a Java o C# que es cada clase por archivo, el peor error es tratar de usar Golang como java o C#.
¿Cómo podemos implementar visibilidad de propiedades y métodos en los structs?
Solo haz que la primera letra del nombre del método o la propiedad del struct inicien con mayúscula.
¿No he entendido bien cual es el beneficio de usar interfaces, no seria mejor simplemente ejecutar el metodo de cada objeto para asi no tener que escribir la interface ni el metodo que recibe como parametro la interface y asi usar menos codigo, osea no seria mas facil poner obj1.getMessage() y obj2.getMessage()?
Imagina que actualizas obj2.getMessage() y quedas con una implementación que te retorne un tipo de dato diferente como un byte, un objeto clase Message, o una dirección en memoria, corres el riesgo de tener un resultado no deseado y romper tu código. La idea de la interfaz es organizar el código para evitar algún comportamiento no esperado.
El beneficio de la interfaz se ve a medida que aumenta la complejidad de la aplicación, ya que no solo te dice el nombre del método, sino también que tipos de datos tomar como entrada y como salida. (Esto es lo que llaman la firma de un método)
Esto te ayuda en temas de abstracción y polimorfismo, por ejemplo si quieres subir el volumen a una película o música, puedes definir una "interfaz de audio" con un método "subirVolumen" que como entradas tenga un array de bytes con un entero y como salida un array bytes ( que va a estar multiplicado o dividido, por decir cualquier cosa). El comportamiento será el esperado para ambos objetos porque te aseguraste de que implementan esta interfaz y se puede usar con confianza en otros lugares del código.
También te ayuda a que otros desarrolladores no utilicen esto en objetos que no cumplen esta interfaz por ejemplo algo como una Imagen o un pdf pero si en futuros objetos como una grabacion o una llamada de telefono.
Si no queda muy claro lo de interfaces, básicamente es un plano, igual que los structs pero en lugar de enfocarse en los datos (estructura), se enfoca en las acciones (métodos).
Ya que no existe implementación en go, al utilizar la interface como parámetro (como el caso de getMessage), validará cualquier estructura que cumpla con el plano, es decir, que contenga todos los métodos de la interface.
Comparto mi codigo con implemetacion de constructores con interfacespackage main
import "fmt"
type Person struct { name string age int}
type Employee struct { id int}
// Composicion de herencia, se agrega el tipo de objetotype FullTimeEmployee struct { Person Employee yearsExperience int}type HalfTimeEmployee struct { Person Employee internship bool}
//Constructores
func NewFullTimeEmployee(name string, age int, id int, yearExperience int) *FullTimeEmployee { return &FullTimeEmployee{ Person: Person{ name: name, age: age, }, Employee: Employee{ id: id, }, yearsExperience: yearExperience, }}
func (ftEmployee *FullTimeEmployee) GetMesagge() { fmt.Printf("FullTimeEmployee's Name :%s with a age %d and him/her id is %d with %d years of experience\n", ftEmployee.name, ftEmployee.age, ftEmployee.id, ftEmployee.yearsExperience)}
func NewHalfTimeEmployee(name string, age int, id int, internship bool) *HalfTimeEmployee { return &HalfTimeEmployee{ Person: Person{ name: name, age: age, }, Employee: Employee{ id: id, }, internship: internship, }}
func (htEmployee *HalfTimeEmployee) GetMesagge() { fmt.Printf("HalfTimeEmployee's Name: %s, Age: %d, ID: %d, Internship: %t\n", htEmployee.name, htEmployee.age, htEmployee.id, htEmployee.internship)}
type PrintInfo interface { GetMesagge()}
func PrintData(p PrintInfo) { p.GetMesagge()}
func main() { ft := NewFullTimeEmployee("Axel", 22, 1, 2) ht := NewHalfTimeEmployee("Alexis", 22, 2, true) PrintData(ft) PrintData(ht)}
package main
import"fmt"type Person struct { name string
age int
}type Employee struct { id int
}// Composicion de herencia, se agrega el tipo de objetotype FullTimeEmployee struct {PersonEmployee yearsExperience int
}type HalfTimeEmployee struct {PersonEmployee internship bool
}//Constructoresfunc NewFullTimeEmployee(name string, age int, id int, yearExperience int)*FullTimeEmployee{return&FullTimeEmployee{Person:Person{name: name,age: age,},Employee:Employee{id: id,},yearsExperience: yearExperience,}}func(ftEmployee *FullTimeEmployee)GetMesagge(){ fmt.Printf("FullTimeEmployee's Name :%s with a age %d and him/her id is %d with %d years of experience\n", ftEmployee.name, ftEmployee.age, ftEmployee.id, ftEmployee.yearsExperience)}func NewHalfTimeEmployee(name string, age int, id int, internship bool)*HalfTimeEmployee{return&HalfTimeEmployee{Person:Person{name: name,age: age,},Employee:Employee{id: id,},internship: internship,}}func(htEmployee *HalfTimeEmployee)GetMesagge(){ fmt.Printf("HalfTimeEmployee's Name: %s, Age: %d, ID: %d, Internship: %t\n", htEmployee.name, htEmployee.age, htEmployee.id, htEmployee.internship)}type PrintInfointerface{GetMesagge()}func PrintData(p PrintInfo){ p.GetMesagge()}func main(){ft:=NewFullTimeEmployee("Axel",22,1,2)ht:=NewHalfTimeEmployee("Alexis",22,2,true)PrintData(ft)PrintData(ht)}
Hola! Los invito a "jugar" con el siguiente código, ver cómo se comporta solo con el primer método (comentando los últimos 2 SIN comentar las correspondientes llamadas) antes de pasar a interfaces. (Go 1.22.1)
package main
import"fmt"type Person struct {Name string
Age int
}type Employee struct {Person// Embedding id int
}type Developer struct {Employee// EmbeddingLanguage string
}func(p *Person)GetBdayMsj() string {return fmt.Sprintf("Hi %s. Congratulations! on your %dth birthday", p.Name, p.Age)}func(e *Employee)GetBdayMsj() string {return fmt.Sprintf("Hi %s. Congratulations! on your %dth birthday. Your employee id is %d", e.Name, e.Age, e.id)}func(d *Developer)GetBdayMsj() string {return fmt.Sprintf("Hi %s. Congratulations! on your %dth birthday. I see you are a %s developer", d.Name, d.Age, d.Language)}func main(){dev:=Developer{Employee:Employee{Person:Person{Name:"John",Age:25,},id:1,},Language:"Go",} fmt.Println(dev.Person.GetBdayMsj()) fmt.Println(dev.Employee.GetBdayMsj()) fmt.Println(dev.GetBdayMsj())}
se puede deducir que Developer SI es un Employee y SI es un Person, y además puede crearse el override del método
Genial lo que he podido recordar de TS., pero para quienes no conocen de JS o TS, siento podría ser un poco engorroso tener que ver media clase de un lenguaje al cual no le están apuntando. Por lo demás, muy bien explicado.
package main
import"fmt"type Person struct { name string
age uint8
sex string
}type PrintInfointerface{getMessage() string
}type Employee struct { id uint64
}type FullTimeEmployee struct {PersonEmployee endTime int
}func(ftEmployee FullTimeEmployee)getMessage() string {return"Este es un fulltimeEmployee"}type TemporaryEmployee struct {PersonEmployee taxRate string
}func(tEmployee TemporaryEmployee)getMessage() string {return"Este es un TemporaryEmployee"}func getMessage(p PrintInfo){ fmt.Printf("%+v\n", p.getMessage())}func main(){ftEmployee:=FullTimeEmployee{} ftEmployee.name="Carlos" ftEmployee.age=25 ftEmployee.id=1 ftEmployee.sex="hombre" fmt.Printf("%+v\n", ftEmployee)tEmployee:=TemporaryEmployee{} fmt.Printf("%+v\n", tEmployee)getMessage(ftEmployee)getMessage(tEmployee)}