Интерфейсы нужны для описания требований к поведению объектов без уточнения типов самих объектов.
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
из стандартной библиотеки:
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)
}
}
Интерфейс -- это полезный инструмент, позволяющий решать проблемы связности, архитектурного развития, изоляции.