Thursday, June 20, 2024

Golang - Interface


Consider a wall outlet (power provider) and devices (power consumers) as examples.

A wall outlet knows nothing about the devices (power consumers). However, it can charge devices after their power adapter cable is plugged in. Devices like lamps and blenders require a power adapter to take electricity from a wall outlet.

The following is a crude implementation based on our initial ideas.


dishwasher.go
type dishwasher struct {
    brand           string
    model           string
    numOfWashCycles int
    price           float64
}

func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
    return &dishwasher{
      brand:           brand,
      model:           model,
      numOfWashCycles: numOfWashCycles,
      price:           price,
    }
}

func (dw *dishwasher) consume() {
    fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}


hairdryer.go
type hairDryer struct {
    brand            string
    model            string
    numOfHeatSetting int
    price            float64
}

func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
    return &hairDryer{
      brand:            brand,
      model:            model,
      numOfHeatSetting: numOfHeatSetting,
      price:            price,
    }
}

func (hd *hairDryer) consume() {
    fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}


walloutlet.go
type wallOutlet struct {
    // Connected Devices
    connectedDishwasher *dishwasher
    connectedHairDryer  *hairDryer
}

func newWallOutlet() *wallOutlet {
    return &wallOutlet{
      connectedDishwasher: &dishwasher{},
      connectedHairDryer:  &hairDryer{},
    }
}

func (w *wallOutlet) plugDishwasher(dw *dishwasher) {
    w.connectedDishwasher = dw
}

func (w *wallOutlet) plugHairDryer(hd *hairDryer) {
    w.connectedHairDryer = hd
}

func (w *wallOutlet) sendPower() {
    fmt.Println("Wall Outlet")

    w.connectedDishwasher.consume()
    w.connectedHairDryer.consume()
}


main.go
func main() {
    wallOutlet := newWallOutlet()
    hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
    dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)

    // Plug in devices
    wallOutlet.plugHairDryer(hairDryer)
    wallOutlet.plugDishwasher(dishwasher)

    wallOutlet.sendPower()
}


Result
 Wall Outlet
    -> The Dishwasher (Bosch - SHE3AEM2N) is powered
    -> The Hair Dryer (Dyson - Supersonic) is powered


From the example above, we can see that the struct of a hair dryer and a dishwasher are very similar. They contain the same behavior (method) known as 'consume'.

Although they appear to be similar, they are actually different struct types. As a result, the 'wallOutlet' struct must include a number of plug methods for connecting various types of power consumers.

Furthermore, adding new fields to the 'wallOutlet' struct will be challenging to maintain in the future.



Interface



To improve it, Go provides 'Interface', which allows us to design an abstract type that simply outlines the intended behaviors (methods) and does not go into detail about implementation. It is a similar concept to other OOP languages.

    * 'Interface' is a Protocol.

    * 'Interface' is an abstract type.

    * There is no implementation in the 'Interface'.

Take a look below for further information on the 'Interface' syntax.

Format
type powerConsumer interface {
    consume()
}


    * The powerConsumer is the interface type.

    * The interface name is better to end in 'er' (suffix) as a naming convention.

    * We simply need to identify the expected behaviors (methods); we don't need to know how people will implement them.

    * Unlike other OOP languages, there is no 'implements' keyword. When a type implements all of the  interface's methods, it automatically satisfies it.


Based on the Go 'interface' description above, we can apply the previous example's common action 'consume' to the powerConsumer interface.

Then, using the 'powerConsumer' abstract type, we may tract/iterate the actual data type, such as dishwasher and hair dryer.

dishwasher.go
type dishwasher struct {
    brand           string
    model           string
    numOfWashCycles int
    price           float64
}

func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
    return &dishwasher{
      brand:           brand,
      model:           model,
      numOfWashCycles: numOfWashCycles,
      price:           price,
    }
}

func (dw *dishwasher) consume() {
    fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}


hairdryer.go
type hairDryer struct {
    brand            string
    model            string
    numOfHeatSetting int
    price            float64
}

