É "evitar o problema do ioiô" uma razão válida para permitir a "obsessão primitiva"?


42

De acordo com Quando a obsessão primitiva não é um cheiro de código? , Devo criar um objeto ZipCode para representar um CEP em vez de um objeto String.

No entanto, na minha experiência, eu prefiro ver

public class Address{
    public String zipCode;
}

ao invés de

public class Address{
    public ZipCode zipCode;
}

porque acho que o último requer que eu mude para a classe ZipCode para entender o programa.

E acredito que preciso passar entre muitas classes para ver a definição se todos os campos de dados primitivos fossem substituídos por uma classe, que parece estar sofrendo do problema do ioiô (um antipadrão).

Então, eu gostaria de mover os métodos ZipCode para uma nova classe, por exemplo:

Velho:

public class ZipCode{
    public boolean validate(String zipCode){
    }
}

Novo:

public class ZipCodeHelper{
    public static boolean validate(String zipCode){
    }
}

para que apenas quem precise validar o código postal dependa da classe ZipCodeHelper. E encontrei outro "benefício" de manter a obsessão primitiva: mantém a classe parecida com sua forma serializada, se houver, por exemplo: uma tabela de endereços com a coluna de string zipCode.

Minha pergunta é: "evitar o problema do ioiô" (alternar entre definições de classe) é uma razão válida para permitir a "obsessão primitiva"?


9
@ jpmc26 Então você ficaria chocado ao ver quão complexo é nosso objeto de código postal - não dizendo que está certo, mas existe
Jared Goguen

9
@ jpmc26, não vejo como você passa de "complexo" a "mal projetado". Código complexo geralmente é o resultado de código simples entrar em contato com a complexidade do mundo real, e não com o mundo ideal que poderíamos desejar existir. "Voltando à função de duas páginas. Sim, eu sei, é apenas uma função simples exibir uma janela, mas ela cresceu pouco e nada e ninguém sabe o porquê. Bem, eu vou lhe dizer o porquê: essas são falhas Conserta."
Kyralessa 25/01

19
@ jpmc26 - o objetivo de envolver objetos como o ZipCode é o tipo safety. O código postal não é uma cadeia, é um código postal. Se uma função espera um CEP, você só poderá passar um CEP, não uma string.
Davor Ždralo 25/01

4
Isso parece altamente específico do idioma; idiomas diferentes fazem coisas diferentes aqui. @ DavorŽdralo No mesmo trecho, também devemos adicionar muitos tipos numéricos. "somente números inteiros positivos", "apenas números pares" também podem ser tipos.
paul23 25/01

6
@ paul23 Sim, de fato, e a principal razão pela qual não temos essas é que muitas linguagens não suportam maneiras elegantes de defini-las. É perfeitamente razoável definir "idade" como um tipo diferente de "temperatura em graus celsius", apenas para que "userAge == currentTemperature" seja detectado como um absurdo.
IMSoP 25/01

Respostas:


116

A suposição é que você não precisa fazer ioiô na classe ZipCode para entender a classe Address. Se o ZipCode for bem projetado, deve ser óbvio o que ele faz apenas lendo a classe Address.

Os programas não são lidos de ponta a ponta - normalmente os programas são complexos demais para tornar isso possível. Você não pode manter todo o código em um programa em sua mente ao mesmo tempo. Portanto, usamos abstrações e encapsulamentos para "dividir" o programa em unidades significativas, para que você possa ver uma parte do programa (por exemplo, a classe Address) sem precisar ler todo o código do qual depende.

Por exemplo, tenho certeza de que você não deve ler o código-fonte para String sempre que encontrar String no código.

Renomear a classe de ZipCode para ZipCodeHelper sugere que agora existem dois conceitos separados: um código postal e um auxiliar de código postal. Tão duas vezes mais complexo. E agora o sistema de tipos não pode ajudá-lo a distinguir entre uma sequência arbitrária e um CEP válido, pois eles têm o mesmo tipo. É aqui que a "obsessão" é apropriada: você está sugerindo uma alternativa mais complexa e menos segura, apenas porque deseja evitar um tipo simples de invólucro em torno de um primitivo.

O uso de uma primitiva é IMHO justificado nos casos em que não há validação ou outra lógica, dependendo desse tipo específico. Mas assim que você adiciona qualquer lógica, é muito mais simples se essa lógica for encapsulada com o tipo

Quanto à serialização, acho que parece uma limitação na estrutura que você está usando. Certamente você deve conseguir serializar um ZipCode em uma string ou mapeá-lo para uma coluna em um banco de dados.


2
Concordo com a parte "unidades significativas" (principal), mas não tanto que um código postal e uma validação de código postal sejam o mesmo conceito. ZipCodeHelper(que eu prefiro chamar ZipCodeValidator) pode muito bem estabelecer uma conexão com um serviço da Web para fazer seu trabalho. Isso não faria parte da responsabilidade única "manter os dados do código postal". Tornar o sistema de tipos desaprovar códigos postais inválidos ainda pode ser alcançado, tornando o ZipCodeconstrutor o equivalente ao pacote privado do Java e chamando-o com um ZipCodeFactoryque sempre chama o validador.
R. Schmitz

16
@ R.Schmitz: Não é isso que "responsabilidade" significa no sentido do princípio da responsabilidade única. Mas, em qualquer caso, é claro que você deve usar quantas classes precisar, desde que encapsule o código postal e sua validação. O OP sugere um auxiliar em vez de encapsular o CEP, o que é uma má ideia.
JacquesB 24/01

1
Eu quero discordar respeitosamente. SRP significa que uma classe deve ter "uma e apenas uma razão para ser alterada" (alteração em "no que um CEP consiste em" vs. "como é validado"). Este caso específico aqui é aprofundado no livro Código Limpo : "Os objetos ocultam seus dados atrás de abstrações e expõem funções que operam nesses dados. A estrutura de dados expõe seus dados e não possui funções significativas " . - ZipCodeseria uma "estrutura de dados" e ZipCodeHelperum "objeto". De qualquer forma, acho que concordamos que não devemos passar as conexões da Web ao construtor ZipCode.
R. Schmitz

9
O uso de uma primitiva é IMHO justificado nos casos em que não há validação ou outra lógica, dependendo desse tipo específico. => Eu discordo. Mesmo que todos os valores sejam válidos, eu ainda preferiria transmitir a semântica para o idioma, em vez de usar primitivas. Se uma função pode ser chamada em um tipo primitivo que não faz sentido para seu uso semântico atual, não deve ser um tipo primitivo, deve ser um tipo adequado, com apenas as funções sensíveis definidas. (Por exemplo, usar intcomo ID permite multiplicar um ID por um ID ...)
Matthieu M.

@ R.Schmitz Acho que os códigos postais são um péssimo exemplo para a distinção que você está fazendo. Algo que muda frequentemente pode ser um candidato para aulas Fooe FooValidatoraulas separadas . Nós poderia ter uma ZipCodeclasse que valida o formato e um ZipCodeValidatorque bate em algum serviço Web para verificar se um formatado corretamente ZipCodeé realmente atual. Sabemos que os códigos postais mudam. Mas, na prática, teremos uma lista de códigos postais válidos encapsulados em ZipCodeou em algum banco de dados local.
Não U

55

Se pode fazer:

new ZipCode("totally invalid zip code");

E o construtor para ZipCode faz:

ZipCodeHelper.validate("totally invalid zip code");

Então você quebrou o encapsulamento e adicionou uma dependência bastante boba à classe ZipCode. Se o construtor não chamar ZipCodeHelper.validate(...), você terá uma lógica isolada em sua própria ilha sem realmente aplicá-la. Você pode criar códigos postais inválidos.

O validatemétodo deve ser um método estático na classe ZipCode. Agora, o conhecimento de um código postal "válido" é agrupado com a classe ZipCode. Como seus exemplos de código se parecem com Java, o construtor de ZipCode deve lançar uma exceção se um formato incorreto for fornecido:

public class ZipCode {
    private String zipCode;

    public ZipCode(string zipCode) {
        if (!validate(zipCode))
            throw new IllegalFormatException("Invalid zip code");

        this.zipCode = zipCode;
    }

    public static bool validate(String zipCode) {
        // logic to check format
    }

    @Override
    public String toString() {
        return zipCode;
    }
}

O construtor verifica o formato e lança uma exceção, impedindo a criação de códigos postais inválidos, e o validatemétodo estático está disponível para outro código, para que a lógica de verificar o formato seja encapsulada na classe ZipCode.

Não há "ioiô" nessa variante da classe ZipCode. É apenas chamado de Programação Orientada a Objetos adequada.


Também vamos ignorar a internacionalização aqui, que pode exigir outra classe chamada ZipCodeFormat ou PostalService (por exemplo, PostalService.isValidPostalCode (...), PostalService.parsePostalCode (...), etc.).


28
Nota: A principal vantagem da abordagem de @Greg Burkhardt aqui é que, se alguém fornecer um objeto ZipCode, você poderá confiar que ele contém uma sequência válida sem precisar verificar novamente, pois o tipo e o fato de ter sido construído com êxito fornecem essa garantia. Se você passou as strings por aí, pode sentir a necessidade de "afirmar validar (zipCode)" em vários locais do seu código apenas para garantir que você tenha um código postal válido, mas com um objeto ZipCode construído com êxito, você pode confiar que seu conteúdo é válido sem ter que verificá-los novamente.
Some Guy

3
@ R.Schmitz: O ZipCode.validatemétodo é a pré-verificação que pode ser realizada antes de chamar um construtor que lança uma exceção.
Greg Burghardt 25/01

10
@ R.Schmitz: Se você está preocupado com uma exceção irritante, uma abordagem alternativa à construção é tornar o construtor ZipCode privado e fornecer uma função de fábrica estática pública (Zipcode.create?) Que executa a validação dos parâmetros passados, retorna null se malsucedido e, de outro modo, constrói um objeto ZipCode e o retorna. O chamador sempre terá que verificar um valor de retorno nulo, é claro. Por outro lado, se você tem o hábito, por exemplo, de sempre validar (regex? Validar? Etc.) antes de construir um ZipCode, a exceção pode não ser tão irritante na prática.
Some Guy

11
Uma função de fábrica que retorna um <ZipCode> opcional também é uma possibilidade. Em seguida, o chamador não tem escolha a não ser lidar explicitamente com a possível falha da função de fábrica. Independentemente disso, em ambos os casos, o erro será descoberto em algum lugar próximo ao local em que foi criado, em vez de possivelmente muito mais tarde, pelo código do cliente longe do problema original.
Some Guy

6
Você não pode validar o ZipCode independentemente, por isso não. Você realmente precisa do objeto Country para procurar as regras de validação ZipCode / PostCode.
Joshua

11

Se você luta muito com essa pergunta, talvez o idioma que você usa não seja a ferramenta certa para o trabalho? Esse tipo de "primitivas do tipo domínio" é trivialmente fácil de expressar, por exemplo, em F #.

Lá você pode, por exemplo, escrever:

type ZipCode = ZipCode of string
type Town = Town of string

type Adress = {
  zipCode: ZipCode
  town: Town
  //etc
}

let adress1 = {
  zipCode = ZipCode "90210"
  town = Town "Beverly Hills"
}

let faultyAdress = {
  zipCode = "12345"  // <-Compiler error
  town = adress1.zipCode // <- Compiler error
}

Isso é realmente útil para evitar erros comuns, como comparar IDs de diferentes entidades. E como essas primitivas digitadas são muito mais leves que uma classe C # ou Java, você realmente as usará.


Interessante - como seria se você quisesse aplicar a validação ZipCode?
Hulk

4
@ Hulk Você pode escrever o estilo OO em F # e transformar os tipos em classes. No entanto, eu prefiro o estilo funcional, declarando o tipo com o tipo ZipCode = private ZipCode da string e adicionando um módulo ZipCode com uma função create. Existem alguns exemplos aqui: gist.github.com/swlaschin/54cfff886669ccab895a
Guran

@ Bent-Tranberg Obrigado pela edição. Você está certo, uma abreviação de tipo simples não fornece segurança do tipo de tempo de compilação.
Guran 28/01

Se você recebeu um e-mail com meu primeiro comentário, que eu excluí, o motivo foi que eu entendi errado sua fonte. Não li com atenção o suficiente. Quando tentei compilá-lo, finalmente percebi que você estava realmente tentando demonstrar exatamente isso, então decidi editar para acertar.
Bent Tranberg 28/01

Sim. Minha fonte original era válida, infelizmente, incluindo o exemplo que deveria ser inválido. Doh! Deveria ter apenas ligado ao Wlaschin em vez de digitar o código eu mesmo :) fsharpforfunandprofit.com/posts/…
Guran

