Skip to content

Latest commit

 

History

History
419 lines (317 loc) · 13.2 KB

File metadata and controls

419 lines (317 loc) · 13.2 KB

2.6 Interfaces

Interfaces

Una de las características de diseño más sutiles en Go son las interfaces. Después de leer esta sección, probablemente quedara impresionado por su implementación.

¿Que es una interfaz?

En pocas palabras, una interfaz es un conjunto de métodos, que se usan para definir un conjunto de acciones.

Al igual que en los ejemplos de las secciones anteriores, ambos, Student y Employee pueden llamar a SayHi(), pero ellos no hacen lo mismo.

Vamos a trabajar un poco mas, vamos a agregarle a ellos el método Sing(), y también vamos a agregar BorrowMoney() a Student y SpendSalary() a Employee.

Ahora Student tiene tres métodos llamados SayHi(), Sing(), BorrowMoney(), y Employee tiene SayHi(), Sing() y SpendSalary().

Esta combinación de métodos es llamada interfaz, y es implementada por Student y Employee. Entonces Student y Employee implementan la interfaz: SayHi(), Sing(). Al mismo tiempo, Employee no implementa la interfaz: SayHi(), Sing(), BorrowMoney(), y Student no implementa la interfaz: SayHi(), Sing(), SpendSalary(). Esto es porque Employee no tiene el método BorrowMoney() y Student no tiene el método SpendSalary().

Tipo de Interfaces

Una interfaz define un grupo de métodos, entonces si un tipo implementa todos los métodos entonces nosotros decimos que ese tipo implementa la interfaz.

	type Human struct {
		name  string
		age   int
		phone string
	}

	type Student struct {
		Human
		school string
		loan   float32
	}

	type Employee struct {
		Human
		company string
		money   float32
	}

	func (h *Human) SayHi() {
		fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
	}

	func (h *Human) Sing(lyrics string) {
		fmt.Println("La la, la la la, la la la la la...", lyrics)
	}

	func (h *Human) Guzzle(beerStein string) {
		fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
	}

	// Employee sobrescribe Sayhi
	func (e *Employee) SayHi() {
		fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
			e.company, e.phone) //Si acá podes cortarlo en 2 líneas.
	}

	func (s *Student) BorrowMoney(amount float32) {
		s.loan += amount // (otra vez...)
	}

	func (e *Employee) SpendSalary(amount float32) {
		e.money -= amount
	}

	// definimos algunas interfaces
	type Men interface {
		SayHi()
		Sing(lyrics string)
		Guzzle(beerStein string)
	}

	type YoungChap interface {
		SayHi()
		Sing(song string)
		BorrowMoney(amount float32)
	}

	type ElderlyGent interface {
		SayHi()
		Sing(song string)
		SpendSalary(amount float32)
	}

Sabemos que una interfaz puede ser implementada por cualquier tipo, y un tipo puede implementar muchas interfaces al mismo tiempo.

Tenga en cuenta que todos los tipos implementan la interfaz vacía interface{} porque esta no tiene ningún método y todos los tipos tienen cero métodos por defecto.

Valor de una interfaz

Entonces, ¿qué tipo de valores se pueden poner en una interfaz? Si definimos una variable como tipo interfaz, cualquier tipo que implemente la interfaz puede asignarse a esta variable.

Al igual que en el ejemplo anterior, si definimos una variable m como la interfaz Men, entonces cualquier Student, Human o Employee puede ser asignado a m. Entonces podríamos tener una lista de Men, y cualquier tipo que implemente la interfaz Men puede agregarse a esa lista (slice). Sin embargo sea consciente que una lista de interfaces no tiene el mismo comportamiento que una lista de otros tipos.

	package main

	import "fmt"

	type Human struct {
		name  string
		age   int
		phone string
	}

	type Student struct {
		Human
		school string
		loan   float32
	}

	type Employee struct {
		Human
		company string
		money   float32
	}

	func (h Human) SayHi() {
		fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
	}

	func (h Human) Sing(lyrics string) {
		fmt.Println("La la la la...", lyrics)
	}

	func (e Employee) SayHi() {
		fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
			e.company, e.phone) //Si acá podes cortarlo en 2 líneas.
	}

	// La interfaz Men es implementada por Human, Student y Employee
	type Men interface {
		SayHi()
		Sing(lyrics string)
	}

	func main() {
		mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
		paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
		sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
		Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}

		// definimos la interfaz i
		var i Men

		//i podemos guardar un Student
		i = mike
		fmt.Println("This is Mike, a Student:")
		i.SayHi()
		i.Sing("November rain")

		//i podemos guardar un Employee
		i = Tom
		fmt.Println("This is Tom, an Employee:")
		i.SayHi()
		i.Sing("Born to be wild")

		// lista de Men
		fmt.Println("Let's use a slice of Men and see what happens")
		x := make([]Men, 3)
		// estos tres elementos son de diferentes tipos pero todos ellos implementan la interfaz Men
		x[0], x[1], x[2] = paul, sam, mike

		for _, value := range x {
			value.SayHi()
		}
	}

