lesson03.md 7.9 KB

Интерфейсы

Интерфейсы нужны для описания требований к поведению объектов без уточнения типов самих объектов.

flowchart TD
    ObjType1 --> Interface1
    ObjType2 --> Interface1
    Interface1 --> func1
    ObjType2 --> Interface2
    Interface2 --> func2
// Интерфейс может только возвращать строку (ObjType1)
type Interface1 interface{
    Get()string
}

// Включает в себя интерфейс на чтение, расширяет отправкой (ObjType2)
type Interface2 interface{
    Interface1
    Write(string)error
}

func1 не различает фактические типы ObjType1 и ObjType2 -- для неё это один тип Interface1.

Что даёт интерфейс

  • гарантия контракта поведения объекта;
  • не нужно изменять существующий алгоритм при передаче объекта нового типа;
  • создание возможностей тестирования на разных этапах;
  • скрытие деталей реализации объекта.

Контракт поведения

Контракт поведения -- это гарантия того, что объект передаваемый как параметр вызова обладает нужным поведением. Контракт поведения -- это набор методов и их аргументов с типами для успешной передачи.

Разные вызовы требуют разных контрактов (наборов методов), таки образом один и тот же объект может удовлетворять разным интерфейсам, например:

type IWriter interface {
    Write(fileName string, binData []byte)error
}

type IReader interface {
    Read(fileName string)([]bte, error)
}

type IWriterReader interface {
    IWriter
    IReader
}

func write(wr IWriter){
    _ = wr.Write("test.txt")
}

func read(rd IReader){
    _, _ = rd.Read("test.txt")
}

func rewrite(rw IWriterReader){
    binData,_=rw.Read("test.txt")
    binData = append(binData, []byte("hello\n"))
    _ = rw.Write("test.txt", binData)
}

Вызову на чтение -- не интересна возможность записи.

Вызову на запись -- не интересна возможность чтения.

Вызов на запись и чтение одновременно требует обоих методов.

Объект передаваемый во все три вызова может удовлетворять как конкретным интерфейсу, так и всем сразу.

Конкретный вызов с требованием интерфейса:

  • нужное поведение требует;
  • лишнее поведение проигнорирует и обратиться к такому неописанному поведению из интерфейса невозможно.

Сохранение алгоритма

Алгоритм может быть расположен в вендоринге. Алгоритм сложный, специфичный, обобщённый и покрыт тестами. Как передать туда объект нового типа -- только через интерфейс.

Типичный пример: пакет sort из стандартной библиотеки:

Описание swap

type Sort interface {
    Less(adr0, adr1)bool
    Swap(adr0, adr1)
    Len()int
}

Не важно какой тип реализует этот интерфейс -- он будет иметь возможность сортировки оптимальным способом.

Объекты разные -- алгоритм один.

Тестирование

Интерфейсы пригодятся:

  • нужна БД, а она недоступна на локальной машине разработчика;
  • нужно имитировать отсутствие связи с шиной данных;
  • нужно имитировать внезапный сбой.

Пример:

type ILink interface {
    Get()(string, error)
}

func Get(link ILink)(string, error) {
    resp,err:=link.Get()
    return resp, err
}

type Link struct{}

func (sf *Link)Get()(string, error){
    . . . // Вот здесь на реальном клиенте НЕВОЗМОЖНО имитировать обрыв связи
    return resp, err
}

type MockLink struct{
    IsBadLink_ bool // Устанавливаемый извне признак отсутствия связи для тестов
}

func (sf *MockLink)Get()(string, error){
    if sf.IsLinkBad {
        return "", fmt.Errorf("MockLink.Get(): IsBadLink_==true")
    }
    return "ok", nil
}

Подставляя во время тестов MockLink можно смоделировать любую ситуацию сбоя реального клиента.

Подставляя любые структуры мок-клиента БД -- можно смоделировать любую ситуацию с данными в БД (в том числе -- обрыв связи, испорченные данные, неожиданные данные, задержки при перегрузках базы и т.п.)

Скрытие деталей реализации объекта

Это полезное свойство заставляет выделять существенные части объекта и защищать внутренее устройство объекта.

Пример:


type ILink interface {
    Get()(string, error)
}

func GetInterface(link ILink)(string, error) {
    resp, err := link.Get()
    return resp, err
}

type Link struct{
    Url string // Вот это поле при прямом доступе можно легко испортить
}

func GetInterface(link Link)(string, error) {
    link.Url = "bad_url"
    resp, err := link.Get() // Здесь с вероятность 146% будет ошибка
    return resp, err
}

В первом случае -- невозможно испортить (намеренно или необдуманно) внутреннее состояние объекта (интерфейс не имеет состояния).

Во втором случае -- работа программы будет нарушена неизбежно и последствия ошибки будут распространены на всю программу.

Как вернуть объект

Интерфейс не скрывает до конца объект. Но если вызывающая сторона не знает о реальном типе объекта -- она его не получит.

Пример:

func Get(link ILink)(string, error){
    switch link.(type){
        case Link:
            ...
        case LinkProxy:
            ...
        case LinkForward:
            ...
        default:
            return "", fmt.Errorf("Get(): unknown type ILink(%#v)", link)
    }
}

Проблемы интерфейсов

  • сложнее отлаживать (нужен отладочный вывод или отличный отладсик в IDE);
  • нужно выработать привычку делать узкие интерфейсы;
  • можно случайно удовлетворить требованиям интерфейса.

Интерфейс -- это полезный инструмент, позволяющий решать проблемы связности, архитектурного развития, изоляции.