Інтерфейси в Go

10 хв. читання

Об'єктно-орієнтована модель Go побудована навколо інтерфейсів. Інтерфейси є важливою конструкцією мови, і всі проектні рішення мають бути орієнтовані, в першу чергу, на них.

Ви дізнаєтеся, що таке інтерфейс у Go, як їх реалізувати і які обмеження мають інтерфейси на відміну від контрактів.

Що таке інтерфейс у Go?

Інтерфейс – тип даних, який містить колекцію визначених, але не імплементованих(не реалізованих) методів.

Інтерфейс у Go має такий вигляд:

type Serializer interface {
    Serialize() (string, error)
    Deserialize(s string) error
}

Serializer – інтерфейс, який має 2 методи:

  • Serialize() – не приймає аргументів і повертає string та error;
  • Deserialize(s string) – приймає аргумент s, і повертає error.

Можливо, інтерфейс Serializable може здатися вам знайомим, і ви вже здогадалися, що метод Serialize() повертає серіалізовану версію цільового об'єкта, який може бути відтворений викликом Deserialize() і передасть оригінальний виклик Serialize().

Зверніть увагу, що вам не потрібно використовувати ключове слово func перед кожним оголошенням методу. Go знає, що інтерфейс може містити тільки методи і не потребує явного визначення методу ключовим словом.

Досвід використання

Інтерфейси Go – найліпший спосіб побудови кістяка вашої програми, адже у такому випадку, об'єкти мають взаємодіяти між собою через інтерфейси. Це означає, що ви маєте створити об'єктну модель своєї програми, яка складається тільки з інтерфейсів та базових типів або об'єктів з даними (структури, члени яких є базовими типами, або іншими об'єктами даних). Нижче приведено одні з найкращих прикладів того, що ви повинні знати для використання інтерфейсів:

Прозорість намірів

Дуже важливо, щоб причина створення кожного методу та послідовності викликів методів були зрозумілими, прозорими та чітко визначеними як для використання, так і для імплементації. На жаль, Go не підтримує це на рівні мови.

Впровадження залежностей

Якщо один об'єкт взаємодіє з іншим через інтерфейс, то він отримає інтерфейс ззовні, як аргумент функції або методу і не створить об'єкт. Пам'ятайте, цей принцип застосовується не тільки до об'єктів, а також і до окремих функцій. Функція має отримувати всі свої залежності через інтерфейс.

Наприклад:

type SomeInterface {
   DoSomethingAesome()
}
   func foo(s SomeInterface) {
   s.DoSomethingAwesome()
}

Тепер, під час виклику, функція foo() буде працювати з різними реалізаціями інтерфейсу SomeInterface.

Фабрики

Очевидно, що хтось має створювати об'єкти, тому цим займаються окремі об'єкти – фабрики.

Вони використовуються у двох випадках:

  1. На початку роботи програми фабрика використовується для створення об'єктів, які потрібні на протязі усього часу роботи.
  2. Програма під час виконання потребує динамічного створення об'єктів. Для цього також слід використовувати фабрики.

Зазвичай корисно впровадити динамічний інтерфейс фабрики для об'єктів, щоб підтримувати патерн взаємодії тільки через інтерфейси. У наступному прикладі приведено інтерфейси Wiget і WigetFactory. Останній повертає інтерфейс Wiget методом CreateWidget().

Функція PerformMainLogic() отримує інтерфейс WidgetFactory з модуля, що викликається. Тепер він здатний динамічно створювати новий віджет на основі специфікації й викликати метод Widgetize(), не знаючи нічого про його конкретний тип.

type Widget interface {
   Widgetize()
}
   type WidgetFactory interface {
   CreateWidget(widgetSpec string) (Widget, error)
}
 func PerformMainLogic(factory WidgetFactory) {
    ...
    widgetSpec := GetWidgetSpec()
    widget := factroy.CreateWidget(widgetSpec)
    widget.Widgetize()
}

Тестування

Тестування – один з найважливіших етапів розробки програмного забезпечення. Інтерфейси у Go є найкращим механізмом підтримки тестування у програмах. Щоб ретельно протестувати функцію або метод, потрібно контролювати всі вхідні та вихідні дані, а також побічні ефекти тестованої функції.

Для нетривіального коду, який безпосередньо пов'язаний з файловою системою, системними годинниками, базами даних, віддаленими службами та користувацьким інтерфейсом, дуже складно досягти такого рівня контролю. Але, якщо взаємодія відбувається через інтерфейси, то дуже легко знаходити й керувати зовнішніми залежностями.

Розглянемо функцію, що відпрацьовує лише наприкінці місяця, і запускає код, який очищує невдалі транзакції. Без інтерфейсів вам доведеться йти на екстремальні заходи, такі як зміна фактичного комп'ютерного годинника для імітації кінця місяця. За допомогою інтерфейсу, який відтворює потрібний вам час, ви можете просто передати структуру, яка вже містить потрібний час.

Замість імпорту часу і прямого виклику методу time.Now(), ви зможете передати інтерфейс за допомогою методу Now(), який буде реалізовано шляхом посилання на time.Now(), але при тестуванні, буде імплементовано об'єкт, який буде повертати фіксований час, щоб заморозити оточення під час тесту.

