Interfacce

Un'interfaccia è un concetto molto potente che sta alla base del concetto di astrazione e dell'effetto di polimorfismo di metodo.

Un'interfaccia è un tipo che lista una serie di dichiarazioni di funzioni, ciascuna con il nome e il/i tipo/i di ritorno.

Le interfacce vengono implementate da una struct scrivendo la serie di metodi dichiarati nell'interfaccia.
Come al solito Go è molto più pratico e meno formale, p.es. di Java. Non esiste la parola chiave implements, ne tanto meno extends con eredità di interfacce.

Esempio

(200interfaces.go):

package main

import "fmt"
import "math"

// Un'interface è come una struct ma tutti
// gli elementi sono metodi dichiarati
type geometry interface {
	area() float64
	perim() float64
}

// Due struct su cui implementare l'interface
type rect struct {
	width, height float64
}
type circle struct {
	radius float64
}

// I metodi dichiarati in un'interface devono essere implementati
// per tutte le struct a cui si appliacano
// Per rect
func (r rect) area() float64 {
	return r.width * r.height
}
func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}

// Per circle
func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
	return 2 * math.Pi * c.radius
}

// Se una funzione ha un parametro di tipo interface
// possiamo richiamare tutti i metodi dell'interface
// NB: qui non è definito se measure si applica a rect o circle
// La funzione è richiamabile per tutte le struct
// che implementano l'interface geometry
func measure(t string, g geometry) {
	fmt.Print(t, g,": ")
	fmt.Printf("area: %8.2f\n", g.area())
	fmt.Printf("perimeter: %8.2f\n", g.perim())
	fmt.Println()
}

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}

	// Dato che sia rect che circle implementano geometry
	// si può invocare measure su r e su c
	measure("rect", r)
	measure("circle", c)
}
// La separazione tra interfaccia e implementazione
// è un pattern molto usato

Separazione di package

Vogliamo che le struct e l'interfaccia siano in un package e che i campi delle struct siano privati.

1. Creare la directory figs e andarvi dentro

mkdir figs
cd figs

2. Editare il file figs.go:

package figs

import (
        "fmt"
        "math"
)

// Dichiarazione dell'interfaccia
type Geometry interface {
        area() float64
        perim() float64
}

// Rettangolo
type Rect struct {
        width, height float64
}

// Getters e setters per il rettangolo
func (r Rect) GetWidth() float64 {
        return r.width
}
func (r *Rect) SetWidth(w float64) {
        r.width = w
}
func (r Rect) GetHeight() float64 {
        return r.height
}
func (r *Rect) SetHeight(w float64) {
        r.height = w
}

// Implementazione dell'interfaccia
func (r Rect) area() float64 {
        return r.width * r.height
}
func (r Rect) perim() float64 {
        return 2*r.width + 2*r.height
}

// Cerchio
type Circle struct {
        radius float64
}
// Getters e setters per il cerchio
func (c Circle) GetRadius() float64 {
        return c.radius
}
func (c *Circle) SetRadius(rad float64) {
        c.radius = rad
}

// Implementazione dell'interfaccia
func (c Circle) area() float64 {
        return math.Pi * c.radius * c.radius
}
func (c Circle) perim() float64 {
        return 2 * math.Pi * c.radius
}

// E' opportuno che ogni funzione esportata
// abbia un commento a precedere
func Measure(t string, g Geometry) {
        fmt.Print(t, g, ": ")
        fmt.Printf("area: %8.2f\n", g.area())
        fmt.Printf("perimeter: %8.2f\n", g.perim())
        fmt.Println()
}

3. Installare il package

go install

Nel nostro caso il suo percorso di package diventa core/figs

4. In un'altra directory del workspace scrivere il programma che lo usa:

(202-interface-package.go):

package main

import (
	fig "core/figs"
	"fmt"
)

func main() {
	// Le seguenti istruzioni non funzionano
	// perchè gli attributi non sono esportati
	//	r := fig.Rect{width: 3, height: 4}
	//	c := fig.Circle{radius: 5}

	var r fig.Rect
	r.SetWidth(10.0)
	r.SetHeight(5.0)
	fmt.Println("Rectangle: width= ", r.GetWidth(),
		" height= ", r.GetHeight())
	var c fig.Circle
	c.SetRadius(5.0)
	fmt.Println("Circle: radius= ", c.GetRadius())

	// Dato che sia rect che circle implementano geometry
	// si può invocare measure su r e su c
	fig.Measure("rect", r)
	fig.Measure("circle", c)
}

Costruttori

Go non possiede costruttori, ma si possono emulare con una convenzione: per una struct Rect inserire la funzione

func NewRect(w, h float64) *Rect {
        return &Rect{w, h}
}

Notare che ritorna un puntatore a Rect, che viene silenziosamente allocato in memoria dinamica.
Tutti i metodi che agiscono sulla struct devono ora prendere un argomento puntatore.

Il programma di package diventa core/figure/figure.go:

package figure

import (
    "fmt"
    "math"
)

// Dichiarazione dell'interfaccia
type Geometry interface {
    area() float64
    perim() float64
}

// Rettangolo
type Rect struct {
    width, height float64
}

// Pseudocostruttore
func NewRect(w, h float64) *Rect {
    return &Rect{w, h}
}

// Getters e setters per il rettangolo
func (r *Rect) GetWidth() float64 {
    return r.width
}
func (r *Rect) SetWidth(w float64) {
    r.width = w
}
func (r *Rect) GetHeight() float64 {
    return r.height
}
func (r *Rect) SetHeight(w float64) {
    r.height = w
}

// Implementazione dell'interfaccia
func (r *Rect) area() float64 {
    return r.width * r.height
}
func (r *Rect) perim() float64 {
    return 2*r.width + 2*r.height
}

// Cerchio
type Circle struct {
    radius float64
}

// Pseudocostruttore
func NewCircle(r float64) *Circle {
    return &Circle{r}
}

// Getters e setters per il cerchio
func (c *Circle) GetRadius() float64 {
    return c.radius
}
func (c *Circle) SetRadius(rad float64) {
    c.radius = rad
}

// Implementazione dell'interfaccia
func (c *Circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c *Circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

// E' opportuno che ogni funzione esportata
// abbia un commento a precedere
func Measure(t string, g Geometry) {
    fmt.Print(t, g, ": ")
    fmt.Printf("area: %8.2f\n", g.area())
    fmt.Printf("perimeter: %8.2f\n", g.perim())
    fmt.Println()
}

E il programma principale è:

(204-interface-constructor.go):

package main

import (
	fig "core/figure"
	"fmt"
)

func main() {
	// r e c sono puntatori, ma non si nota
	r := fig.NewRect(10, 5)
	c := fig.NewCircle(5)

	// Il resto del programma non cambia
	fmt.Println("Rectangle: width= ", r.GetWidth(),
		" height= ", r.GetHeight())
	fmt.Println("Circle: radius= ", c.GetRadius())

	fig.Measure("rect", r)
	fig.Measure("circle", c)
}

Le convenzioni sono quindi:

  1. Le struct sono gestite per riferimento
  2. Il costruttore di struct Pippo è NewPippo
  3. Se un attributo si chiama attr
  • Il suo getter è GetAttr
  • Il suo setter è SetAttr

Commento

Abbiamo riprodotto la sitazione di Java, in cui tutti gli oggetti sono silenziosamente gestiti per riferimento.