Una interfaz es un conjunto de métodos abstractos, y puede ser implementada por tipos que no son interfaces. Por lo tanto esta no puede implementarse a si misma.

Interfaz vacía

Una interfaz vacía es una interfaz que no contiene ningún método, entonces todos los tipos implementan una interfaz vacía. Es muy útil cuando buscamos guardar todos los tipos en el mismo lugar, y es similar a void* en C.

	// definimos una interfaz vacía
	var a interface{}
	var i int = 5
	s := "Hello world"
	// a puede guardar un valor de cualquier tipo
	a = i
	a = s

Si una función usa una interfaz vacía como su tipo de argumento, esta puede aceptar cualquier tipo; si una función usa una interfaz vacía como el tipo de valor de retorno, esta puede devolver cualquier tipo.

Argumentos de métodos de una interfaz

Cualquier variable puede usarse en una interfaz, entonces podemos pensar sobre como podemos usar esta característica para pasar cualquier tipo de variable a una función.

Por ejemplo, usamos mucho fmt.Println, pero alguna vez notaste que ¿acepta cualquier tipo de argumento? Si vemos el código fuente de fmt que es libre, podemos ver la siguiente definición.

	type Stringer interface {
    		String() string
	}

Esto significa que cualquier tipo que implemente la interfaz Stringer puede ser pasada a fmt.Println como argumento. Vamos a probarlo.

	package main

	import (
		"fmt"
		"strconv"
	)

	type Human struct {
		name  string
		age   int
		phone string
	}

	// Human implementa fmt.Stringer
	func (h Human) String() string {
		return "Name:" + h.name + ", Age:" + strconv.Itoa(h.age) + " years, Contact:" + h.phone
	}

	func main() {
		Bob := Human{"Bob", 39, "000-7777-XXX"}
		fmt.Println("This Human is : ", Bob)
	}

Volviendo atrás al ejemplo de Box, podemos ver que Color también implementa la interfaz Stringer, por lo que somos capaces de personalizar el formato de impresión. Si no implementamos esa interfaz, fmt.Println imprimirá el tipo con el formato por defecto.

	fmt.Println("The biggest one is", boxes.BiggestsColor().String())
	fmt.Println("The biggest one is", boxes.BiggestsColor())

Atención: Si el tipo implementa la interfaz error, fmt va a llamar a error(), entonces en este punto no tendrás que implementar Stringer.

Tipos de variables en un interfaz

Si una variable es del tipo que implementa una interfaz, sabemos que cualquier otro tipo que implemente la misma interfaz puede ser asignada a esta variable. La pregunta es ¿Cómo podemos saber cual es el tipo específico almacenado en la interfaz? Tenemos dos formas, que te voy a comentar a continuación.

  • Patron de Aserción Comma-ok

Go tiene la sintaxis value, ok := element.(T). Esto comprueba si la variable es del tipo que se espero, donde value es el valor de la variable, y ok es un valor de tipo booleano, element es la variable interfaz y T es el tipo que se afirma tener.

Si el elemento es del tipo que esperamos, ok será true, en otro caso será false.

Veamos un ejemplo para verlo con más claridad.

	package main

	import (
		"fmt"
		"strconv"
	)

	type Element interface{}
	type List []Element

	type Person struct {
		name string
		age  int
	}

	func (p Person) String() string {
		return "(Nombre: " + p.name + " - edad: 	" + strconv.Itoa(p.age) + " años)"
	}

	func main() {
		list := make(List, 3)
		list[0] = 1       // un int
		list[1] = "Hola" // una string
		list[2] = Person{"Dennis", 70}

		for index, element := range list {
			if value, ok := element.(int); ok {
				fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
			} else if value, ok := element.(string); ok {
				fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
			} else if value, ok := element.(Person); ok {
				fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
			} else {
				fmt.Println("list[%d] is of a different type", index)
			}
		}
	}