Використання інтерфейсу

Використання інтерфейсу у Go повністю очевидне. Ви просто називаєте його методи так, як ви називаєте будь-яку іншу функцію, проте, різниця в тому, що ви не можете бути певні, що станеться, оскільки реалізації методів можуть відрізнятися.

Реалізація інтерфейсів

Інтерфейси у Go можуть бути реалізовані як методи структур. Розглянемо наступний інтерфейс:

type Shape interface {
   Perimeter() int
   Area() int
}

Дві окремі реалізації інтерфейсу Shape:

type Square struct {
   side  uint
}
 
func (s *Square) Perimeter() uint {
   return s.side * 4
}
 
func (s *Square) Area() uint {
   return s.side * s.side
}
  
type Rectangle struct {
   width  uint
   height uint
}
 
func (r *Rectangle) Perimeter() uint {
    return (r.width + r.height) * 2
}
 
func (r *Rectangle) Area() uint {
    return r.width * r.height
}

Квадрат і прямокутник реалізують розрахунки по-різному, виходячи з власних полів та геометричних властивостей. Наступний код демонструє, як заповнити частину інтерфейсу Shape з конкретними об'єктами, що його реалізують, а потім ітерувати по частинах і викликати метод Area() для кожної форми, щоб обчислити загальну площу.

func main() {
   shapes := []Shape{&Square{side: 2}, 
    &Rectangle{width: 3, height: 5}}
    var totalArea uint
 
    for _, shape := range shapes {
        totalArea += shape.Area()
    }
     
    fmt.Println("Total area: ", totalArea)
}

Базова реалізація

Більшість мов програмування мають таке поняття, як базовий клас, який може бути використаний для реалізації спільного функціоналу, він може бути використаний всіма підкласами. Go віддає перевагу композиції перед спадкуванням.

Ви можете отримати подібний ефект шляхом вставки структури. Визначимо структуру Cache, яка зможе зберігати значення попередніх обчислень.

Якщо значення отримується з поля, то друкується повідомлення cache hit, якщо ж значення відсутнє, то на екран виводиться cache miss і повертається -1.

type Cache struct {
   cache map[string]uint
}
 
func (c *Cache) GetValue(name string) int {
   value, ok := c.cache[name]
    if ok {
       fmt.Println("cache hit")
       return int(value)
    } else {
       fmt.Println("cache miss")
       return -1
    } 
}
 
func (c *Cache) SetValue(name string, value uint) {
   c.cache[name] = value
}

Тепер під'єднаємо цей кеш до Square і Rectangle. Зауважте, що реалізація Perimetr() i Area() тепер, спершу перевіряє кеш і обчислює значення тільки у випадку його відсутності у кеші.

type Square struct {
   Cache
   side  uint
}
  
func (s *Square) Perimeter() uint {
   value := s.GetValue("perimeter")
    if value == -1 {
       value = int(s.side * 4)
       s.SetValue("perimeter", uint(value))
    }
    return uint(value)  
}
 
func (s *Square) Area() uint {
   value := s.GetValue("area")
    if value == -1 {
       value = int(s.side * s.side)
        s.SetValue("area", uint(value))
    }
    return uint(value) 
}
  
type Rectangle struct {
   Cache
   width  uint
   height uint
}
 
func (r *Rectangle) Perimeter() uint {
   value := r.GetValue("perimeter")
   if value == -1 {
       value = int(r.width + r.height) * 2
       r.SetValue("perimeter", uint(value))
   }
   return uint(value)
}
 
func (r *Rectangle) Area() uint {
   value := r.GetValue("area")
   if value == -1 {                
      value = int(r.width * r.height)
      r.SetValue("area", uint(value))
   }
   return uint(value) 
}

Функція main() двічі обчислює загальну площу, щоб можна було побачити ефект від кешу.

func main() {
   shapes := []Shape{
      &Square{Cache{cache: make(map[string]uint)}, 2}, 
      &Rectangle{Cache{cache: make(map[string]uint)}, 3, 5}
    }
    var totalArea uint
    for _, shape := range shapes {
       totalArea += shape.Area()
    }
   fmt.Println("Total area: ", totalArea)
   totalArea = 0
   for _, shape := range shapes {
      totalArea += shape.Area()
    }
      fmt.Println("Total area: ", totalArea)

Результат:

cache miss
cache miss
Total area:  19
cache hit
cache hit
Total area:  19

Інтерфейс vs Контракт

Інтерфейси – чудовий інструмент, проте вони не гарантують того, що структура, яка реалізує інтерфейс дійсно буде побудована згідно з намірами розробника. Go не має жодного способу виражати цей намір. Все, що ви можете вказати – це підписати методи для чіткішого розуміння їх призначення.

Для виходу за рамки базового рівня вам потрібен контракт. Контракт об'єкту вказує точно, що має робити метод, які побічні ефекти може мати його застосування, і який стан має об'єкт в окремі моменти часу. Контракт завжди існує у явному, чи не явному вигляді. У випадку зовнішніх API контракти є критично необхідними.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація