Como criar dicionários case insensitive

Dicionários são sempre listados entre as melhores estruturas de dados pois o custo de acesso a um dado registro é O(1), ou seja, não é necessário percorrer todo dataset para encontrar um elemento, basta acessar o seu índice diretamente. Mas como criar dicionários case insensitive? Ou em outras palavras, como criar dicionários que ignorem a forma como uma palavra foi escrita?

O código abaixo armazena o estoque de produtos para uma determinada loja. Sempre que um produto é adicionado ele verifica se o produto já existe, caso exista aumenta a quantidade em estoque, quando não existe cria um novo registro.

namespace Dictionary {
   public class Store {
      public Dictionary<string, int> Items = new();

      public void Add(string product, int quant) {
         if (Items.ContainsKey(product)) {
            Items[product] += quant;
         }
         else {
            Items.Add(product, quant);
         }
      }
   }
   public static class DictionaryExample {
      public static void Main() {
         var store = new Store();
         store.Add("Bike", 10);
         store.Add("bike", 15);
         store.Add("Skate", 8);

         foreach (var item in store.Items) {
            Console.WriteLine($"{item.Key} - {item.Value}");
         }
         Console.ReadKey();
      }
   }
}

O problema é que, por padrão, o runtime trata strings como case sensitive, então “Bike”, “BIKE”, …. e “bike” são palavras diferentes, resultando em dois diferentes produtos:

Bike - 10
bike - 15
Skate - 8

Uma solução simples

Uma solução simples para o problema é converter todas as letras do nome do produto para minúsculas ou maiúsculas, usando ToLower ou ToUpper no método Add():

public void Add(string product, int quant) {
   product = product.ToLower();
   if (Items.ContainsKey(product)) {
      Items[product] += quant;
   }
   else {
      Items.Add(product, quant);
   }
}

O que trará como resultado:

bike - 25
skate - 8

Funciona, mas não é o ideal.

Uma solução eficaz

Uma solução melhor para o problema é especificar ao runtime como ele deve tratar as strings armazenadas no dicionário usando a classe StringComparer no momento que o dicionário é instanciado.

Então, ao invés de instanciar o objeto simplesmente usando new(), basta passar o tipo de comparação desejada no construtor no mesmo. A opção InvariantCultureIgnoreCase fará com que as strings “Bike” e “bike” sejam consideradas a mesma sem a necessidade do uso do método ToLower e produzirá exatamente a mesma saída.

public Dictionary<string, int> Items = new(StringComparer.InvariantCultureIgnoreCase);

Quer saber mais sobre os diferentes tipos de comparação, então acesse a documentação oficial da Microsoft: StringComparer Class

 

Adição de elementos numa lista dentro de um laço sobre a mesma

Participei da seleção para uma empresa da Bélgica que desenvolve jogos para cassinos. Na primeira fase do processo foram feitas várias pequenas questões para verificar se o o candidato conhece os conceitos da linguagem. Quatro destas questões eram sobre listas ou listas e Linq. Abaixo uma delas, sobre a adição de novos elementos numa lista dentro de um laço foreach sobre a mesma lista:

Qual será a saída ao executar este código?

class Program {
   static void Main() {
      var ints = new List<int>(3) { 1, 2 };

      foreach (int i in ints) {
           ints.Add(i + 1);
      }
      Console.WriteLine("{0}", ints[2]);
   }
}

a) A saída será 2 e a lista conterá 4 elementos.
b) A saída será 1 e a lista conterá 6 elementos.
c) A saída será 2 e a lista conterá 6 elementos.
d) Não irá compilar.
e) Haverá uma exceção do tipo “fora do intervalo” em tempo de execução.
f) Haverá uma exceção do tipo “operação inválida” em tempo de execução.

O programa é bem simples: cria uma lista com três posições e preenche as duas primeiras com 1 e 2. Depois disto, para cada elemento da lista adiciona um novo elemento, cujo valor é o elemento atual mais 1. Por fim, mostra o terceiro elemento da lista.

Ou seja:

A lista começa com os elementos 1 e 2. Na primeira iteração adiciona um novo elemento com valor 2 (1 + 1) e lista passa a ter três elementos (1, 2, 2). Na segunda interação adiciona o elemento 3 (2 + 1) e lista passa a ter 4 elementos (1, 2, 2, 3). Resultado, letra  A: saída será 2 e a lista conterá 4 elementos. Certo? Não, errado. O sistema gera uma exceção em tempo de execução.

Mas por que ocorre uma exceção?

