DEV Community

Cover image for Getters e setters muito conhecido em “Java” é comum o seu uso em Go?
jefferson otoni lima
jefferson otoni lima

Posted on

Getters e setters muito conhecido em “Java” é comum o seu uso em Go?

Antes de iniciarmos temos que definir alguns conceitos importantes que a pergunta nos força a refletir.

Os conceitos de "getters" e "setters" são fundamentais para a programação orientada a objetos (OOP) e estão intimamente ligados ao princípio do encapsulamento, um dos pilares da OOP. O encapsulamento envolve a restrição do acesso direto a alguns dos componentes de um objeto e a provisão de métodos públicos (como getters e setters) para manipular esses componentes.

Poderíamos dizer que a origem ao conceito de getters e setters se da com a criação do paradigma de programação orientado a objeto e elas emergiram na década de 1980 e evoluíram rapidamente. No entanto, linguagens como Smalltalk, uma das primeiras linguagens orientadas a objetos, já tinham conceitos que permitiam o encapsulamento de propriedades de objetos. Mais tarde, linguagens como C++ e Java popularizaram o uso de métodos específicos para acessar e modificar propriedades privadas, que se tornaram conhecidos como getters e setters.

Quando referenciamos ao getters e setters eles não são um "padrão arquitetural", eles são mais um padrão de design ou uma prática de desenvolvimento que facilita o encapsulamento. No entanto, eles são usados em muitos padrões de design e arquitetura. Por exemplo, no padrão de design "Bean" em Java, o uso de getters e setters é padrão.

Getters e setters permitem o controle sobre como um atributo é acessado ou modificado. Eles podem incluir lógica adicional, como validação, ou podem ser usados para criar atributos somente leitura e é este o motivo de sua utilização independente da linguagem de programação que venha utiliza-lo.

Sua utilização também podem ser vistos como verbosos, especialmente em linguagens onde a definição de cada getter e setter pode exigir várias linhas de código a mais. Além disso, o uso excessivo de getters e setters pode ser um sinal de que pode está fazendo muito ou violando o princípio da responsabilidade única.

Enquanto o conceito de encapsulamento é fundamental para a programação orientada a objetos, a forma como esse encapsulamento é implementado, seja através de getters e setters ou outros mecanismos, varia entre linguagens e frameworks.

Olha o exemplo feito em Smalltalk a primeira linguagem de Programação Orientada a Objeto como é escrito:

Object subclass: Pessoa [
    | nome idade |

    Pessoa class >> new [
        ^super new initialize
    ]

    Pessoa >> initialize [
        nome := ''.
        idade := 0.
    ]

    "Getters"
    Pessoa >> nome [
        ^nome
    ]

    Pessoa >> idade [
        ^idade
    ]

    "Setters"
    Pessoa >> nome: umNome [
        nome := umNome.
    ]

    Pessoa >> idade: umaIdade [
        idade := umaIdade.
    ]
]

Enter fullscreen mode Exit fullscreen mode

Agora confira como é instânciada e chamado os métodos.

| jeff |
jeff := Pessoa new.
jeff nome: 'jeffotoni'.
jeff idade: 35.

Transcript show: jeff nome; cr.
Transcript show: jeff idade printString; cr.
Enter fullscreen mode Exit fullscreen mode

No java teríamos algo assim:

public class Pessoa {

    private String nome;
    private int idade;

    public Pessoa() {
        this.nome = "";
        this.idade = 0;
    }

    // Getter para nome
    public String getNome() {
        return nome;
    }

    // Setter para nome
    public void setNome(String nome) {
        this.nome = nome;
    }

    // Getter para idade
    public int getIdade() {
        return idade;
    }