Es bastante sencillo usar este patrón, pero si tenemos que verificar varios tipos, es mejor que usemos un switch.

  • Verificación con un switch

Vamos a ver el uso de switch para reescribir el ejemplo anterior.

	package main

	import (
		"fmt"
		"strconv"
	)

	type Element interface{}
	type List []Element

	type Person struct {
		name string
		age  int
	}

	func (p Person) String() string {
		return "(Nombre: " + p.name + " - edad: " + strconv.Itoa(p.age) + " años)"
	}

	func main() {
		list := make(List, 3)
		list[0] = 1       //un int
		list[1] = "Hello" //una string
		list[2] = Person{"Dennis", 70}

		for index, element := range list {
			switch value := element.(type) {
			case int:
				fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
			case string:
				fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
			case Person:
				fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
			default:
				fmt.Println("list[%d] is of a different type", index)
			}
		}
	}

Una cosa que deberíamos recordar es que element.(type) no puede ser usado fuera del cuerpo del switch, lo que significa que en este caso no puedes usar el patrón comma-ok.

Interfaces embebidas

La cosa más atractiva es que Go tiene mucha lógica embebida en su sintaxis, como campos anónimos en un struct. No es para sorprenderse, que podamos usar interfaces también como campos anónimos, pero vamos a llamarlas Interfaces embebidas. Aquí, vamos a seguir las mismas reglas que para los campos anónimos. Más específicamente, si una interfaz tiene otra interfaz como una interfaz embebida, esta tendrá todos los métodos que la clase embebida tiene.

Podemos ver el archivo fuente container/heap que tiene una definición como la siguiente.

	type Interface interface {
	    	sort.Interface // embebida sort.Interface
	    	Push(x interface{}) //el método Push para empujar elementos a la pila
	    	Pop() interface{} //el elemento Pop que saca elementos de la pila
	}

Podemos ver que sort.Interface es una interfaz embebida, por lo que la interfaz anterior tiene tres métodos que son explícitos de sort.Interface.

	type Interface interface {
	    	// Len es el número de elementos en la colección.
	    	Len() int
	    	// Less devuelve si el elemento de índice i se debe ordenar
	    	// antes que el elemento con índice j.
	    	Less(i, j int) bool
	    	// Swap intercambia los elementos con índices i y j.
	    	Swap(i, j int)
	}

Otro ejemplo es el de io.ReadWriter en el paquete io.

	// io.ReadWriter
	type ReadWriter interface {
    		Reader
    		Writer
	}

Reflexión

La reflexión en Go es usada para obtener información en tiempo de ejecución. Vamos a usar el paquete reflect, y este articulo oficial articulo explica como funciona la reflexión en Go.

Tenemos que realizar tres pasos para usar reflect. Primero, necesitamos convertir una interfaz en un tipo reflect types (reflect.Type o reflect.Value, depende en que situación nos encontremos).

	t := reflect.TypeOf(i)    // tomamos la meta-data de el tipo i, y usamos t para tomar todos los elementos
	v := reflect.ValueOf(i)   // tomamos el valor actual de el tipo i, y usamos v para cambiar este valor

Después de eso, necesitamos convertir el tipo reflect de el valor que tomamos a el tipo que necesitamos.

	var x float64 = 3.4
	v := reflect.ValueOf(x)
	fmt.Println("type:", v.Type())
	fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
	fmt.Println("value:", v.Float())

Finalmente, si buscamos cambiar el valor que vino del tipo reflects, necesitamos hacerlo modificable. Como hablamos antes, esta es una diferencia entre pasar por valor o por referencia. El siguiente código no compilará.

	var x float64 = 3.4
	v := reflect.ValueOf(x)
	v.SetFloat(7.1)

En lugar de eso, debemos usar el siguiente código para cambiar el valor de los tipos reflect.

	var x float64 = 3.4
	p := reflect.ValueOf(&x)
	v := p.Elem()
	v.SetFloat(7.1)

Acabamos de hablar sobre los conocimientos básicos de sobre el uso de reflexión (reflection), deberías practicar más para entenderlo mejor.

Enlaces