A exceção ocorre porque o código está tentando alterar a coleção dentro de um laço que percorre a própria coleção. Quando o programa é executado ele roda normalmente até inserir o primeiro novo elemento na lista, entretanto quando o cursor volta para o laço foreach ocorre o erro:

System.InvalidOperationException: ‘Collection was modified; enumeration operation may not execute.’

Ou seja, a coleção foi modifica e a operação sobre ela não pode ser executada. Resposta correta letra F: haverá uma exceção do tipo “operação inválida” em tempo de execução.

Execute o programa passo a passo usando o debug para ver o ponto exato onde a exceção é gerada

Este comportamento faz todo sentido, pois o laço foreach está preparado para percorrer toda a lista. Se a lista é modifica, seja por adição ou exclusão de elementos, como o compilador irá controlar quando o laço deve ser finalizado?

Mais uma dúvida….

Caso fosse possível adicionar novos elementos à lista dentro do foreach, surge uma nova questão: a lista foi declarada com tamanho três e iniciada com dois elementos, seria possível adicionar mais do que um elemento a ela ou neste caso teria uma exceção do tipo out of range (fora do intervalo)?

Isto pode ser testado modificando ligeiramente o código da questão, alterando o laço foreach para um laço for:

class Program {
   static void Main() {
      var ints = new List<int>(3) { 1, 2 };

      for(int i = 1; i < 3; i++) {
           ints.Add(i + 1);
      }
      Console.WriteLine("{0}", ints[2]);
   }
}

No código acima dois novos elementos (2 e 3) são adicionados a lista, que passa a ter quatro elementos (1, 2, 2, 3). Nenhuma exceção é levantada, pois o tamanho da lista é alterado automaticamento de 3 para 4. O “3” usado no momento que a lista é instanciada em memória determina o tamanho inicial alocado e não o tamanho máximo.

Origem da questão
País: Bélgica Tipo: Conceitos Assunto: List
Ramo de negócio da empresa: Games Grau de Dificuldade: fácil

Conceitos: Como chamar um Delegate

Como chamar um Delegate? Questões sobre delegates são incomuns em provas para candidatos iniciantes, aparecem com alguma frequência em vagas para profissionais plenos e é “figurinha carimbada” na maioria das provas para seniores.

Entretanto, nem sempre é uma pergunta difícil. Para acertar a questão abaixo, o candidato precisa apenas conhecimentos básicos sobre o assunto.

Qual(ais) da(s) próximas alternativas substitui(em) ***** na função main fazendo com que o programa retorne o valor 15?

using System;

namespace Program {
   class Program {
      static void Main(string[] args) {
         int result;

         *****

         Console.WriteLine(result);
      }

      static int Add(int a, int b) {
         return a + b;
      }

      static int DoIntMath(Func<int,int,int> function, int x, int y) {
         return function(x, y);
      }    
   }
}

A. result = DoIntMath(new delegate int(int a, int b){ return a + b; }, 5, 10);
B. result = DoIntMath(Add, 5, 10);
C. result = DoIntMath((a, b) => (a + b),  5,  10);
D. result = DoIntMath(delegate (int a, int b){ return a + b; }, 5, 10);
E. result = DoIntMath(Add(5, 10));

Mas o que é um delegate?

Um delegate é um tipo que permite fazer referência para um método com uma lista de parâmetros e um tipo de retorno específico. Ou seja, é um ponteiro que pode ser associado a qualquer método com uma assinatura e tipo de retorno compatíveis.

A função DoIntMath

static int DoIntMath(Func <int, int, int> function, int x, int y)

… possui três parâmetros. Os dois últimos (x e y ) são parâmetros inteiros comuns, já o primeiro (function) é uma função que retorna um número inteiro e recebe dois parâmetros também inteiros: Func< retorno, parâmetro 1, parâmetro 2>.

A função function aceita qualquer coisa com o formato int(parâmetro int, parâmetro int). Em outras palavras, para invocar a função DoIntMath, é preciso passar uma função semelhante à int Add(int a, int b), o que já elimina a alternativa “E”. A alternativa “A” também pode ser facilmente eliminada por causa da palavra new, uma vez que não se instancia um delegate.

Com isto, restam como corretas as alternativas “B”, onde é passada uma função já existente (Add) e as alternativas “C” e “D”, onde são passadas novas funções que foram declaradas diretamente no parâmetro function.

Origem da questão
País: Holanda Tipo: Conceitos Assunto: Delegate
Ramo de negócio da empresa: Consultoria Grau de Dificuldade: Médio