    // Setter para idade
    public void setIdade(int idade) {
        this.idade = idade;
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora olha como fica sua instância:

public static void main(String[] args) {
        Pessoa jeff = new Pessoa();
        jeff.setNome("jeffotoni");
        jeff.setIdade(35);

        System.out.println("Nome: " + jeff.getNome());
        System.out.println("Idade: " + jeff.getIdade());
    }
Enter fullscreen mode Exit fullscreen mode

E em Go ?

Sabemos que Go não é orientado a objeto não da forma que conhecemos como Java, C++ ou C#. Para mais detalhes pode ler o meu post sobre este assunto clicando neste link: Go é orientado a objetos ?

Go não suporta classes e nem heranças ele não foi construído para um designer de paradigma orientado a objeto. No entanto, Go oferece recursos que permitem aos desenvolvedores alcançar objetivos semelhantes ao da programação orientada a objetos, principalmente através de structs e interfaces que também tem um comportamento diferente do que conhecemos nas linguagens de programação Java, C++ ou C#.

Em Go temos struct, uma struct é uma coleção de campos, que podem ter diversos tipos. Elas são comparáveis aos registros em algumas outras linguagens. Uma struct define um tipo de dados que agrupa variáveis relacionadas.

Logo abaixo uma struct pública e com campos públicos em Go.

type Pessoa struct {
    Nome  string // público
    Idade int    // público
}
Enter fullscreen mode Exit fullscreen mode

Então, o que temos até o momento é uma forma de fazer polimorfismo utilizando "interface", encapsulamento de métodos e disponibiliza-los de forma dinâmica muito semelhante ao "Duck typing". E para complementar a grande maioria dos patterns possíveis a serem feitos em Go usam interface, dentre eles: Abstract Factory, Adapter, Builder, Bridge, Composite, Prototype , Decorator.

Logo abaixo um exemplo de utilização de interface, struct e método Go para facilitar nosso entendimento em nosso exemplo da struct Pessoa.

package main

import "fmt"

type Apresentavel interface {
    Apresentar() string
}

type Pessoa struct {
    Nome  string
    Idade int
}

func (p Pessoa) Apresentar() string {
    return fmt.Sprintf("Olá! Meu nome é %s e eu tenho %d anos.", p.Nome, p.Idade)
}

type Estudante struct {
    Pessoa
    Curso string
}

func (e Estudante) Apresentar() string {
    return fmt.Sprintf("Meu nome é %s, eu tenho %d anos e estudo %s.", e.Nome, e.Idade, e.Curso)
}
Enter fullscreen mode Exit fullscreen mode

Em nosso exemplo acima tanto Pessoa quanto Estudante implementam a interface Apresentavel. A função main demonstra polimorfismo, pois tanto pessoa1 quanto pessoa2 são tratados como tipos Apresentavel, mas seus comportamentos são diferentes.

Confira nossa função main logo abaixo:

func main() {
    var pessoa1 Apresentavel = Pessoa{"Silver Gama", 28}
    var pessoa2 Apresentavel = Estudante{Pessoa{"jeffotoni", 21}, "Ciência da Computação"}

    fmt.Println(pessoa1.Apresentar())
    fmt.Println(pessoa2.Apresentar())
}
Enter fullscreen mode Exit fullscreen mode

Caso queira executar e brincar basta clicar no neste link Polimorfismo em Go.

Em Go, a visibilidade de membros de uma struct (ou funções em um pacote) é controlada pela primeira letra do identificador. Se começar com uma letra maiúscula, é permitido ser exportado ou melhor acessada fora do pkg Go (equivalente a public em linguagens como Java); caso contrário, é unexported (semelhante a private) ou seja não poderá ser acessado fora do pkg Go.

Em nosso exemplo acima se criarmos um pacote as a primeira letra maiúscula do nome da nossa struct e seus respectivos campos isto significa que nossa struct é pública e poderá ser instanciada ou acessada de forma externa.

Go não tem uma sintaxe específica para getters e setters como algumas linguagens OOP. Em vez disso, você simplesmente usa métodos. Por causa das convenções de nomenclatura em Go, getters e setters normalmente não são prefixados com "Get" ou "Set" e podemos fazer da forma que convier e conforme a necessidade do projeto e cenários, porém alguns desenvolvedores preferem manter a sintaxe por uma melhor prática pois são poliglotas e conhecendo de OOP pode utilizar da mesma sintaxe para facilitar a legibilidade e o entendimento do seus códigos Go.

Antes de mostrar a abordagem com getters e setters vamos ver a abordagem mais direta e bem prática de como fazemos em Go.

package main

import (
    "fmt"
)

type Pessoa struct {
    Nome  string
    Idade int
}

func main() {
    jeff := Pessoa{}
    jeff.Nome = "jeffotoni"
    jeff.Idade = 35

    fmt.Println("Nome:", jeff.Nome)
    fmt.Println("Idade:", jeff.Idade)
}
Enter fullscreen mode Exit fullscreen mode

Para acessar o Código basta clicar no link: Struct em Go.

Neste exemplo, Nome e Idade são campos exportados da struct Pessoa. Isso significa que você pode acessar e modificar esses campos diretamente, como demonstrado na função main, e quando criado um pacote em Go também isto é importante dizer.

Esta abordagem é mais direta e é frequentemente preferida em Go quando não há necessidade de lógica adicional ao definir ou obter o valor de um campo.

Só para reforçar, se criar um pkg em Go irá conseguir fazer import deste pkg em seu projeto e fazer a chamada da sua struct igual o exemplo do main acima.

Quando usar Getters e Setters em Go?

Só para reforçar estamos utilizando do conceito "getters e setters" não é obrigatório o seu uso com nomes específicos e nem sua obrigatoriedade para encapsulamento em Go você tem a liberdade de escolha, repetindo: "getters e setters é um padrão de design ou uma prática de desenvolvimento que facilita o encapsulamento".

Quando necessitar de validações, lógicas adicionais ao acessar ou modificar os campos, quando precisar fazer isto temos um bom motivo em deixar nossos campos da struct private ou seja com a primeira letra minúscula e criarmos métodos sejam eles com interface ou não, para fazer todo o controle e também podemos deixar a struct private para que todo seu controle seja feito pelo seu pkg.

Aqui temos duas abordagens diferentes campos da struct private e struct private.

Aqui o exemplo de uma struct publica que pode ser exposta e campos privados sendo controlados por métodos em Go:

type Pessoa struct {
    nome  string
    idade int
}
Enter fullscreen mode Exit fullscreen mode

Agora observe nossos métodos como ficariam:

// Getter para nome
func (p *Pessoa) Nome() string {
    return p.nome
}

// Setter para nome
func (p *Pessoa) SetNome(nome string) {
    p.nome = nome
}

// Getter para idade
func (p *Pessoa) Idade() int {
    return p.idade
}

// Setter para idade
func (p *Pessoa) SetIdade(idade int) {
    p.idade = idade
}
Enter fullscreen mode Exit fullscreen mode

Aqui temos getters e setters para os campos nome e idade da struct Pessoa. Eles são implementados como métodos da struct.

Olha aqui nossa função main utilizando nossos métodos:

func main() {
    jeff := &Pessoa{}
    jeff.SetNome("jeff")
    jeff.SetIdade(30)

    fmt.Println("Nome:", jeff.Nome())
    fmt.Println("Idade:", jeff.Idade())
}
Enter fullscreen mode Exit fullscreen mode

A outra abordagem seria deixar a struct private desta forma não consegue acessar a struct diretamente somente através de métodos. Nesta abordagem temos uma função que faz o papel de um construtor. Em linguagens de programação orientadas a objetos, como Java, C++ ou C# construtores são métodos especiais que são usados para inicializar uma instância de uma classe. Esses construtores permitem definir valores iniciais, alocar recursos, ou realizar outras tarefas de inicialização necessárias para o objeto.

Go, sendo uma linguagem que não é tradicionalmente orientada a objetos, não tem um conceito embutido de "construtor" como essas linguagens. No entanto, é comum em Go criar uma função que serve o mesmo propósito que um construtor em outras linguagens. Esta função geralmente começa com "New" (mas vai de projeto a projeto não é regra) e retorna uma instância da struct.

Confira o exemplo abaixo de como ficaria nossa função construtora e os métodos getters e setters.

type pessoa struct {
    nome  string
    idade int
}
Enter fullscreen mode Exit fullscreen mode

Agora confira nosso "construtor" como ficaria:

func New(nome string, idade int) *pessoa {
    return &pessoa{nome: nome, idade: idade}
}
Enter fullscreen mode Exit fullscreen mode

Nossos métodos ficaram assim:

// Getter para nome
func (p *pessoa) Nome() string {
    return p.nome
}

// Setter para nome
func (p *pessoa) SetNome(nome string) {
    p.nome = nome
}

// Getter para idade
func (p *pessoa) Idade() int {
    return p.idade
}

// Setter para idade
func (p *pessoa) SetIdade(idade int) {
    p.idade = idade
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos ver como fazemos a instanciação e chamamos isto em nosso main.

Confira o código abaixo:

func main() {
    jeff := New("jeffotoni", 35)
    fmt.Println("Nome:", jeff.Nome())
    fmt.Println("Idade:", jeff.Idade())
}
Enter fullscreen mode Exit fullscreen mode

Gostaria de rodar o código completo click neste link: Go e construtor "New".

E as vulnerabilidades ?

Ao exportar structs e seus campos em Go (ou seja, torná-los públicos, começando com uma letra maiúscula), estamos permitindo que outros pacotes os acessem diretamente. Isso pode levar há algumas reflexões que irá precisar fazer quando estiver desenvolvendo seus pacotes em Go.

Encapsulamento Quebrado

Uma das principais preocupações é o potencial para quebrar o encapsulamento. Quando você exporta um campo, qualquer código fora do pacote pode alterá-lo diretamente. Se existir uma necessidade no meio do caminho ou seja não foi percebido antes de criar o pacote e precisar colocar ou criar uma lógica interna ou invariáveis que precisam ser mantidas, o acesso direto pode levar a estados inconsistentes ou inválidos, em outras palavras poderá quebrar seu pacote.

Manutenibilidade

Se outros pacotes começarem a depender diretamente dos campos exportados, pode ser difícil mudar a estrutura interna da sua struct no futuro. Qualquer mudança pode quebrar o código que depende dela.

Segurança

Em alguns casos, pode haver preocupações de segurança acredito que muito poucos mesmo. Por exemplo, se um campo exportado contiver informações sensíveis, ele poderia, em teoria, ser lido ou modificado por código mal-intencionado em outro pacote mas para isto ocorrer você precisaria instanciá-lo no pacote de alguma forma.

Escondendo Detalhes de Implementação

Ao exportar campos, você está expondo detalhes de implementação que talvez preferisse manter ocultos. Esconder detalhes de implementação pode tornar seu código mais fácil de usar e menos propenso a erros isto é fato.

Validações

Se certas validações ou lógicas devem ser aplicadas ao definir valores em sua struct, exportar campos diretamente contorna essas validações, a menos que você dependa de métodos de setter.

Conclusão

Só para reforçar utilizei do conceito "Getters e Setters" que é um padrão de designer para abordar como poderíamos encapsular nossas structs em Go, relembrando que não é obrigatório é somente uma prática de desenvolvimento que é bem comum dos desenvolvedores que veem de java para Go.

Ao projetar structs e pacotes em Go, é importante ponderar cuidadosamente as vantagens e desvantagens de exportar campos. Em muitos casos, a decisão dependerá das necessidades específicas e trade-offs do projeto.

Enquanto o Go certamente permite e, em alguns casos, incentiva o uso direto de campos exportados para simplicidade, há situações em que usar getters e setters é benéfico. O uso de getters e setters em Go, como em muitas outras linguagens, está relacionado a práticas de encapsulamento e controle sobre como os dados de uma struct ou tipo são acessados e modificados.

Espero que tenha gostado do resumo e uma breve introdução do que podemos fazer em Go quando o assunto é getters e setters. Qualquer observação, dica, melhoria etc.. no conteúdo por favor só enviar para melhora-lo ainda mais.

Top comments (1)

Collapse
 
gaiveknopf profile image
Macgeiver Knopf

Jeff, sensacional! Obrigado por abrir a minha mente!