6

A resposta depende inteiramente do que você realmente deseja fazer com os códigos postais. Aqui estão duas possibilidades extremas:

(1) Todos os endereços são garantidos em um único país. Sem exceções. (Por exemplo, nenhum cliente estrangeiro ou funcionário cujo endereço particular esteja no exterior enquanto estiver trabalhando para um cliente estrangeiro.) Este país possui CEPs e espera-se que eles nunca sejam seriamente problemáticos (ou seja, não requerem entrada de forma livre) como "atualmente o D4B 6N2, mas isso muda a cada 2 semanas"). Os códigos postais são usados ​​não apenas para endereçamento, mas para validação de informações de pagamento ou finalidades semelhantes. - Nessas circunstâncias, uma classe de código postal faz muito sentido.

(2) Os endereços podem estar em quase todos os países; portanto, dezenas ou centenas de esquemas de endereçamento com ou sem CEP (e com milhares de exceções estranhas e casos especiais) são relevantes. Um código "ZIP" é realmente solicitado apenas para lembrar as pessoas de países onde os códigos postais são usados ​​para não esquecer de fornecer os seus. Os endereços são usados ​​apenas para que, se alguém perder o acesso à sua conta e puder provar seu nome e endereço, o acesso seja restaurado. - Nessas circunstâncias, as classes de CEP para todos os países relevantes seriam um esforço enorme. Felizmente eles não são necessários.


3

As outras respostas falaram sobre modelagem de domínio OO e uso de um tipo mais rico para representar seu valor.

Não discordo, especialmente considerando o código de exemplo que você postou.

Mas também me pergunto se isso realmente responde ao título da sua pergunta.

Considere o seguinte cenário (extraído de um projeto real no qual estou trabalhando):

Você tem um aplicativo remoto em um dispositivo de campo que fala com o servidor central. Um dos campos do banco de dados para a entrada do dispositivo é um CEP para o endereço em que o dispositivo de campo está. Você não se importa com o CEP (ou qualquer outro endereço do resto). Todas as pessoas que se preocupam com isso estão do outro lado de um limite HTTP: você é a única fonte de verdade para os dados. Não tem lugar na sua modelagem de domínio. Você apenas grava, valida, armazena e, a pedido, embaralha em um blob JSON para pontos em outros lugares.

Nesse cenário, fazer muito além de validar a inserção com uma restrição de regex SQL (ou seu equivalente ORM) provavelmente é um exagero da variedade YAGNI.


6
Sua restrição de regex SQL pode ser vista como um tipo qualificado - no seu banco de dados, o CEP não é armazenado como "VarChar", mas "VarChar restringido por esta regra". Em alguns DBMSes, você pode facilmente atribuir um nome a esse tipo + restrição como um "tipo de domínio" reutilizável, e estamos de volta ao local recomendado para fornecer aos dados um tipo significativo. Concordo com sua resposta em princípio, mas não pense que esse exemplo corresponde; um exemplo melhor seria se seus dados forem "dados brutos do sensor" e o tipo mais significativo for "matriz de bytes" porque você não tem idéia do que os dados significam.
IMSoP 24/01

@IMSoP ponto interessante. Porém, não tenho certeza se concordo: você pode validar um código postal em Java (ou qualquer outra linguagem) com uma expressão regular, mas ainda assim lidar com ela como uma sequência em vez de um tipo mais rico. Dependendo da lógica do domínio, uma manipulação adicional pode ser necessária (por exemplo, garantir que o CEP corresponda ao estado, algo que seria difícil / impossível de validar com o regex).
Jared Smith

Você poderia , mas assim que o fizer, estará atribuindo algum comportamento específico ao domínio, e é exatamente isso que os artigos citados estão dizendo que devem levar à criação de um tipo personalizado. A questão não é se você pode fazê-lo em um estilo ou outro, mas se deve , assumindo que sua linguagem de programação lhe dá a opção.
IMSoP 24/01

Você pode modelar algo como a RegexValidatedString, contendo a própria sequência e o regex usado para validá-la. Mas, a menos que cada instância tenha um regex único (o que é possível, mas improvável), isso parece um pouco tolo e desperdiçador de memória (e possivelmente tempo de compilação do regex). Então, você coloca o regex em uma tabela separada e deixa para trás uma chave de pesquisa em cada instância para encontrá-lo (o que é possivelmente pior devido a indireção) ou encontra uma maneira de armazená-lo uma vez para cada tipo comum de compartilhamento de valor que regra - - por exemplo. um campo estático em um tipo de domínio ou método equivalente, como disse o IMSoP.
Miral 25/01

2

A ZipCodeabstração só faria sentido se sua Addressclasse também não tivesse uma TownNamepropriedade. Caso contrário, você terá meia abstração: o código postal designa a cidade, mas esses dois bits de informação relacionados são encontrados em classes diferentes. Não faz muito sentido.

No entanto, mesmo assim, ainda não é uma aplicação correta (ou melhor, uma solução para) obsessão primitiva; que, na minha opinião, se concentra principalmente em duas coisas:

  1. Usando primitivas como valores de entrada (ou mesmo saída) de um método, especialmente quando uma coleção de primitivas é necessária.
  2. Classes que desenvolvem propriedades extras ao longo do tempo sem nunca reconsiderar se algumas delas devem ser agrupadas em uma subclasse própria.

Seu caso também não é. Um endereço é um conceito bem definido com propriedades claramente necessárias (rua, número, CEP, cidade, estado, país, ...). Há pouco ou nenhum motivo para quebrar esses dados, pois eles têm uma única responsabilidade: designar um local na Terra. Um endereço requer todos esses campos para ser significativo. Metade de um endereço é inútil.

É assim que você sabe que não precisa mais subdividir-se: decompô-lo ainda mais prejudicaria a intenção funcional da Addressclasse. Da mesma forma, você não precisa que uma Namesubclasse seja usada na Personclasse, a menos que Name(sem uma pessoa conectada) seja um conceito significativo em seu domínio. O que (geralmente) não é. Os nomes são usados ​​para identificar pessoas, elas geralmente não têm valor por si próprias.


1
@RikD: Da resposta: "você não precisa de uma Namesubclasse para ser usada na Personclasse, a menos que Nome (sem uma pessoa conectada) seja um conceito significativo em seu domínio ." Quando você tem validação personalizada para os nomes, o nome se torna um conceito significativo em seu domínio; que mencionei explicitamente como um caso de uso válido para o uso de um subtipo. Em segundo lugar, para a validação do CEP, você está introduzindo suposições adicionais, como códigos postais que precisam seguir o formato de um determinado país. Você está abordando um tópico muito mais amplo do que a intenção da pergunta do OP.
Flater 24/01

5
" Um endereço é um conceito bem definido com propriedades claramente necessárias (rua, número, CEP, cidade, estado, país). " - Bem, isso está completamente errado. Para uma boa maneira de lidar com isso, consulte o formulário de endereço da Amazon.
R. Schmitz

4
@Flater Bem, eu não vou culpá-lo por não ler a lista completa de falsidades, porque é bastante longa, mas literalmente contém "Endereços terão uma rua", "Um endereço exige cidade e país", "Um endereço terá um código postal "etc., o que é contrário ao que a frase citada diz.
R. Schmitz

8
@GregBurghardt "O código postal assume o Serviço Postal dos Estados Unidos e você pode derivar o nome da cidade a partir do código postal. As cidades podem ter vários códigos postais, mas cada código postal está vinculado apenas a 1 cidade". Isso não está correto em geral. Eu tenho um CEP que é usado principalmente para uma cidade vizinha, mas minha residência não está localizada lá. Os códigos postais nem sempre estão alinhados com os limites governamentais. Por exemplo 42223 contém municípios de TN e KY .
JimmyJames 24/01

2
Na Áustria, existe um vale que só é acessível na Alemanha ( en.wikipedia.org/wiki/Kleinwalsertal ). Existe um tratado especial para essa região que, entre outras coisas, também inclui que os endereços nessa área têm códigos postais austríacos e alemães. Portanto, em geral, você não pode nem assumir que um endereço tem apenas um único código postal válido;)
Hulk

1

Do artigo:

De um modo mais geral, o problema do ioiô também pode se referir a qualquer situação em que uma pessoa deva continuar alternando entre diferentes fontes de informação para entender um conceito.