Roteiro de Entrevista: Desenvolvedor para empresa de apostas da Irlanda

Costumo fazer uma entrevista técnica a cada quinze dias. Vou passar a publicar o roteiro das entrevistas aqui, começando por uma fiz na semana passada para o cargo de Desenvolvedor Sênior de uma empresa de apostas da Irlanda. Na empresa não ganha dinheiro diretamente com as apostas, mas sim com um algoritmo que gera as estatísticas (rates) para os possíveis resultados de um jogo, inclusive quando ele está em andamento. Uma das principais ocupações lá é a manipulação de grandes arquivos textos, com dados desconhecidos e que necessitam de um rigoroso controle de memória.

Algumas das perguntas que me foram feitas durante a entrevista, que durou quase uma hora e meia, estão listadas abaixo.

Arquitetura e padrões

  • O que é RestAPI? O que faz uma API ser REST?
  • Quais são os princípios SOLID? Você concorda com todos eles? Se você tivesse que escolher apenas um deles para seguir, qual seria?
  • Quais os design patterns da Gang of Four você conhece? Explique o pattern decorator e dê um exemplo prático de quando ele pode ser usado.
  • Dê um exemplo de quando você utilizaria um banco de dados NoSQL ao invés de um banco transacional

C#

  • O que é Injeção de Dependência (dependency injection)? Quais são as principais vantagem do seu uso? Você vê desvantagens neste tipo de abordagem? Quais os frameworks de Injeção de Dependência que você conhece/já trabalhou?
  • O que é o Garbage Collection e como ele funciona?
  • Do que trata a classe IDisposable? Cite um exemplo de quando se deve implementá-la.
  • O Linq (Language-Integrated Query) pode ser usado em quais fontes de dados (data sources)?
  • O que são tipos anônimos (anonymous type)?
  • Qual a diferença entre value type e reference type? Um objeto do tipo value type é armazenado na mémoria heap ou na stack?
  • O que são tipos dinâmicos (dynamic type)?
  • Por que um string é imutável? Se ela não fosse, quais seriam as consequências?
  • Diferencie threads de tasks.

Comentários sobre a questão envolvendo Princípios SOLID

Como pode ser visto não há nada de muito surpreendente nas perguntas, com exeção da questão sobre os princípios SOLID. Não a questão para explicar os princípios, esta é recorrente. Tão recorrente que, eu diria, está presente na maioria absoluta das entrevistas. O que surpreendeu foram as duas questões que se seguiram a partir dela.

Você concorda com todos eles? Respodi que sim, mas com restrições. Na verdade acho que o princípio open-close que diz que “as entidades devem ser abertas (open) para extensão mas fechadas (close) para alterações” pode ser quebrado quando a nova funcionalidade estiver diretamente relacionada com o que faz a classe em questão.

Por exempo, imagine que você possui uma classe que valida um conjunto de dados contendo datas e números. Depois de algum tempo, uma nova regra surge e é preciso também passar a validar se um determinado campo string possui apenas os valores válidos (por exemplo, a lista de estados brasileiros). Esta validação deveria estar em uma nova classe? Não, então o princípio open-close foi quebrado.

Se você tivesse que escolher apenas um deles para seguir, qual seria? Eu disse que a resposta óbvia aqui seria o primeiro deles (responsabilidade única – single responsability), mas que eu optaria pelo princípio de substituição de Liskov (Liskov Substitution), que diz que “se S é um subtipo de T, então os objetos do tipo T”devem poder ser substituídos pelos objetos de tipo S sem que seja necessário alterar as propriedades do programa”.

O motivo da escolha dele é que para não violá-lo as abastrações precisam ser muito bem estruturadas, ou seja, a arquitetura do software precisa ser muito bem pensada. Além disto, provavelmente será preciso usar injeção de dependência, que está diretamente correlacionado com outro princípio do SOLID: Inversão de Dependência – Dependency Inversion, o que significa que minhas classes terão baixo acoplamento, sendo assim fáceis de serem mantidas.

Roteiro de Entrevista
País: Irlanda Cargo: Desenvolvedor Sênior
Ramo de negócio da empresa: Apostas

Conceitos: Tipos Anônimos (anonymous type)

Qual das próximas afirmações descrevem corretamente o código C# abaixo (selecione todas as corretas):

var pessoa = new {Id = 1, Nome = "Maria"};

A. O código tem um erro e não compila

B. pessoa pode ser reatribuída da seguinte forma: pessoa = new {Idade = 25, Nome = “Pedro”};

