Назначение интерфейсов

Пытаюсь понять, назначение интерфейсов. Написал такой пример:

package main

type Talker interface {
    Talk()
}

type Person1 struct {
    text string
}
type Person2 struct {
    text string
}

func (n *Person1) Talk() {
    println("person1 says", n.text)
}

func (n *Person2) Talk() {
    println("person2 says", n.text)
}

func Converse(l Talker) {
    l.Talk()
}

func main() {
    Converse(&Person1{"hello"})
    Converse(&Person2{"hi"})
}

И написав его, могу сформулировать назначение интерфеса так:

Если объект реализует не все методы интерфеса или реализует хотя бы один из методов не с той сигнатурой, что указана в интерфесе, то обьект не сможет иметь доступ к универсальным функциям обьектов этого интерфейса(в моем примере такая функция Converse(l Talker)).

Неужели, назначение интерфесов только в, своего рода, инкапсуляции обьекта и его данных? Выглядит так, как будто это защита от программиста, который не понимает, что он делает, и какими методами и функциями нужно обрабатывать объекты и данные, а интерфес, как бы, уточняет у него, "ты точно уверен, что этот объект должен обрабатываться именно этой функцией?" Раньше я не рассмтаривал интерфесы в этом ключе, так как всегда их преподносят как средство универсализации чего то и я не могу их понять, пока ищу эту универсализацию, потому что не наблюдаю универсализацию(методы Talk, все равно, нужно писать разные для каждого обьекта)

Дополняю по ответам:

    package main

import (
    "log"
    "os"
)

type MoveSaveReader interface {
    Copy()
    Save()
    Read()
}

type OBJ struct {
    data, filename, newpath string
}

type NewObj struct {
    data, path string
}

func (o *NewObj) Copy() {
}

func (o *NewObj) Save() {
    err := os.WriteFile(o.path, []byte(o.data), 6060)
    if err != nil {
        log.Println(err)
        return
    }
    log.Println("Saved")
}

func (o *NewObj) Read() {
    d, err := os.ReadFile(o.path)
    if err != nil {
        log.Println(err)
        return
    }
    o.data = string(d)
}

func (o *OBJ) Copy() {
    o.Read()
    o.filename = o.newpath
    o.Save()
}

func (o *OBJ) Save() {
    err := os.WriteFile(o.filename, []byte(o.data), 6060)
    if err != nil {
        log.Println(err)
        return
    }
    log.Println("Saved")
}

func (o *OBJ) Read() {
    d, err := os.ReadFile(o.filename)
    if err != nil {
        log.Println(err)
        return
    }
    o.data = string(d)
}

func CopyFile(m MoveSaveReader) {
    m.Copy()
}

func WriteNewFile(m MoveSaveReader) {
    m.Save()
}

func ReadFile(m MoveSaveReader) {
    m.Read()
}

func main() {
    obj := &OBJ{"sometext", "path", "newpath"}
    WriteNewFile(obj)
    ReadFile(obj)
    CopyFile(obj)
    newObj := &NewObj{"newtext", "newSomePAth"}
    WriteNewFile(newObj)
    ReadFile(newObj)
}

Объекты , у которых нет хотя бы одно из методов MoveSaveReader-а, а именно: Copy(), Save(), Read() или методы есть но хотя бы один неправильно сигнатуры, то такой объект защищен от ошибочного помещения в функции CopyFile(m MoveSaveReader), ReadFile(m MoveSaveReader), WriteFile(m MoveSaveReader). Т.е с помощью интерфесов я больше не универсализирую обработку данных а защищаю?


Ответы (1 шт):

Автор решения: Pak Uula

Если коротко, то основное назначение интерфейсов - реализация парадигмы type safe duck typing.

Если более развёрнуто.

Не знаю, как вы, а я познакомился с интерфейсами в языке Java. Там интерфейс - это такой тип, у которого есть только сигнатуры методов, нет ни данных, ни тел методов. Если какой-то класс реализует интерфейс, то в нём есть все методы интерфейса. Например интерфейс Closable, в котором всего один метод void close(). В стандартной библиотеке Java несколько десятков классов, реализующих интерфейс Closable, и в каждом, что характерно, есть метод close.

В чём различие между интерфейсами java и go, и причём тут duck typing.

В Java связь между интерфейсом и типом слева направо: если тип реализует интерфейс, значит, в типе есть все методы интерфейса. В go направление обратное. Если в типе реализованы все методы интерфейса, то тип реализует интерфейс.

Разница, на самом деле, огромная. В Java тип реализует только те интерфейсы, которые перечислили разработчики класса. Если они что-то не упомянули, то хоть ты тресни, но привести тип к нужному интерфейсу не получится.

Пример. Восхотелось мне задать интерфейс

interface Writable {
    void write(char[] cbuf, int off, int len) throws IOException;
}

В библиотеке java.io есть класс Writer в котором, среди прочего, есть метод Writer.write с как раз подходящей сигнатурой. Означает ли это, что объекты класса java.io.Writer заодно являются объектами типа Writable? Нет, не означает, так как создатели класса Writer не знали о существовании моего интерфейса Writable, не указали его в разделе implements, и, тем самым, не включили в список типов объектов Writer.

В Go ситуация обратная. При создании типа никто не перечисляет интерфейсы, которые тип реализует. При проверке приводимости объекта к интерфейсу компилятор проверяет, чтобы у типа были все необходимые методы. Если методы есть, значит тип реализует интерфейс. Так, все типы, имеющие метод func (T) Write([]byte)(int,error) реализуют интерфейс io.Writer просто потому, что реализуют небходимый метод. Даже если сами не знают об этом.

Выглядит как утка, ходит как утка, крякает как утка - значит, утка.

Теперь про type safety.

Duck typing встречается сплошь и рядом в интерпретируемых языках. Python, Javascript, Ruby, Lisp, ... несть им числа. В любом из этих языков вы можете написать что-то вроде stream.write(buf) (в случае Лиспа это будет (write stream buf)), и молиться про себя, чтобы у stream таки нашелся метод write, который умеет обрабатывать объекты типа buf. Если ваша молитва услышана, то всё хорошо, программа работает. Но если нужного метода нет, то вы узнаете об этом только во время выполнения программы. Особенно обидно, если этот write вызывается в самом конце длииииинного вычисления, и все результаты бессмысленно теряются из-за того, что stream не той системы.

В go компилятор проверяет, что в объекте есть необходимые методы: совпадают имена, типы параметров, типы возвращаемых значений. Если хоть что-то не совпало, то вы об этом узнаете ещё на этапе компиляции. Программа просто не соберётся.

Так, если вы напишете что-то вроде func save_results(data Data, sink io.Writable) { ...; _, err := sink.Write(data.Bytes()); ... } то можете быть железно уверены, что в объекте sink есть метод с именем Write, и этот метод

  • принимает единственный параметр типа []byte,
  • возвращает два значения,
  • и тип второго значения error.

Если этот код сломается во время выполнения, то не по причине отсутствия нужного метода.

→ Ссылка