O código fonte é lido com muito mais frequência do que está escrito. Portanto, o problema do ioiô, de ter que alternar entre muitos arquivos, é uma preocupação.

No entanto, não , o problema do ioiô parece muito mais relevante ao lidar com módulos ou classes profundamente interdependentes (que se alternam entre si). Esse é um tipo especial de pesadelo para ler, e provavelmente é o que o criador do problema do ioiô tinha em mente.

No entanto - sim , é importante evitar muitas camadas de abstração!

Todas as abstrações não triviais, até certo ponto, são vazadas. - a lei das abstrações com vazamentos.

Por exemplo, discordo da suposição feita na resposta da mmmaaa de que "você não precisa fazer ioiô para [(visitar)] a classe ZipCode para entender a classe Address". Minha experiência tem sido que fazer - pelo menos nas primeiras vezes que você ler o código. No entanto, como os outros, não são momentos em que uma ZipCodeclasse mais adequada.

YAGNI (Você não vai precisar disso) é um padrão melhor a seguir para evitar o código de lasanha (código com muitas camadas) - abstrações, como tipos e classes, estão lá para ajudar o programador e não devem ser usadas a menos que sejam um auxílio.

Pessoalmente, pretendo "salvar linhas de código" (e, claro, os relacionados "salvar arquivos / módulos / classes", etc). Estou confiante de que há quem me aplique o epíteto de "obsessivo primitivo" - acho mais importante ter um código fácil de raciocinar do que se preocupar com rótulos, padrões e antipadrões. A escolha correta de quando criar uma função, um módulo / arquivo / classe ou colocar uma função em um local comum é muito situacional. Eu aponto aproximadamente para 3-100 funções de linha, 80-500 arquivos de linha e "1, 2, n" para código de biblioteca reutilizável ( SLOC - sem incluir comentários ou clichê; normalmente eu quero pelo menos um SLOC adicional adicional por linha obrigatória padrão).

A maioria dos padrões positivos surgiu dos desenvolvedores fazendo exatamente isso, quando precisavam deles . É muito mais importante aprender a escrever código legível do que tentar aplicar padrões sem o mesmo problema a ser resolvido. Qualquer bom desenvolvedor pode implementar o padrão de fábrica sem o ter visto antes, no caso incomum em que é o ajuste certo para o problema. Eu usei o padrão de fábrica, o padrão de observador e, provavelmente, centenas além, sem saber seu nome (ou seja, existe um "padrão de atribuição de variáveis"?). Para um experimento divertido - veja quantos padrões GoF estão embutidos na linguagem JS - parei de contar após 12 a 15 anos em 2009. O padrão Factory é tão simples quanto retornar um objeto de um construtor JS, por exemplo - não há necessidade de um WidgetFactory.

Então - sim , às vezes ZipCode é uma boa aula. No entanto, não , o problema do ioiô não é estritamente relevante.


0

O problema do ioiô é relevante apenas se você precisar se virar e voltar. Isso é causado por uma ou duas coisas (às vezes ambas):

  1. Nomeação incorreta. O ZipCode parece bom, o ZoneImprovementPlanCode exigirá uma olhada pela maioria das pessoas (e as poucas que não ficarão impressionadas).
  2. Acoplamento inadequado. Digamos que a classe ZipCode tenha uma pesquisa de código de área. Você pode pensar que faz sentido porque é útil, mas não está realmente relacionado ao ZipCode, e incorporá-lo significa que agora as pessoas não sabem para onde ir.

Se você pode olhar o nome e ter uma idéia razoável do que ele faz, e os métodos e propriedades fazem coisas razoavelmente óbvias, você não precisa olhar o código, basta usá-lo. Esse é o objetivo das classes em primeiro lugar - elas são trechos de código modulares que podem ser usados ​​e desenvolvidos isoladamente. Se você precisar procurar algo diferente da API da classe para ver o que ela faz, é, na melhor das hipóteses, uma falha parcial.


-1

Lembre-se, não há bala de prata. Se você estiver escrevendo um aplicativo extremamente simples que precisa ser rastreado rapidamente, uma sequência simples pode fazer o trabalho. No entanto, em 98% das vezes, um Objeto de Valor, conforme descrito por Eric Evans no DDD, seria o ajuste perfeito. Você pode ver facilmente todos os benefícios que os objetos Value oferecem lendo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.