C. pessoa é do tipo anônimo (anonymous type)

D. pessoa deriva de Object.

E. Os atributos Id e Nome são somente leitura (read-only)

A primeira coisa a ser feita para resolver esta questão é analisar o código, que apresenta a criação de um novo objeto chamado pessoa, sem no entanto especificar um tipo. Portanto, trata-se de um “tipo anônimo” (anonymous type, em inglês).

Os Tipos Anônimos fornecem uma maneira rápida de encapsular um grupo de propriedades em único objeto, sem que seja necessário definir explicitamente o seu tipo. Os tipos dos atributos são inferidos pelo compilador e não estão disponíveis ao nível do código fonte. Além disto, eles são somente leitura (read-only).

Agora que você já sabe o que é um tipo anônimo, vamos analisar as opções:

  • Letra A:  errada, pois o código compila perfeitamente;
  • Letra B: errada, não é possível reatribuir um tipo anônimo;
  • Letra C: correta, trata-se de um tipo anônimo;
  • Letra D: correta, tupo em C# deriva de Object; e
  • Letra E: correta, já que os atributos de tipos anônimos são somente leitura;

Sendo assim, estão corretas as letras C, D e E.

Atenção

Não confunda um tipo anônimo com a nova forma de instânciar objetos introduzida no C#9, chamada target-typed object creation. Ela permite instanciar um objeto sem usar o nome da classe depois da palavra reservada new, desde que a classe tenha sido explicitamente declarada antes do nome do objeto (mais informações em docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new – texto em inglês).

public class Pessoa {
   public int Id;
   public string Nome;
}

(...)

//a classe "Pessoa" eh explicitamente declarada antes do objeto "pessoa", 
//não sendo necessario repetir a declaracao depois da palavra  "new"
Pessoa pessoa = new(1, "Maria");
Origem da questão
País: Holanda Tipo: Conceitos Assunto: Tipos Anônimos
Ramo de negócio da empresa: Consultoria Grau de Dificuldade: Médio

Teste de código: contando os vales percorridos

Enunciado em inglês. Desde já, tenha em mente que o texto foi parcialmente alterado para diminuir a chance da solução seja encontrada por alguém que esteja fazendo a prova:

A hiker keeps records of their hikes registering when is uphill, sea level, or downhill. Hikes always start and end at sea level, and each step up or down represents a  unit change in altitude. We define the following terms:

    • A hill is a sequence of consecutive steps above sea level, starting with a step up from sea level and ending with a step down to sea level.
    • valley is a sequence of consecutive steps below sea level, starting with a step down from sea level and ending with a step up to sea level.

Given the sequence of up and down steps during a hike, find and print the number of valleys walked through.

Sample Input: 8, UDDDUDUU

Expected result: 1

Explanation: If we represent _ as sea level, a step up as /, and a step down as \, the hike can be drawn as:

_/\      _
   \    /
    \/\/

Explicando

Um caminhante registra todos seus passos durante suas caminhadas (a redundância está no texto original). Ele sempre começa as caminhadas no nível do mar, sempre que sobe registra um U (up) e sempre que desce registra um D (down).

No exemplo dado, o caminhante começa subindo um nível uma colina (UDDDUDUU), imediatamente após desce um nível (UDDDUDUU), o que faz com que ele volte ao nível do mar e depois começa a descer um vale (UDDDUDUU), sobre um nível (UDDDUDUU), volta a descer (UDDDUDUU) e depois sobe novamente até o nível do mar (UDDDUDUU). O exercício pede que seja retornado o número de vales pelo qual o caminhante passou (neste caso: 1).

Resolvendo

Há várias formas de resolver o problema, uma delas é ir contado o número de passos para cima e para baixo. Quando eles são iguais e o próximo passo for para baixo, está começando a descer um vale. Uma vez que o  número de passos volte a ser iguais, parou de descer e voltou ao nível do mar. Apesar de funcionar, não é uma solução das mais eficiente.

Outra forma, com melhor performance, é comparar o nível em que o caminhante se encontra. Considere zero como sendo o nível do mar, cada vez que subir adicione uma unidade ao nível, quando ele descer, subtraia uma unidade. Desta forma, quando o nível for menor que zero, o caminhante se encontra num vale., quando o nível voltar a zero, ele saiu do vale. Então, basta contar quantas vezes ele entrou no vale.  Abaixo o código devidamente comentado:

int contandoVales(int passos, string caminho) {
    int nivel = 0; //nivel do mar
    int numeroVales = 0;
    bool estaNumVale = false; //comeca no nivel do mar
    char desce = 'D';
 
    //percorre toda a string "caminho"
    for (int i = 0; i < passos; i++)
    {
        //se a posicao for igual a "D" subtrai uma unidade do "nivel"
        //senao adiciona uma unidade (este é um IF ternário)
        nivel = caminho[i] == desce ? --nivel : ++nivel;

        //se o nível for menor que zero e nao está no vale        
        if (nivel < 0 && !estaNumVale)
        {
            numeroVales++; //incrementa o número de vales
        }

        //se nível for menor que zero está no vale
        estaNumVale = nivel < 0;
    }

    return numeroVales;
}
Origem da questão
País: Bélgica Tipo: Teste de Código Assunto: Manipulação de strings
Ramo de negócio da empresa: Banco Grau de Dificuldade: Fácil

Teste de código: encontrar o maior número possível

O texto e valores do enunciado em inglês foram ligeiramente alterados para diminuir a chance da solução seja encontrada por alguém que esteja fazendo a prova:

Write a function that, given an integer N, returns the maximum possible value obtainable by deleting one ‘3’ digit from the decimal representation of N.

Examples: Given N = 13938, the function should return 1938.  Given N = -3839, the function should return -389. Given N = -30, the function should return 0. After deleting the ‘3’, the only digits in the number are zeroes, so its value is 0.

Assume that:

    • N is an integer within the range -99999999 to 99999999;
    • N contains at least one ‘3’ digit in its decimal representation;
    • N consists of at least two digits in its decimal representation.

In your solution, focus on correctness. The performance of your solution will not be the focus of the assessment.

Enunciado (resumido) em português

Dado um número inteiro N, retorne o maior valor possível ao remover o dígito 3 do número. Por exemplo: dado N= 393, deve retornar 93. Se N = 3153, retorna 315. Se N= -323, retorna -23. Por fim, se N = -30, retorna 0 (zero).

Assuma que sempre haverá um 3 no valor informado e este valor sempre terá pelo menos duas casas decimais.

Apesar de receber um número inteiro, trata-se de uma questão de parse de strings, uma vez que o valor precisa ser convertido para string para ser alterado (é possível trabalhar apenas com números, mas isto torna a questão muito mais complexa).

Para resolver o problema vamos pegar o valor 3153. Se remover o primeiro “3”, retorna 153, se remover o segundo 315, que é a resposta esperada.

Uma das formas de resolver o problema é identificar as posições da string que possuem o 3 e depois eliminar estas posições (indexes) uma a uma, comparando o resultado, o que gera o seguinte algoritmo em português estruturado

1. Converta o número para string
2. Percora a string identificando as posições onde o valor = 3
3. Armaze as posições num vetor 
4. Inicialize uma variável maiorValor com o menor valor possível
5. Para cada uma das posições do vetor faça
   6. Remova o caracter da posição na string original
   7. Compara o valor de maiorValor com o novo valor gerado
   8. Se o novo valor for maior, armaze no valor em maiorValor
9. Fim do para
10. Retorne o maior valor

A solução em C#

using System;
using System.Collections.Generic;

class Solution {
    //retorna um vetor com as posicoes da string "number" 
    //ocupadas pelo numero 3
    //ex: 635131 retorna [1,4] => a contagem comeca em 0
    private static List<int> GetIndexes(string number) {
        //cria uma lista (que sera retornada)
        var indexes = new List<int>();

        //percore todos os caracteres da string
        //number.Length = 6, para o numero 635131 
        for(int i = 0; i< number.Length; i++) {
            //se caracter = 3, adiciona o indice na lista
            if (number[i] == '3') {
                indexes.Add(i);
            }
        }
        return indexes;
    }

    public int solution(int N) {
        //conver o numero N para uma string
        var originalNumber = N.ToString();
        //pega a lista de indices
        var indexes = GetIndexes(originalNumber);
        //seta o maior valor como o menor inteiro existe
        //assim qualquer valor gerado serah maior q ele
        var maxValue = int.MinValue;

        //percore todos os elementos do vetor
        foreach(var index in indexes) {
           //remove o caracter da posicao armazenada em GetIndexes()
            var number = originalNumber.Remove(index,1);
            //transforma a string num numero inteiro
            var newNumber = int.Parse(number);

            //se o "valor maximo" for menor que o "novo valor"
            //seta o "valor maximo" como sendo o "novo valor"
            if (maxValue < newNumber) {
                maxValue = newNumber;
            }
        }
        return maxValue;
    }
}

 

Origem da questão
País: Irlanda Tipo: Teste de Código Assunto: manipulação de strings
Ramo de negócio da empresa: Pagamentos Grau de Dificuldade: Fácil

Banco de Dados: Select recursivo e subselect em tabelas com autorelacionamento

Questão de Entrevista
País: Irlanda Tipo: Técnica Assunto: Banco de Dados
Ramo de negócio da empresa: Financeiro (Banco) Grau de Dificuldade: Média

 

Data a tabela Employee, escreva consultas que retorne:

  1. O nome e o salário dos empregados que recebem mais do que os seus gerentes (manager) diretos, ordenando o resultado pelo salário de forma decrescente
  2. O nome dos empregados que possuem exatamente outros dois empregados com o mesmo dia e mês de aniversário.

tabela employee (empregado)

Abaixo o script para criar e inserir os dados da tabela (padrão ANSI, pode ser usado em qualquer banco de dados)

-- cria a tabela Employee (Empregado)
-- com chave primária nomeada como PK_Employee 
CREATE TABLE dbo.Employee(
  Id int NOT NULL,
  Name varchar(50) NOT NULL,
  Birthday datetime NOT NULL,
  Salary numeric(18, 2) NOT NULL,
  ManagerId int,
  CONSTRAINT PK_Employee 
  PRIMARY KEY (Id)
)

-- cria uma chave estrangeira (foreign key) 
-- relacionando o gerente (ManagerId) ao empregado (Id)
ALTER TABLE dbo.Employee  
WITH CHECK ADD CONSTRAINT FK_Employee_Employee 
FOREIGN KEY(ManagerId)
REFERENCES dbo.Employee (Id)

--insere registros na table a Employee 
INSERT INTO Employee 
       (Id,Name,Birthday,Salary,ManagerId) 
VALUES (1, 'John', '1978-02-22', 5000, null)

-- como todos os campos estão sendo usados, podemos simplificar 
-- o comando ocultado p nome das colunas na instrução INSERT
INSERT INTO Employee VALUES (2, 'Mary', '1983-08-25', 5200, 1)
INSERT INTO Employee VALUES (3, 'Paul', '1994-11-07', 3750, 2)
INSERT INTO Employee VALUES (4, 'Anna', '2001-12-18', 2450, 3)
INSERT INTO Employee VALUES (5, 'Aoifa', '1994-11-07', 4200, 3)
INSERT INTO Employee VALUES (6, 'Emily', '1995-03-05', 3200, 1)
INSERT INTO Employee VALUES (7, 'Alan', '1997-05-26', 3800, 3)
INSERT INTO Employee VALUES (8, 'Lucy', '2001-11-26', 3800, 2)
INSERT INTO Employee VALUES (9, 'Adan', '1989-08-25', 4760, null)
INSERT INTO Employee VALUES (10, 'Will', '2001-12-01', 2800, null)
INSERT INTO Employee VALUES (11, 'Sophia', '2002-04-08', 2930, 10)
INSERT INTO Employee VALUES (12, 'Emma', '1998-09-20', 2560, 10)
INSERT INTO Employee VALUES (13, 'James', '1995-08-25', 2560, 9)
INSERT INTO Employee VALUES (14, 'Charlotte', '1995-10-12', 2980, 9)
INSERT INTO Employee VALUES (15, 'Lucas', '1986-01-11', 2900, 10)

Vamos a questão 1: retornar o nome e o salário dos empregados que recebem mais do que os seus gerentes diretos, ordenando os registros pelo salário de forma decrescente.

Analisando os dados nota-se que a coluna ManagerId é uma auto-referência para a tabela Employee, ou seja, indica o registro na tabela que se refere ao gerente da pessoa em questão. Por exemplo: John (Id = 1) não tem um gerente direto, pois seu ManagerId é nulo. Já Mary (Id= 2) é subordinada a John, pois seu ManagerId é igual 1, que é o Id de John. Mary tem um salário de $5200, enquanto John tem um salário de $5000, logo Mary deve ser listada.

Uma das formas de resolver este problema é usando um subselect e comparar o salário do cada registro da tabela Employee com o salário do seu respectivo gerente. Como quem não tem gerente direto não precisa ser listado, por questões de desempenho pode-se adicionar um filtro para ignorar estas pessoas, com isto o subselect não será executado nestes casos (o engine do banco de dados sabe que uma expressão com subselect é mais cara, computacionalmente falando, que uma expressão que usa apenas um campo da própia tabela, como o operador condicional AND (E) exige que as duas expressões sejam verdadeira para ser verdadeiro, ele executa primeiro a condição e.ManagerId is not null e ignora a segunda expressão se a primeira for falsa):