func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
    return &hairDryer{
      brand:            brand,
      model:            model,
      numOfHeatSetting: numOfHeatSetting,
      price:            price,
    }
}

func (hd *hairDryer) consume() {
    fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}


walloutlet.go
// Abstract type
type powerConsumer interface {
    consume()
}

type wallOutlet struct {
    // Connected Devices
    powerConsumers []powerConsumer
}

func newWallOutlet() *wallOutlet {
    return &wallOutlet{
      powerConsumers: []powerConsumer{},
    }
}

func (w *wallOutlet) plug(pc powerConsumer) {
    w.powerConsumers = append(w.powerConsumers, pc)
}

func (w *wallOutlet) sendPower() {
    fmt.Println("Wall Outlet")

    for _, pc := range w.powerConsumers {
      pc.consume()
    }
}


main.go
func main() {
    wallOutlet := newWallOutlet()
    hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
    dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)

    // Plug in devices
    wallOutlet.plug(hairDryer)
    wallOutlet.plug(dishwasher)

    wallOutlet.sendPower()
}


Result
 Wall Outlet
    -> The Dishwasher (Bosch - SHE3AEM2N) is powered
    -> The Hair Dryer (Dyson - Supersonic) is powered


From the example above, since dishwasher and hair dryer do implement consume() function, they automatically satisfy the 'powerComsumer' interface.


More details in Interface



Zero value of interface is nil.

Ex:
var p powerConsumer
  fmt.Printf("Zero value of interface: %v\n", p)


Result:
Zero value of interface: <nil>


An interface value (powerConsumer) wraps and hides the dynamic value (dishwasher).

You can change the dynamic value to different value in the runtime as long as the type of the assigned value can satisfy the interface.

Ex:
var p powerConsumer
  p = hairDryer
  p.consume()

  p = dishwasher
  p.consume()


Result:
  -> The Hair Dryer (Dyson - Supersonic) is powered
  -> The Dishwasher (Bosch - SHE3AEM2N) is powered


Using interface can help us to collect the data type supporting all the methods defined in the interface.

Then we can easily iterate the collection and call their methods which is defined in the interface.

But we will have troubles to access the fields or methods which is not defined in the interface since we do not know its real data type.

Ex:
// We can access the fields and methods through the concrete type
  hairDryer.changePrice(100)
 
  // But we cannot access the hidden fields or methods throguh interface
  p = hairDryer
  p.changePrice(100)


Compile Error:
  p.changePrice undefined (type powerConsumer has no field or
method changePrice)


From the example above, we added a changePrice() method into the hairDryer struct, and we want to call this method from the powerConsumer interface only if dynamic value type is the dishwasher struct type.

How can we do that? Go provides "type assertion" to extract the hidden value and its hidden type.


Type Assertion




Type assertion allows you to extract the dynamic value.

Format
pc.(*dishwasher)


    * pc is "the interface value" that you want to extract the dynamic value.

    * *dishwasher is the "type name" of the dynamic value that you want to extract.

    * It will return the hidden dishwasher value (dynamic value), and you can use this reference to access its fields or attach methods.


dishwasher.go
type dishwasher struct {
    brand           string
    model           string
    numOfWashCycles int
    price           float64
}

func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
    return &dishwasher{
      brand:           brand,
      model:           model,
      numOfWashCycles: numOfWashCycles,
      price:           price,
    }
}

func (dw *dishwasher) consume() {
    fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}

func (dw *dishwasher) print() {
    fmt.Println("Dishwasher - Specifications")
    fmt.Printf("\tBrand: %s\n", dw.brand)
    fmt.Printf("\tModel: %s\n", dw.model)
    fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
    fmt.Printf("\tPrice: %.2f\n", dw.price)
}


hairdryer.go
type hairDryer struct {
    brand            string
    model            string
    numOfHeatSetting int
    price            float64
}

func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
    return &hairDryer{
      brand:            brand,
      model:            model,
      numOfHeatSetting: numOfHeatSetting,
      price:            price,
    }
}

