One place for hosting & domains

      Interfaces

      How To Use Interfaces in TypeScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      TypeScript is an extension of the JavaScript language that uses JavaScript’s runtime with a compile-time type checker.

      TypeScript offers multiple ways to represent objects in your code, one of which is using interfaces. Interfaces in TypeScript have two usage scenarios: you can create a contract that classes must follow, such as the members that those classes must implement, and you can also represent types in your application, just like the normal type declaration. (For more about types, check out How to Use Basic Types in TypeScript and How to Create Custom Types in TypeScript.)

      You may notice that interfaces and types share a similar set of features; in fact, one can almost always replace the other. The main difference is that interfaces may have more than one declaration for the same interface, which TypeScript will merge, while types can only be declared once. You can also use types to create aliases of primitive types (such as string and boolean), which interfaces cannot do.

      Interfaces in TypeScript are a powerful way to represent type structures. They allow you to make the usage of those structures type-safe and document them simultaneously, directly improving the developer experience.

      In this tutorial, you will create interfaces in TypeScript, learn how to use them, and understand the differences between normal types and interfaces. You will try out different code samples, which you can follow in your own TypeScript environment or the TypeScript Playground, an online environment that allows you to write TypeScript directly in the browser.

      Prerequisites

      To follow this tutorial, you will need:

      • An environment in which you can execute TypeScript programs to follow along with the examples. To set this up on your local machine, you will need the following.
      • If you do not wish to create a TypeScript environment on your local machine, you can use the official TypeScript Playground to follow along.

      • You will need sufficient knowledge of JavaScript, especially ES6+ syntax, such as destructuring, rest operators, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.

      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

      All examples shown in this tutorial were created using TypeScript version 4.2.2.

      Creating and Using Interfaces in TypeScript

      In this section, you will create interfaces using different features available in TypeScript. You will also learn how to use the interfaces you created.

      Interfaces in TypeScript are created by using the interface keyword followed by the name of the interface, and then a {} block with the body of the interface. For example, here is a Logger interface:

      interface Logger {
        log: (message: string) => void;
      }
      

      Similar to creating a normal type using the type declaration, you specify the fields of the type, and their type, in the {}:

      interface Logger {
        log: (message: string) => void;
      }
      

      The Logger interface represents an object that has a single property called log. This property is a function that accepts a single parameter of type string and returns void.

      You can use the Logger interface as any other type. Here is an example creating an object literal that matches the Logger interface:

      interface Logger {
        log: (message: string) => void;
      }
      
      const logger: Logger = {
        log: (message) => console.log(message),
      };
      

      Values using the Logger interface as their type must have the same members as those specified in the Logger interface declaration. If some members are optional, they may be omitted.

      Since values must follow what is declared in the interface, adding extraneous fields will cause a compilation error. For example, in the object literal, try adding a new property that is missing from the interface:

      interface Logger {
        log: (message: string) => void;
      }
      
      const logger: Logger = {
        log: (message) => console.log(message),
        otherProp: true,
      };
      

      In this case, the TypeScript Compiler would emit error 2322, as this property does not exist in the Logger interface declaration:

      Output

      Type '{ log: (message: string) => void; otherProp: boolean; }' is not assignable to type 'Logger'. Object literal may only specify known properties, and 'otherProp' does not exist in type 'Logger'. (2322)

      Similar to using normal type declarations, properties can be turned into an optional property by appending ? to their name.

      Extending Other Types

      When creating interfaces, you can extend from different object types, allowing your interfaces to include all the type information from the extended types. This enables you to write small interfaces with a common set of fields and use them as building blocks to create new interfaces.

      Imagine you have a Clearable interface, such as this one:

      interface Clearable {
        clear: () => void;
      }
      

      You could then create a new interface that extends from it, inheriting all its fields. In the following example, the interface Logger is extending from the Clearable interface. Notice the highlighted lines:

      interface Clearable {
        clear: () => void;
      }
      
      interface Logger extends Clearable {
        log: (message: string) => void;
      }
      

      The Logger interface now also has a clear member, which is a function that accepts no parameters and returns void. This new member is inherited from the Clearable interface. It is the same as if we did this:

      interface Logger {
        log: (message: string) => void;
        clear: () => void;
      }
      

      When writing lots of interfaces with a common set of fields, you can extract them to a different interface and change your interfaces to extend from the new interface you created.

      Returning to the Clearable example used previously, imagine that your application needs a different interface, such as the following StringList interface, to represent a data structure that holds multiple strings:

      interface StringList {
        push: (value: string) => void;
        get: () => string[];
      }
      

      By making this new StringList interface extend the existing Clearable interface, you are specifying that this interface also has the members set in the Clearable interface, adding the clear property to the type definition of the StringList interface:

      interface StringList extends Clearable {
        push: (value: string) => void;
        get: () => string[];
      }
      

      Interfaces can extend from any object type, such as interfaces, normal types, and even classes.

      Interfaces with Callable Signature

      If the interface is also callable (that is, it is also a function), you can convey that information in the interface declaration by creating a callable signature.

      A callable signature is created by adding a function declaration inside the interface that is not bound to any member and by using : instead of => when setting the return type of the function.

      As an example, add a callable signature to your Logger interface, as in the highlighted code below:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      

      Notice that the callable signature resembles the type declaration of an anonymous function, but in the return type you are using : instead of =>. This means that any value bound to the Logger interface type can be called directly as a function.

      To create a value that matches your Logger interface, you need to consider the requirements of the interface:

      1. It must be callable.
      2. It must have a property called log that is a function accepting a single string parameter.

      Let’s create a variable called logger that is assignable to the type of your Logger interface:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      To match the Logger interface, the value must be callable, which is why you assign the logger variable to a function:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      You are then adding the log property to the logger function:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = (message: string) => {
        console.log(message);
      }
      

      This is required by the Logger interface. Values bound to the Logger interface must also have a log property that is a function accepting a single string parameter and that returns void.

      If you did not include the log property, the TypeScript Compiler would give you error 2741:

      Output

      Property 'log' is missing in type '(message: string) => void' but required in type 'Logger'. (2741)

      The TypeScript Compiler would emit a similar error if the log property in the logger variable had an incompatible type signature, like setting it to true:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      
      const logger: Logger = (message: string) => {
        console.log(message);
      }
      logger.log = true;
      

      In this case, the TypeScript Compiler would show error 2322:

      Output

      Type 'boolean' is not assignable to type '(message: string) => void'. (2322)

      A nice feature of setting variables to have a specific type, in this case setting the logger variable to have the type of the Logger interface, is that TypeScript can now infer the type of the parameters of both the logger function and the function in the log property.

      You can check that by removing the type information from the argument of both functions. Notice that in the highlighted code below, the message parameters do not have a type:

      interface Logger {
        (message: string): void;
        log: (message: string) => void;
      }
      
      
      const logger: Logger = (message) => {
        console.log(message);
      }
      logger.log = (message) => {
        console.log(message);
      }
      

      And in both cases, your editor should still be able to show that the type of the parameter is a string, as this is the type expected by the Logger interface.

      Interfaces with Index Signatures

      You can add an index signature to your interface, just like you can with normal types, thus allowing the interface to have an unlimited number of properties.

      For example, if you wanted to create a DataRecord interface that has an unlimited number of string fields, you could use the following highlighted index signature:

      interface DataRecord {
        [key: string]: string;
      }
      

      You can then use the DataRecord interface to set the type of any object that has multiple parameters of type string:

      interface DataRecord {
        [key: string]: string;
      }
      
      const data: DataRecord = {
        fieldA: "valueA",
        fieldB: "valueB",
        fieldC: "valueC",
        // ...
      };
      

      In this section, you created interfaces using different features available in TypeScript and learned how to use the interfaces you created. In the next section, you’ll learn more about the differences between type and interface declarations, and gain practice with declaration merging and module augmentation.

      Differences Between Types and Interfaces

      So far, you have seen that the interface declaration and the type declaration are similar, having almost the same set of features.

      For example, you created a Logger interface that extended from a Clearable interface:

      interface Clearable {
        clear: () => void;
      }
      
      interface Logger extends Clearable {
        log: (message: string) => void;
      }
      

      The same type representation can be replicated by using two type declarations:

      type Clearable = {
        clear: () => void;
      }
      
      type Logger = Clearable & {
        log: (message: string) => void;
      }
      

      As shown in the previous sections, the interface declaration can be used to represent a variety of objects, from functions to complex objects with an unlimited number of properties. This is also possible with type declarations, even extending from other types, as you can intersect multiple types together using the intersection operator &.

      Since type declarations and interface declarations are so similar, you’ll need to consider the specific features unique to each one and be consistent in your codebase. Pick one to create type representations in your codebase, and only use the other one when you need a specific feature only available to it.

      For example, the type declaration has a few features that the interface declaration lacks, such as:

      • Union types.
      • Mapped types.
      • Alias to primitive types.

      One of the features available only for the interface declaration is declaration merging, which you will learn about in the next section. It is important to note that declaration merging may be useful if you are writing a library and want to give the library users the power to extend the types provided by the library, as this is not possible with type declarations.

      Declaration Merging

      TypeScript can merge multiple declarations into a single one, enabling you to write multiple declarations for the same data structure and having them bundled together by the TypeScript Compiler during compilation as if they were a single type. In this section, you will see how this works and why it is helpful when using interfaces.

      Interfaces in TypeScript can be re-opened; that is, multiple declarations of the same interface can be merged. This is useful when you want to add new fields to an existing interface.

      For example, imagine that you have an interface named DatabaseOptions like the following one:

      interface DatabaseOptions {
        host: string;
        port: number;
        user: string;
        password: string;
      }
      

      This interface is going to be used to pass options when connecting to a database.

      Later in the code, you declare an interface with the same name but with a single string field called dsnUrl, like this one:

      interface DatabaseOptions {
        dsnUrl: string;
      }
      

      When the TypeScript Compiler starts reading your code, it will merge all declarations of the DatabaseOptions interface into a single one. From the TypeScript Compiler point of view, DatabaseOptions is now:

      interface DatabaseOptions {
        host: string;
        port: number;
        user: string;
        password: string;
        dsnUrl: string;
      }
      

      The interface includes all the fields you initially declared, plus the new field dsnUrl that you declared separately. Both declarations have been merged.

      Module Augmentation

      Declaration merging is helpful when you need to augment existing modules with new properties. One use-case for that is when you are adding more fields to a data structure provided by a library. This is relatively common with the Node.js library called express, which allows you to create HTTP servers.

      When working with express, a Request and a Response object are passed to your request handlers (functions responsible for providing a response to a HTTP request). The Request object is commonly used to store data specific to a particular request. For example, you could use it to store the logged user that made the initial HTTP request:

      const myRoute = (req: Request, res: Response) => {
        res.json({ user: req.user });
      }
      

      Here, the request handler sends back to the client a json with the user field set to the logged user. The logged user is added to the request object in another place in the code, using an express middleware responsible for user authentication.

      The type definitions for the Request interface itself does not have a user field, so the above code would give the type error 2339:

      Property 'user' does not exist on type 'Request'. (2339)
      

      To fix that, you have to create a module augmentation for the express package, taking advantage of declaration merging to add a new property to the Request interface.

      If you check the type of the Request object in the express type declaration, you will notice that it is an interface added inside a global namespace called Express, as shown in documentation from the DefinitelyTyped repository:

      declare global {
          namespace Express {
              // These open interfaces may be extended in an application-specific manner via declaration merging.
              // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
              interface Request {}
              interface Response {}
              interface Application {}
          }
      }
      

      Note: Type declaration files are files that only contain type information. The DefinitelyTyped repository is the official repository to submit type declarations for packages that do not have one. The @types/<package> packages available on npm are published from this repository.

      To use module augmentation to add a new property to the Request interface, you have to replicate the same structure in a local type declaration file. For example, imagine that you created a file named express.d.ts like the following one and then added it to the types option of your tsconfig.json:

      import 'express';
      
      declare global {
        namespace Express {
          interface Request {
            user: {
              name: string;
            }
          }
        }
      }
      

      From the TypeScript Compiler point of view, the Request interface has a user property, with their type set to an object having a single property called name of type string. This happens because all the declarations for the same interface are merged.

      Suppose you are creating a library and want to give the users of your library the option to augment the types provided by your own library, like you did above with express. In that case, you are required to export interfaces from your library, as normal type declarations do not support module augmentation.

      Conclusion

      In this tutorial, you have written multiple TypeScript interfaces to represent various data structures, discovered how you can use different interfaces together as building blocks to create powerful types, and learned about the differences between normal type declarations and interfaces. You can now start writing interfaces for data structures in your codebase, allowing you to have type-safe code as well as documentation.

      For more tutorials on TypeScript, check out our How To Code in TypeScript series page.



      Source link

      Como usar interfaces em Go


      Introdução

      Escrever um código flexível, reutilizável e modular é vital para o desenvolvimento de programas versáteis. Trabalhar dessa maneira garante que o código seja mais fácil de manter, evitando assim a necessidade de fazer a mesma mudança em vários locais. A maneira de se conseguir isso irá variar de uma linguagem para outra. Por exemplo, a herança é uma abordagem comum usada em linguagens como Java, C++, C#, entre outras.

      Os desenvolvimentos também podem atingir esses mesmos objetivos de design através da composição. A composição é uma maneira de combinar objetos ou tipos de dados em um ambiente mais complexo. Esta é a abordagem que a linguagem Go usa para promover a reutilização, modularidade e flexibilidade dos códigos. As interfaces em Go proporcionam um método de organizar composições complexas. Aprender como usá-las permitirá que você crie um código comum e reutilizável.

      Neste artigo, vamos aprender como compor tipos personalizados que tenham comportamentos comuns, os quais nos permitirão reutilizar o nosso código. Também vamos aprender como implementar interfaces para nossos próprios tipos personalizados que irão atender interfaces definidas de outro pacote.

      Definindo um comportamento

      Uma das implementações principais da composição é o uso das interfaces. Uma interface define um comportamento de um tipo. Uma das interfaces mais comuns usadas na biblioteca padrão do Go é a interface fmt.Stringer:

      type Stringer interface {
          String() string
      }
      

      A primeira linha de código define um type chamado Stringer. Em seguida, ele declara que ele é uma interface. Assim como definir uma struct, o Go usa chaves ({}) para cercar a definição da interface. Em comparação com a definição das structs, definimos apenas o comportamento da interface; ou seja, “o que esse tipo pode fazer”.

      No caso da interface Stringer, o único comportamento é o método String(). O método não aceita argumentos e retorna uma string.

      Em seguida, vamos examinar um código que tem o comportamento fmt.Stringer:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          fmt.Println(a.String())
      }
      

      A primeira coisa que vamos fazer é criar um novo tipo chamado Article. Esse tipo tem um campo Title e um campo Author e ambos são do tipo de dados string:

      main.go

      ...
      type Article struct {
          Title string
          Author string
      }
      ...
      

      Em seguida, definimos um método chamado String no tipo Article. O método String retornará uma string que representa o tipo Article:

      main.go

      ...
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      ...
      

      Então, em nossa função main, criamos uma instância do tipo Article e a atribuímos à variável chamada a. Informamos os valores de "Understanding Interfaces in Go" para o campo Title, e "Sammy Shark" para o campo Author:

      main.go

      ...
      a := Article{
          Title: "Understanding Interfaces in Go",
          Author: "Sammy Shark",
      }
      ...
      

      Em seguida, imprimimos o resultado do método String, chamando fmt.Println e enviando o resultado da chamada do método a.String():

      main.go

      ...
      fmt.Println(a.String())
      

      Após executar o programa, você verá o seguinte resultado:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark.

      Até agora, não usamos uma interface, mas criamos um tipo que tinha um comportamento. Esse comportamento correspondia ao da interface fmt.Stringer. Em seguida, vamos ver como podemos usar aquele comportamento para tornar nosso código mais reutilizável.

      Definindo uma interface

      Agora que temos nosso tipo definido com o comportamento desejado, podemos examinar como usar tal comportamento.

      No entanto, antes de fazer isso, vamos examinar o que precisaríamos fazer se quiséssemos chamar o método String do tipo Article em uma função:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(a Article) {
          fmt.Println(a.String())
      }
      

      Nesse código, adicionamos uma nova função chamada Print, que aceita um Article como um argumento. Note que a única coisa que a função Print faz é chamar o método String. Por conta disso, podemos em vez disso, definir uma interface para enviar para função:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Aqui, criamos uma interface chamada Stringer:

      main.go

      ...
      type Stringer interface {
          String() string
      }
      ...
      

      A interface Stringer tem apenas um método, chamado String() que retorna uma string. Um método é uma função especial com escopo definido para um tipo específico em Go. Ao contrário do que ocorre com uma função, um método só pode ser chamado a partir da instância do tipo em que ele foi definido.

      Em seguida, atualizamos a assinatura do método Print para aceitar um Stringer e não um tipo concreto do Article. Como o compilador sabe que uma interface Stringer define o método String, ele aceitará apenas tipos que também possuem o método String.

      Agora, podemos usar o método Print com qualquer coisa que satisfaça a interface Stringer. Vamos criar outro tipo para demonstrar isso:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title  string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Book struct {
          Title  string
          Author string
          Pages  int
      }
      
      func (b Book) String() string {
          return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title:  "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      
          b := Book{
              Title:  "All About Go",
              Author: "Jenny Dolphin",
              Pages:  25,
          }
          Print(b)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Agora, adicionamos um segundo tipo chamado Book. Ele também tem o método String definido. Isso significa que ele também atende a interface Stringer. Por conta disso, podemos enviá-lo para nossa função Print:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

      Até agora, demonstramos como usar uma única interface apenas. No entanto, uma interface pode ter mais de um comportamento definido. Em seguida, vamos ver como podemos tornar nossas interfaces mais versáteis declarando mais métodos.

      Múltiplos comportamentos em uma interface

      Um dos princípios norteadores para se escrever códigos em Go é escrever tipos pequenos e concisos e que possam entrar em composições com tipos maiores e mais complexos. O mesmo vale quando se compoem interfaces. Para ver como vamos criar uma interface, vamos primeiro começar definindo apenas uma interface. Vamos definir duas formas, um Circle e um Square, e elas definirão um método chamado Area. Esse método retornará a área geométrica de suas formas respectivas:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      type Sizer interface {
          Area() float64
      }
      
      func main() {
          c := Circle{Radius: 10}
          s := Square{Height: 10, Width: 5}
      
          l := Less(c, s)
          fmt.Printf("%+v is the smallestn", l)
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      

      Como cada tipo declara o método Area, podemos criar uma interface que define tal comportamento. Criamos a seguinte interface Sizer:

      main.go

      ...
      type Sizer interface {
          Area() float64
      }
      ...
      

      Então, definimos uma função chamada Less que aceita dois Sizer e retorna o menor deles:

      main.go

      ...
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      ...
      

      Note que aceitamos não apenas ambos argumentos como o tipo Sizer, mas que também retornamos o resultado como um Sizer também. Isso significa que não vamos mais retornar um Square ou um Circle, mas sim a interface do Sizer.

      Por fim, imprimimos o que tinha a menor área:

      Output

      {Width:5 Height:10} is the smallest

      Em seguida, vamos adicionar outro comportamento a cada tipo. Desta vez, vamos adicionar o método String() que retorna uma string. Isso irá satisfazer a interface fmt.Stringer:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      func (c Circle) String() string {
          return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      func (s Square) String() string {
          return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
      }
      
      type Sizer interface {
          Area() float64
      }
      
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      
      func main() {
          c := Circle{Radius: 10}
          PrintArea(c)
      
          s := Square{Height: 10, Width: 5}
          PrintArea(s)
      
          l := Less(c, s)
          fmt.Printf("%v is the smallestn", l)
      
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Como o tipo Circle e o tipo Square implementam tanto os métodos Area como String, podemos agora criar outra interface para descrever aquele conjunto de comportamentos mais amplo. Para tanto, vamos criar uma interface chamada Shaper. Vamos compor isso a partir da interface Sizer e da interface fmt.Stringer:

      main.go

      ...
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      ...
      

      Nota: é considerada como escolha idiomática tentar nomear sua interface de modo a terminar em er, como fmtStringer, io.Writer, etc. É por esse motivo que nomeamos nossa interface como Shaper e não Shape.

      Agora, podemos criar uma função chamada PrintArea, que aceita um Shaper como um argumento. Isso significa que podemos chamar ambos métodos no valor enviado para o método Area e para o método String:

      main.go

      ...
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Se executarmos o programa, vamos receber o seguinte resultado:

      Output

      area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

      Agora, vimos como podemos criar interfaces menores e compilá-las em interfaces maiores, conforme necessário. Embora pudéssemos ter começado com a interface maior para depois enviá-la para todas as nossas funções, é considerada melhor prática enviar apenas a interface menor para uma função que seja necessária. Isso tipicamente resulta em um código mais claro, uma vez que qualquer um que aceite uma interface menor específica pretende trabalhar somente com aquele comportamento definido.

      Por exemplo, se enviássemos o Shaper para a função Less, poderíamos presumir que ele chamaria os dois métodos: Area e String. No entanto, como pretendemos chamar somente o método Area, a função Less se torna clara, na medida em que sabemos que somente podemos chamar o método Area de um argumento que tiver sido enviado para essa função.

      Conclusão

      Vimos como criar interfaces menores e que compilá-las em interfaces maiores nos permite compartilhar apenas o que precisamos para uma função ou método. Também aprendemos que podemos compor nossas interfaces a partir de outras interfaces, incluindo aquelas definidas de outros pacotes, não apenas nossos pacotes.

      Se quiser aprender mais sobre a linguagem de programação Go, confira toda a série sobre Como codificar em Go.



      Source link

      Cómo usar interfaces en Go


      Introducción

      Escribir código flexible, reutilizable y modular es crucial para desarrollar programas versátiles. Trabajar de esta forma garantiza que se pueda facilitar el mantenimiento del código evitando la necesidad de realizar el mismo cambio en varios puntos. La forma en que esto se consigue varía según el lenguaje. Por ejemplo, inheritance es un enfoque común que se utiliza en lenguajes como Java, C++ y C#, entre otros.

      Los desarrolladores también pueden alcanzar esos objetivos de diseño a través de la composición. La composición es una alternativa para combinar objetos o tipos de datos y formar otros más complejos. Este es el enfoque que Go utiliza para promover la reutilización del código, la modularidad y la flexibilidad. Las interfaces de Go proporcionan un método para organizar composiciones complejas y aprender a usarlas le permitirá crear código común y reutilizable.

      En este artículo, aprenderá a componer tipos personalizados que tienen comportamientos comunes, lo que nos permitirá reutilizar nuestro código. También aprenderá a implementar interfaces para nuestros tipos personalizados propios que satisfarán las interfaces definidas desde otro paquete.

      Definir un comportamiento

      Una de las principales implementaciones de la composición es el uso de interfaces. Una interfaz define un comportamiento de un tipo. Una de las interfaces que más se usan en la biblioteca estándar de Go es fmt.Stringer:

      type Stringer interface {
          String() string
      }
      

      La primera línea de código define un type llamado Stringer. Luego indica que es una interfaz. Al igual cuando se define una struct, Go utiliza llaves ({}) para rodear la definición de la interfaz. En comparación con la definición de estructuras, solo definimos el comportamiento de las interfaces; es decir, “qué puede hacer este tipo”.

      En el caso de la interfaz Stringer, el único comportamiento es el método String(). El método no toma argumentos y muestra una cadena.

      A continuación, veremos código que tiene el comportamiento fmt.Stringer:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          fmt.Println(a.String())
      }
      

      Lo primero que hacemos es crear un nuevo tipo llamado Article. Este tipo tiene un campo Title y un campo Author, y ambos son de la cadena de tipo de datos:

      main.go

      ...
      type Article struct {
          Title string
          Author string
      }
      ...
      

      A continuación, definimos un method llamado String en el tipo Article. El método String mostrará una cadena que representa el tipo Article:

      main.go

      ...
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      ...
      

      A continuación, en nuestra función main, creamos una instancia del tipo Article y la asignamos a la variable llamada a. Proporcionamos los valores de "Understanding Interfaces in Go" para el campo Title y "Sammy Shark"para el campo Author:

      main.go

      ...
      a := Article{
          Title: "Understanding Interfaces in Go",
          Author: "Sammy Shark",
      }
      ...
      

      A continuación, imprimimos el resultado del método String invocando fmt.PrintIn y pasando el resultado de la invocación del método a.String():

      main.go

      ...
      fmt.Println(a.String())
      

      Después de ejecutar el programa, verá el siguiente resultado:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark.

      Hasta ahora no usamos una interfaz, pero creamos un tipo que tuvo un comportamiento. Ese comportamiento coincidió con la interfaz fmt.Stringer. A continuación, veremos la forma de usar ese comportamiento para hacer que nuestro código sea más reutilizable.

      Definir una interfaz

      Ahora que nuestro tipo está definido con el comportamiento deseado, podemos ver la forma de usar ese comportamiento.

      Antes de hacer eso, sin embargo, veremos lo que deberíamos hacer si deseáramos invocar el método String desde el tipo Article en una función:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(a Article) {
          fmt.Println(a.String())
      }
      

      En este código, añadimos una nueva función llamada Print que toma un Article como argumento. Observe que lo único que la función Print hace es invocar el método String. Debido a esto, podríamos definir una interfaz que se pasaría a la función:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title: "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Aquí creamos una interfaz llamada Stringer:

      main.go

      ...
      type Stringer interface {
          String() string
      }
      ...
      

      La interfaz Stringer solo tiene un método, llamado String(), que muestra una string. Un método es una función especial que tiene ámbito en un tipo específico en Go. A diferencia de una función, un método solo puede invocarse desde la instancia del tipo sobre el que se definió.

      A continuación actualizamos la firma del método Print para tomar un Stringer y no un tipo concreto de Article. Debido a que el compilador reconoce que una interfaz Stringer define el método String, solo aceptará los tipos que también tienen el método String.

      Ahora podemos usar el método Print con cualquier cosa que se adecue a la interfaz Stringer. Crearemos otro tipo para demostrar esto:

      main.go

      package main
      
      import "fmt"
      
      type Article struct {
          Title  string
          Author string
      }
      
      func (a Article) String() string {
          return fmt.Sprintf("The %q article was written by %s.", a.Title, a.Author)
      }
      
      type Book struct {
          Title  string
          Author string
          Pages  int
      }
      
      func (b Book) String() string {
          return fmt.Sprintf("The %q book was written by %s.", b.Title, b.Author)
      }
      
      type Stringer interface {
          String() string
      }
      
      func main() {
          a := Article{
              Title:  "Understanding Interfaces in Go",
              Author: "Sammy Shark",
          }
          Print(a)
      
          b := Book{
              Title:  "All About Go",
              Author: "Jenny Dolphin",
              Pages:  25,
          }
          Print(b)
      }
      
      func Print(s Stringer) {
          fmt.Println(s.String())
      }
      

      Ahora, añadimos un segundo tipo llamado Book. También tiene el método String definido. Esto significa que además se adecua a la interfaz Stringer. Debido a esto, podemos enviarlo también a nuestra función Print:

      Output

      The "Understanding Interfaces in Go" article was written by Sammy Shark. The "All About Go" book was written by Jenny Dolphin. It has 25 pages.

      Hasta ahora, demostramos la forma de usar una interfaz única. Sin embargo, para una interfaz puede haber más de un comportamiento definido. A continuación, veremos la forma en que podemos hacer que nuestras interfaces sean más versátiles declarando más métodos.

      Varios comportamientos en una interfaz

      Uno de los objetivos principales de escribir código en Go es escribir tipos pequeños y concisos, componerlos de modo que conformen tipos más grandes y complejos. Sucede lo mismo cuando se componen interfaces. Para ver la forma de crear una interfaz, primero comenzaremos definiendo solo una interfaz. Definiremos dos formas, Circle y Square, y ambas definirán un método llamado Area. Este método mostrará el área geométrica de sus respectivas formas:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      type Sizer interface {
          Area() float64
      }
      
      func main() {
          c := Circle{Radius: 10}
          s := Square{Height: 10, Width: 5}
      
          l := Less(c, s)
          fmt.Printf("%+v is the smallestn", l)
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      

      Debido a que cada tipo declara el método Area, podemos crear una interfaz que defina ese comportamiento. Crearemos la siguiente interfaz Sizer:

      main.go

      ...
      type Sizer interface {
          Area() float64
      }
      ...
      

      A continuación definiremos una función llamada Less que toma dos Sizer y muestra el más pequeño:

      main.go

      ...
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      ...
      

      Observe que no solo aceptamos ambos argumentos como el tipo Sizer, sino también mostramos el resultado como Sizer. Esto significa que ya no mostramos un Square ni un Circle, sino la interfaz Sizer.

      Por último, imprimimos el que tenía el área más pequeña:

      Output

      {Width:5 Height:10} is the smallest

      A continuación, añadiremos otro comportamiento a cada tipo. Esta vez, añadiremos el método String() que muestra una cadena. Esto satisfará la interfaz fmt.Stringer:

      main.go

      package main
      
      import (
          "fmt"
          "math"
      )
      
      type Circle struct {
          Radius float64
      }
      
      func (c Circle) Area() float64 {
          return math.Pi * math.Pow(c.Radius, 2)
      }
      
      func (c Circle) String() string {
          return fmt.Sprintf("Circle {Radius: %.2f}", c.Radius)
      }
      
      type Square struct {
          Width  float64
          Height float64
      }
      
      func (s Square) Area() float64 {
          return s.Width * s.Height
      }
      
      func (s Square) String() string {
          return fmt.Sprintf("Square {Width: %.2f, Height: %.2f}", s.Width, s.Height)
      }
      
      type Sizer interface {
          Area() float64
      }
      
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      
      func main() {
          c := Circle{Radius: 10}
          PrintArea(c)
      
          s := Square{Height: 10, Width: 5}
          PrintArea(s)
      
          l := Less(c, s)
          fmt.Printf("%v is the smallestn", l)
      
      }
      
      func Less(s1, s2 Sizer) Sizer {
          if s1.Area() < s2.Area() {
              return s1
          }
          return s2
      }
      
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Debido a que los tipos Circle y Square implementan los métodos Area y String, podemos crear otra interfaz para describir ese conjunto más amplio de comportamientos. Para hacer esto, crearemos una interfaz llamada Shaper. Compondremos lo siguiente con las interfaces Sizer y fmt.Stringer:

      main.go

      ...
      type Shaper interface {
          Sizer
          fmt.Stringer
      }
      ...
      

      Nota: Se considera que corresponde intentar dar nombre a su interfaz con finalización en er; fmt.Stringer e io.Writer son algunos ejemplos. Por eso, dimos a nuestra interfaz el nombre Shaper y no Shape.

      Ahora podemos crear una función llamada PrintArea que toma Shaper como argumento. Esto significa que podemos invocar ambos métodos en el valor pasado para los métodos Area y String:

      main.go

      ...
      func PrintArea(s Shaper) {
          fmt.Printf("area of %s is %.2fn", s.String(), s.Area())
      }
      

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      area of Circle {Radius: 10.00} is 314.16 area of Square {Width: 5.00, Height: 10.00} is 50.00 Square {Width: 5.00, Height: 10.00} is the smallest

      Acabamos de ver la forma de podemos crear interfaces más pequeñas y hacerlas más grandes según sea necesario. Aunque podríamos haber comenzado con la interfaz más grande y haberla pasado a todas nuestras funciones, se considera mejor enviar solo la interfaz más pequeña a una función que sea necesaria. Esto normalmente da como resultado un código más claro, ya que cualquier cosa que acepte una interfaz específica más pequeña solo tiene la intención de funcionar con ese comportamiento definido.

      Por ejemplo, si pasamos Shaper a la función Less podemos suponer que invocará los métodos Area y String. Sin embargo, ya que solo queremos invocar el método Area, hace que la función Less sea clara porque sabemos que solo podemos invocar el método Area de cualquier argumento que se le pase.

      Conclusión

      Hemos visto que la creación de interfaces más pequeñas y su ampliación nos permite compartir solo lo que necesitamos con una función o un método. También aprendió que se pueden componer nuestras interfaces a partir de otras, incluidas aquellas definidas desde otros paquetes, no solo los nuestros paquetes.

      Si desea obtener más información acerca del lenguaje de programación de Go, consulte toda la serie sobre Cómo codificar en Go.



      Source link