select Name, Salary 
from Employee e
where e.ManagerId is not null
  and e.Salary > (select Salary 
                  from Employee e2 
                  where e2.Id = e.ManagerId)
order by e.Salary desc

Executando o comando acima:

Name Salary
Mary 5200.00
Aoifa 4200.00
Alan 3800.00
Sophia 2930.00
Lucas 2900.00

Outra forma de resolver o problema é fazer um INNER JOIN da tabela Employee com ela mesma. Esta forma, além de ter melhor performance, permite listar o nome e o salaário do gerente, o que facilita a verificação do resultado (isto está fora do escopo do exercício inicial, se o exercício for corrigido eletronicamente, devolva exatamente o que se pede. Se for uma programação em par – entrevistador e entrevistado – você pode usar mas deve destacar o que está fazendo):

select e.Name, e.Salary, e2.Name Manager, e2.Salary 
from Employee e
inner join (select Id, Name, Salary from Employee) e2 
      on e2.Id = e.ManagerId
where e.Salary > e2.Salary
order by e.Salary desc

Perceba que no exemplo acima a condição e.ManagerId is not null foi suprimida. Como foi usado um INNER JOIN, que obriga que o registro exista dos dois lados da junção, os registros com ManagerId = NULL são automaticamente descartados.

Name Salary Manager Salary
Mary 5200.00 John 5000.00
Aoifa 4200.00 Paul 3750.00
Alan 3800.00 Paul 3750.00
Sophia 2930.00 Will 2800.00
Lucas 2900.00 Will 2800.00

Agora a questão 2: listar o nome nome dos empregados que possuem exatamente outros dois empregados com o mesmo dia e mês de aniversário.

Para responder a questão vamos usar a mesma base de comando da primeira opção de resposta da questão 1, com alterações na cláusula where da subconsulta. Para comprar os dias e meses foram usadas, respectivamente, as funções DAY e MONTH (estas funções são do SQL Server, consulte a documentação do seu banco de dados para encontrar a função equivalente).

select Name
from Employee e
where (select count(*) 
       from Employee e2 
       where e2.Id <> e.Id
         and day(e2.Birthday) = day(e.Birthday) 
         and month(e2.Birthday) = month(e.Birthday)) = 2

O resultado é:

Name
Mary
Adan
James

Note que Aoifa e Paul também fazem aniversário no mesmo dia e mês (07 de novembro), mas o exercício pede que sejam listadas apenas o empregados que possuem exatamente outros dois com o mesmo dia e mês de aniversário, por isto a inclusão da condição e2.Id <> e.Id na cláusula where da subconsulta.

O que é Injeção de Dependência? Vantagens e desvantagens

Uma das perguntas que mais ouço em entrevistas é: o que é Injeção de Dependência? Normalmente esta pergunta vem acompanhada de outras duas: (1) quais suas vantagens e desvantagens? e (2) que frameworks você conhece que implementam este pattern (padrão)?

O que é Injeção de Dependência?

Injeção de Dependência (DI, do termo em inglês dependency injection) é um padrão de desenvolvimento que implementa Inversão de Controle (IoC – inversion of control – um dos cinco princípios SOLID) para resolver dependências entre classes, mantendo as classes com baixo nível de acomplamento. Neste tipo de solução, ao invés de instanciar objetos diretamente ou usar referências estáticas, os objetos são fornecidos (“injetados”) por um framework sempre que necessário, normalmente através do construtor ou de propriedades.

Acoplamento, neste contexto, é a conexão/dependência/interação entre classes. Quanto maior for o acoplamento, maior a coesão e mais difícil e trabalhosa é a manutenção do sistema.

Pode-se implementar de três maneiras diferentes, através de:

  1. Constructor: modo em que implementa-se DI a na definição dos construtores das classes;
  2. Getter e Setter: implementa DI através dos métodos get e set das propriedades;
  3. Service Locator: usa-se classes que servem como “localizadoras” de objetos que são instanciados na classe.

Injeção de Depêndencia: vantagens e desvantagens