func (hd *hairDryer) consume() {
    fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}


walloutlet.go
// Abstract type
type powerConsumer interface {
    consume()
}

type wallOutlet struct {
    // Connected Devices
    powerConsumers []powerConsumer
}

func newWallOutlet() *wallOutlet {
    return &wallOutlet{
      powerConsumers: []powerConsumer{},
    }
}

func (w *wallOutlet) plug(pc powerConsumer) {
    w.powerConsumers = append(w.powerConsumers, pc)
}

func (w *wallOutlet) sendPower() {
    fmt.Println("Wall Outlet")

    for _, pc := range w.powerConsumers {
      pc.consume()
    }
}

func (w *wallOutlet) print() {
    fmt.Println("Wall Outlet Connected Devices")

    for _, pc := range w.powerConsumers {
      // Extract the dynamic value
      // Only dishwasher type support print method
      dw, isDishwasher := pc.(*dishwasher)
      if !isDishwasher {
        continue
      }

      dw.print()
    }
}


main.go
func main() {
    wallOutlet := newWallOutlet()
    hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
    dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)

    // Plug in devices
    wallOutlet.plug(hairDryer)
    wallOutlet.plug(dishwasher)

    wallOutlet.sendPower()

wallOutlet.print()
}


Result
 Wall Outlet
    -> The Dishwasher (Bosch - SHE3AEM2N) is powered
    -> The Hair Dryer (Dyson - Supersonic) is powered


Also, rather to inspecting types, we can use type assertion to determine whether the interface value contains the desired method.


walloutlet.go
func (w *wallOutlet) print2() {
    fmt.Println("Wall Outlet Connected Devices")

    // Declare the interface in function scope level
    type printer interface {
      print()
    }

    for _, pc := range w.powerConsumers {
      // Check whether the interface value include methods you want or not
      if p, ok := pc.(printer); ok {
          p.print()
      }
    }
}



Empty Interface



Format
type empty_interface_name interface {}


    * It does not have a method.
    * It is empty - it says nothing
    * Every type satisfies it.
    * It can represent any type of value.

We can use the simplified Format:

Format
interface {}


    * You can store any type of value in an empty interface value.

Ex:
  var emptyInterfaceVar interface{}

  // Slice
  emptyInterfaceVar = []int{1, 2, 3}
  fmt.Println(emptyInterfaceVar)

  // Map
  emptyInterfaceVar = map[int]bool{
    1: true,
    2: false,
  }
  fmt.Println(emptyInterfaceVar)

  // string
  emptyInterfaceVar = "Hello World"
  fmt.Println(emptyInterfaceVar)

  // int
  emptyInterfaceVar = 3
  fmt.Println(emptyInterfaceVar)


Result:
  [1 2 3]
  map[1:true 2:false]
  Hello World
  3


* But you cannot directly use the dynamic value of an empty interface value.

Ex:
  var emptyInterfaceVar interface{}

  // int
  emptyInterfaceVar = 3
  fmt.Println(emptyInterfaceVar)

  // We cannot directly use the dynamic value of an empty inteface value
  fmt.Println(emptyInterfaceVar * 3)


Result:
  invalid operation: emptyInterfaceVar * 3 (mismatched types interface{}
and int)


We need to extract the dynamic value by type assertion first.

Ex:
  var emptyInterfaceVar interface{}

  // int
  emptyInterfaceVar = 3

  // The dynamic type of 'emptyInterfaceVar' is int
  // The dynamic value of 'emptyInterfaceVar' is 3

  // We need to extract the dynamic value by type assertion before using it.
  fmt.Println(emptyInterfaceVar.(int) * 3)


Result:
  3
  9


Take the following as an example.

dishwasher.go
type dishwasher struct {
    brand           string
    model           string
    numOfWashCycles int
    price           float64
}

func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
    return &dishwasher{
      brand:           brand,
      model:           model,
      numOfWashCycles: numOfWashCycles,
      price:           price,
    }
}

func (dw *dishwasher) consume() {
    fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}

func (dw *dishwasher) print() {
    fmt.Println("Dishwasher - Specifications")
    fmt.Printf("\tBrand: %s\n", dw.brand)
    fmt.Printf("\tModel: %s\n", dw.model)
    fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
    fmt.Printf("\tPrice: %.2f\n", dw.price)
    pd := format(dw.purchaseDate)
    fmt.Printf("\tPurchase Date: %s\n", pd)
}

func format(v interface{}) string {
    if v == nil {
      return "unknown"
    }

    var t int
    if v, ok := v.(int); ok {
      t = v
    }

    if v, ok := v.(string); ok {
      t, _ = strconv.Atoi(v)
    }

    u := time.Unix(int64(t), 0)
    return u.String()
}


hairdryer.go
type hairDryer struct {
    brand            string
    model            string
    numOfHeatSetting int
    price            float64
}

func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
    return &hairDryer{
      brand:            brand,
      model:            model,
      numOfHeatSetting: numOfHeatSetting,
      price:            price,
    }
}

func (hd *hairDryer) consume() {
    fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}


walloutlet.go
// Abstract type
type powerConsumer interface {
    consume()
}

type wallOutlet struct {
    // Connected Devices
    powerConsumers []powerConsumer
}

func newWallOutlet() *wallOutlet {
    return &wallOutlet{
      powerConsumers: []powerConsumer{},
    }
}

func (w *wallOutlet) plug(pc powerConsumer) {
    w.powerConsumers = append(w.powerConsumers, pc)
}

func (w *wallOutlet) sendPower() {
    fmt.Println("Wall Outlet")

    for _, pc := range w.powerConsumers {
      pc.consume()
    }
}

func (w *wallOutlet) print() {
    fmt.Println("Wall Outlet Connected Devices")

    for _, pc := range w.powerConsumers {
      // Extract the dynamic value
      // Only dishwasher type support print method
      dw, isDishwasher := pc.(*dishwasher)
      if !isDishwasher {
        continue
      }

      dw.print()
    }
}


main.go
func main() {
    wallOutlet := newWallOutlet()
    hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
    dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)

    // Plug in devices
    wallOutlet.plug(hairDryer)
    wallOutlet.plug(dishwasher)

    wallOutlet.sendPower()

wallOutlet.print()
}


In summary, Go can let you write type safety code, but empty interface can break the rule and make the code hard to maintain.

Therefore, try not use it only if really necessary.


Type Switch



Detects and extracts the dynamic value from an interface value.

Format
  switch e := v.(type) {
    case int:
      // e is an int here
    case string:
      // e is a string here
    default:
      // e's type equals to v's type
  }


    * v is an interface value.

    * v.(type) will extract the type from the interface value v.

    * e is the extracted value.

    * The type of e will be changed depending on the extracted value.

    * Type switch compares types instead of values.

dishwasher.go
type dishwasher struct {
    brand           string
    model           string
    numOfWashCycles int
    price           float64
}

func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
    return &dishwasher{
      brand:           brand,
      model:           model,
      numOfWashCycles: numOfWashCycles,
      price:           price,
    }
}

func (dw *dishwasher) consume() {
    fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}

func (dw *dishwasher) print() {
    fmt.Println("Dishwasher - Specifications")
    fmt.Printf("\tBrand: %s\n", dw.brand)
    fmt.Printf("\tModel: %s\n", dw.model)
    fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
    fmt.Printf("\tPrice: %.2f\n", dw.price)
    pd := format(dw.purchaseDate)
    fmt.Printf("\tPurchase Date: %s\n", pd)
}

func format(v interface{}) string {
    var t int

    switch e := v.(type) {
    case int:
      t = e
    case string:
      t, _ = strconv.Atoi(e)
    default:
      return "unknown"
    }

    const layout = "1973, Oct"

    u := time.Unix(int64(t), 0)
    return u.Format(layout)
}

turn u.String()
}


No comments:

Post a Comment