As principais vantagens são:

  • Se não elimina, ou ao menos reduz significativamente a dependência entre as classes;
  • Aumenta a reusabilidade de componentes – como eles não sabem como as classes das quais dependem são implementadas, tornam-se mais reutilizáveis;
  • Reduz a complexidade do módulo (classe);
  • Facilita a execução de testes unitários (unit tests), uma vez que este tipo de padrão utiliza interfaces, que podem ser facilmente “mockadas”; e
  • Tona o código mais legíveis e, por consequência, facilita a manutenção projeto;

Dentre as desvantagens, pode-se citar:

  • Aumenta a complexidade do projeto e pode requer mais esforço de desenvolvimento inicial;
  • Faz com que os clientes exigem detalhes de configuração fornecidos pelo código de construção;
  • Torna o debug mais difícil, pois torna o código mais difícil de rastrear uma vez que separa o comportamento da construção da classe que é injetada.

Frameworks para Injeção de Depêndencia

Além do .NET, há outros frameworks que implementam injeção de depêndencia, os mais conhecidos são:

  • Unity (unitycontainer.org): projeto criado e desenvolvido pela Microsoft e entregue à comunidade em 2015. Suporta .NET Framework 4.0+, .NET Core e .NET Standard 1.0 e 2.0, o código pode ser baixado em github.com/unitycontainer/unity. Número de download no NuGet: 44 milhões.
  • Autofac (autofac.org): projeto open source com código disponível em github.com/autofac/Autofac. Suporta .NET Core, ASP.NET Core, .NET Standard 2.1 e .NET Framework 4.5.1+. Número de download no NuGet: 38,8 milhões.
  • Ninject (ninject.org): sua versão mais atual (3.3.4) suporta .NET Framework 4.5 e .NET Standard 2.0. O código está disponível em github.com/ninject/ninject. Número de download no NuGet: 23,4 milhões.
  • Castle Windsor (castleproject.org/projects/windsor): mais um projeto open source hospedado no GitHub github.com/castleproject/Windsor. Sua versão mais atual (5.1.1) suporta .NET Framework 4.5 e .NET Standard 1.6. Número de download no NuGet: 20,4 milhões.

 

 

Olá, mundo!

Saí do Brasil em setembro de 2109, aos 41 anos, com esposa e duas filhas pequenas de 1,5 e 7 anos. Morar fora sempre foi um sonho, mas só comecei a tomar as atitudes necessárias para realizá-lo uns três anos antes. Por atitudes entenda: (1) melhorar o nível do inglês, (2) aprender tecnologias novas e (3) se preparar psicologicamente para ficar longe da família e amigos.

Enviei meu currículo para cerca de 100 empresas de diferentes países. Devo ter feito duas dezenas de entrevistas antes de ser aprovado e aceitar uma proposta para trabalhar como Tech Leader C# numa empresa de consultoria em Portugal.

O mercado de TI português, e o europeu em geral, é ainda mais competitivo e aquecido do que o brasileiro. Por consequência, novas oportunidades surgem toda a semana. Em dois anos em Portugal fui contactado e fiz entrevistas nao apenas para outras empresas portuguesas, mas também para empresas da Alemanha, Bélgica, Finlândia, Holanda, Inglaterra, Irlanda, Itália, Malta e Suíça.

Recebi propostas formais para me mudar para Bélgica, Holanda, Malta e Irlanda. Aceitei esta última, para trabalhar com microserviços numa empresa que tinha na época nove times Scrum (hoje tem treze).

Alguém escreveu que passar em entrevista é uma skill. É a mais pura verdade. Quanto mais se pratica, mais fácil fica (ou menos difícil). Antes de decidir sair do país eu não fazia entrevistas. Quer dizer, na verdade fazia, mas estava do outro lado. Como tinha minha própria empresa e eventualmente precisava contratar novos estagiários e/ou funcionários, eu entrevistava. A partir do momento que troquei de lado, percebi o quanto praticar influenciava no meu desempenho.

Este site é um projeto pessoal para tentar ajudar as pessoas a passar em entrevistas. Seja para o primeiro emprego ou para uma posição com maior senioridade. Seja para atuar no Brasil ou para imigrar ou trabalhar remotamente para empresas do exterior.

De 2018 até hoje (final de 2021), em quase uma centena entrevistas, acumulei um grande repertório das perguntas e testes de código. Boa parte dessas perguntas se repetem e a ideia é colocá-la aqui, para auxíliar aqueles que estão passando pelo processo ou se preparando para ele.

O conteúdo será majoritariamente em português, apesar das questões terem sido feitas para processos de outros países, de forma a ser o mais inclusivo possível e assim poder contribuir com aqueles que estão iniciando na área e ainda não possuem o domínio mínimo da língua inglesa.