[Pesquisar este blog]

quarta-feira, 28 de dezembro de 2016

Desenvolvedor Poliglota

Poliglota. Segundo o Grande Dicionário Houaiss, adjetivo e substantivo de dois gêneros, cujo principal significado é aquele que sabe ou fala muitas línguas. Este termo é geralmente aplicado para pessoas que falam mais de três línguas. Mas também pode ser aplicado aos desenvolvedores capazes de construir softwares usando várias linguagens de programação.



Neste post vamos discutir (e incentivar) o aprendizado de várias linguagens de programação partindo do argumento conhecido da necessidade de conhecer vários idiomas.

Bilíngue, Trilíngue, Poliglota, Hiperglota


O domínio de vários idiomas é considerando, pela maioria dos recrutadores, um diferencial, que potencializa as qualidades de qualquer candidato, como nesta matéria da revista Exame.

Mas o maior benefício de falar várias línguas é o maior desenvolvimento de empatia, da criatividade, sem contar que é benéfico para o cérebro em qualquer idade, como afirma a revista Deutsche Welle. Não é necessário ser um hiperglota.

Linguagens de Programação

As estimativas sobre o número de linguagens de programação existentes são bastantes imprecisas, mas são várias centenas. Um artigo na DZone lista 256 (duzentas e cinquenta e seis) linguagens de programação diferentes, começando por 4D (4th Dimension), ABAP, ABC, e terminando em yacc, Yorick e Z Shell.

E por que tantas linguagens? O grande número de linguagens de programação que surgiu é justificado por dois fatores principais: evolução e necessidade.

É bastante comum que, após o uso sistemático de uma linguagem de programação qualquer, que sejam observadas limitações em sua sintaxe e até mesmo defeitos em seu projeto. Isto estimula a criação de novas versões desta linguagem. Mas como existem questões comerciais, problemas com retrocompatibilidade e outras, novas versões podem não atender os anseios dos programadores, motivando a construção de novas linguagens derivadas. Isto é a evolução, vista como motor de criação de novidades.

Outra situação é que problemas específicos, difíceis de solucionar com as linguagens disponíveis (ou conhecidas), acabem sendo solucionados por linguagens de programação construídas para esta finalidade ou para este domínio. Aqui a necessidade é a mãe da invenção.

Assim, muitas linguagens de programação são semelhantes, compartilhando uma raiz comum que nada mais é que o modelo conceitual tomado para seus projetos. Partindo da linguagem C (que também foi uma evolução de outras linguagens), do paradigma imperativo, surgiram: C++, que acrescentou o paradigma da orientação a objetos; PHP, voltado para o desenvolvimento web; Java, com sua natureza multiplataforma; C#, uma releitura deste mesmo conjunto de linguagens para a plataforma Microsoft; entre outras.

O que é importante notar é que enquanto algumas linguagens de programação são semelhantes; outras são substancialmente diferentes. E tais diferenças enfatizam os propósitos de cada uma destas alternativas.

Porque ser um Desenvolvedor Poliglota


Exatamente pelo fato de cada linguagem ter sido criada com objetivos claros em mente, a solução de um problema específico pode ser bastante facilitada pela escolha da linguagem de programação mais adequada. Note que a palavra usada aqui é adequada, pois não existe opção correta ou incorreta, mas apenas uma escolha que pode conduzir a melhores resultados no processo de desenvolvimento.

Para que possamos escolher uma linguagem de adequada para a solução de um problema, dois fatores são determinantes: quais linguagens de programação são conhecidas; e quais são convenientes no cenário do problema.

Se não conhecemos linguagens diferentes, não poderemos escolher. E aquela que conhecemos pode não ser a melhor alternativa. Além disso, o cenário do problema é preponderante: se o cliente tem uma coleção de aplicações numa linguagem específica, como propor a construção de um novo software com outra plataforma, que só dificultara a reutilização de módulos e a integração?

Conhecer várias linguagens é, então, uma necessidade do desenvolvedor de software profissional, ou seja, é importante ser um programador poliglota!

Linguagens de Programação Populares


Existem vários rankings, mantidos cuidadosamente por revistas especializadas e outras organizações na internet, interessantes em medir a popularidade das linguagens de programação. Cada índice tem metodologia própria, diferente, mas que mostra uma visão distinta da questão central: qual a linguagem de programação com maior utilização?

Seguem as oito linguagens de programação mais populares segundo os dados colhidos de alguns dos rankings disponíveis para 2016: 
  • TIOBE:
    Java, C, C++, Python, VB.NET, C#, PHP, JavaScript
  • GitHub PYPL:
    Java, Python, PHP, C#, JavaScript, C++, C, Objective-C
  • RedMonk:
    JavaScript, Java, ,PHP, Python, C#, C++, Ruby, CSS
  • English4IT:
    Java, C, C++, PHP, VB.NET, Python, C#, JavaScript
  • IEEE Spectrum:
    C, Java, Python, C++, R, C#, PHP, JavaScript
  • NewRelic:
    Java, C#, C++, JavaScript, PHP, Python, Ruby, C

É fácil notar que algumas linguagens estão sempre nestes rankings, o que é ilustrado na figura que segue. A pontuação atribuída (8 para 1º, 7 para 2º, até 1 para 8º) permite agregar as listas diferentes numa última classificação (última coluna da figura). Lá vemos: Java, C++, Python, C, PHP, C#, JavaScript e VB.NET.

Rankings 2016 das Linguagens de Programação

Java não é surpresa no primeiro lugar. Mas as veteranas C++ e C continuam nas listas das populares. Python vem crescendo em termos de popularidade. PHP e JavaScript são linguagens para web (no criterioso ranking da StackOverflow, JavaScript figura como a linguagem de programação mais usada no mundo). Se outros rankings forem consultados, veremos outras diferenças, como na figura abaixo:



De fato, a colocação nestes rankings não importa quase nada. O que vale, mesmo, é que a linguagem aparece na listagem de popularidade. E aí, a pergunta que não quer calar: você é capaz de programar em várias destas linguagens?

Como é muito provável que seu próximo empregador deseje isto, que seus colegas trabalhem com isso, e que existam muitos projetos que fazem uso destas linguagens, que tal pensar em expandir seus conhecimentos desenvolvendo suas habilidade de programação numas quatro ou cinco destas linguagens?

Espero ter convencido você! Fica a dica!

Para saber mais




quinta-feira, 22 de dezembro de 2016

Coleções e Streams outra vez::Parte IV

A nova API Stream foi adicionada ao Java 8 permitir a realização de operações de filtragem, mapeamento e redução sobre coleções. Grande parte de seus métodos são encadeáveis a outros, como na programação funcional, permitindo a criação de pipelines de operações. Assim é possível tratar os elementos de uma coleção sem a necessidade de construção explícita de laços de repetição e, com isso, especificando a realização de operações em massa sobre as coleções.

Segundo a Oracle:
Classes do novo pacote java.util.stream compõem a API Stream para suportar operações no estilo funcional em streams de elementos. A API Stream á integrada a API de Coleções, o que possibilita operações em massa sobre as coleções, tal como transformações de mapeamento/redução sequenciais ou paralelas. (Oracle, 2015, http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html )


Neste último artigo desta série sobre coleções e streams serão comentados outros exemplos para mostrar outras aplicações desta API. Esta série foi dividida em  quatro artigos:

Nos fragmentos de código que seguem, o comentário inicial indica o nome arquivo-fonte Java onde estão contidos, permitindo que sejam testados com maior facilidade. Todos exemplos estão disponíveis no GitHub (projeto pjandl/stream_again).

Mind Map

Na figura abaixo temos um mapa mental para auxiliar no uso da API Stream. Do lado esquerdo é possível visualizar Stream<T>, que representa um stream de qualquer tipo T. A partir de um stream deste tipo, são possíveis operações terminais, operações terminais de mapeamento e outras operações intermediárias.

Mapa mental para operações terminais e intermediárias de Stream<T>.
As operações terminais estão agrupadas na parte superior da figura e sua execução resulta em tipos primitivos (boolean e long) ou tipos objeto (T, Optional<T>, Object e arrays). Muitas destas operações são de redução, pois processam um stream produzindo um resultado final de tipo específico.

As operações terminais de mapeamento resultam em streams de tipo R ou de outros tipos diferentes de Stream<T>. Com estes streams é possível encadear novas operações, criando pipelines do resultado do mapeamento.

Na parte inferior da figura estão ilustradas operações intermediárias, que a partir do stream de entrada Stream<T> resultam em outro stream do mesmo tipo Stream<T>, permitindo o encadeamento de operações e a construção de pipelines de operações.

Uma aplicação

A Enviromental Protection Agency (EPA) é uma agência norte-americana que divulga anualmente informações sobre os automóveis à venda nos EUA sob o nome de Fuel Eficiency Guide. Para cada modelo temos dados: do fabricante, do motor, da transmissão, do combustível usado e do consumo.

Com a planilha de dados de 2015, foi gerado um arquivo CSV (Comma Separated Values), ilustrado abaixo.

Fragmento de arquivo CSV originado do Fuel Eficiency Guide 2015/EPA.
O programa que segue, ProcessCSVtoList lê o arquivo CSV denominado "2015 FEGuide.xlsx", criando um objeto Car com os dados de cada modelo encontrado no arquivo. A lista criada, na verdade uma instância de ArrayList, é serializada, permitindo que os dados dos modelos sejam persistidos e usados por outras aplicações sem a necessidade de bancos de dados ou da repetição deste processamento.

// ProcessCSVtoList.java
package jandl.streamAgain.app;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

public class ProcessCSVtoList {

public static void main(String[] args) throws Exception {
List<Car> list = new ArrayList<>();
BufferedReader br = new BufferedReader(
  new FileReader("2015 FEGuide.csv"));
String line = br.readLine();
while ((line = br.readLine()) != null) {
StringTokenizer st = new StringTokenizer(line, ";");
if (st.countTokens() == 22) {
String[] data = new String[22];
int i = 0;
while (st.hasMoreTokens()) {
data[i] = st.nextToken();
i++;
}
list.add(dataToCar(data));
}
}
System.out.println("cars: " + list.size());
br.close();
serializeList(list, "2015carlist.ser");
}

public static Car dataToCar(String[] data)
  throws ParseException {
Car car = new Car();
car.modelYear = Integer.parseInt(data[0]);
car.manufacturerName = data[1];
car.manufacturerDivision = data[2];
car.carline = data[3];
NumberFormat nf = NumberFormat.getNumberInstance();
car.engineVolume = nf.parse((data[4])).doubleValue();
car.numberOfCylinders = Integer.parseInt(data[5]);
car.transmission = data[6];
car.fuelEficiencyCity = Integer.parseInt(data[7]);
car.fuelEficiencyHighway = Integer.parseInt(data[8]);
car.fuelEficiencyCombined = Integer.parseInt(data[9]);
car.airAspirationMethod = data[10];
car.transmissionDescription = data[11];
car.numberOfGears = Integer.parseInt(data[12]);
car.driveDescription = data[13];
car.maximumEthanolPercentage = Integer.parseInt(data[14]);
car.maximumBiodieselPercentage = Integer.parseInt(data[15]);
car.fuelUsage = data[16];
car.numberOfIntakeValvesPerCyl = Integer.parseInt(data[17]);
car.numberOfExhaustValvesPerCyl = Integer.parseInt(data[18]);
car.varValveTimingDescription = data[19];
car.fuelMeteringSysDescription = data[20];
car.oilViscosity = data[21];
return car;
}

public static void serializeList(List<Car> list, String fileName)
     throws Exception {

ObjectOutputStream oos = new ObjectOutputStream(
  new FileOutputStream(fileName));
oos.writeObject(list);
oos.close();
}

public static List<Car> deserializeList(String fileName)
  throws Exception {
ObjectInputStream ois = new ObjectInputStream(
  new FileInputStream(fileName));
@SuppressWarnings("unchecked")
List<Car> list = (List<Car>) ois.readObject();
ois.close();
return list;
}
}

A classe  ProcessCSVtoList contém quatro métodos estáticos: main(String[]), dataToCar(String[]), serializeList(List<Car>, String) e serializeList(String). O método main(String[], que constitui o início da aplicação, lê o arquivo CSV e, com o auxílio de um StringTokenizer, separa os dados de cada campo dos automóveis, armazenando-os em um array de String. Este array é transformados em um objeto Car por meio do  uso do método dataToCar(String[]), cuja implementação é pouco elegante, mas adequada.

O método serializeList(List<Car>, String) realiza a serialização do objeto List<Car> recebido, salvando-o num arquivo cujo nome é dado pelo segundo parâmetro. Já o método deserializeList(String) efetua a operação inversa, ou seja, recupera um ArrayList de objetos Car do arquivo denominado pelo parâmetro recebido.

A classe Car é muito simples, constituindo-se apenas de um conjunto de campos para armazenar os dados de cada modelo. O único método disponível é toString(), implementado para facilitar a exibição de tais dados.

// Car.java
package jandl.streamAgain.app;

import java.io.Serializable;

public class Car implements Serializable {
private static final long serialVersionUID = 1L;

int modelYear;
String manufacturerName;
String manufacturerDivision;
String carline;
double engineVolume;
int numberOfCylinders;
String transmission;
int fuelEficiencyCity;
int fuelEficiencyHighway;
int fuelEficiencyCombined;
String airAspirationMethod;
String transmissionDescription;
int numberOfGears;
String driveDescription;
int maximumEthanolPercentage;
int maximumBiodieselPercentage;
String fuelUsage;
int numberOfIntakeValvesPerCyl;
int numberOfExhaustValvesPerCyl;
String varValveTimingDescription;
String fuelMeteringSysDescription;
String oilViscosity;

public String toString() {
return manufacturerName + " "+ carline + " " + engineVolume + " " + transmission;
}
}

Considerando que existem mais de 1000 modelos diferentes na lista que dispomos, para encontrarmos aqueles cujos motores possuem, por exemplo, 10 cilindros, seria necessário código como:

List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int iCylinders = 10;
// código convencional
for(Car c: lista) {
if (c.numberOfCylinders==iCylinders) {
System.out.println(c);
}
}

Com o uso da API Stream e de expressões lambda, tal construção se reduz para:

// StreamFrag401.java
lista.stream()
     .filter(car -> car.numberOfCylinders==cylinders)
     .forEach(c-> System.out.println(c));

Observe que a variável cylinder, definida fora da expressão lambda, pode ser utilizada sem qualquer problema, desde que para leitura. Isto é chamado de captura de variáveis.

Caso seja necessário a contagem dos elementos filtrados pelo pipeline, o uso de variáveis simples não é adequado, pois não podem ser alteradas (as expressões lambdas se comportam como closures e não produzem efeitos colaterais) requerendo um pequeno expediente: o uso de um objeto para realizar a contagem.

O código convencional que segue filtra e conta veículos que podem utilizar um percentual mínimo de etanol indicado pela variável ethanol.

List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int ethanol = 85;

// código convencional
int count = 0;
for(Car c: lista) {
if (c.maximumEthanolPercentage>=ethanol) {
System.out.println(c);
count++
}
}
System.out.println("count = " + count);

Como antes, o uso da API Stream e expressões lambda simplifica bastante o código necessário. Observe também o uso de uma instância da classe Counter, dada a seguir, que implementa um conveniente contador.

Counter counter = new Counter();
lista.stream()
     .filter(car -> car.maximumEthanolPercentage>=ethanol)
     .forEach(c-> { System.out.println(c); counter.inc(); });
System.out.println("count = " + counter.count);

A classe Counter é simples, direta e útil!

package jandl.streamAgain.app;

public class Counter {
public int count;
public Counter() { this(0); }
public Counter(int count) { count = 0; }
public void inc() { count++; }
public void preset(int count) { this.count = count; }
public void reset() { count = 0; }
}

Composição de Predicados

A filtragem de elementos de um stream utiliza-se de predicados, isto é, de objetos que implementam a interface funcional Predicate<T>, convenientemente substituíveis por expressões lambda. Quando o critério de filtragem é simples, tal como mostrado nos fragmentos anteriores, a expressão lambda obtida é igualmente simples.

Mas existem situações onde o critério de filtragem é composto e, portanto, mais complexo. Como por exemplo, filtrar modelos que aceitem mais de 50% etanol, que tenham motores de 6 cilindros e transmissão com 7 ou mais velocidades. Para efetuar tal filtragem é possível escrever:

lista.stream()
.filter(car -> car.maximumEthanolPercentage >= ethanol &&
car.numberOfCylinders==iCylinders &&
car.numberOfGears>=gears)
.forEach(c ->System.out.println(c));

É fácil perceber que predicados compostos tem legibilidade comprometida. Para contornar isso, mantendo o código legível e mais fácil de manter, podemos fazer como no fragmento que segue:

// StreamFrag403.java
List<Car> lista =
    ProcessCSVtoList.deserializeList("2015carlist.ser");


int ethanol = 50;
int iCylinders = 6;
int gears = 7;

Predicate<Car> ethanolPredicate =
    car -> car.maximumEthanolPercentage >= ethanol;

Predicate<Car> cylinderPredicate =
    car -> car.numberOfCylinders==iCylinders;

Predicate<Car> gearPredicate =
    car -> car.numberOfGears>=gears;


lista.stream()     .filter(ethanolPredicate
             .and(cylinderPredicate)
             .and(gearPredicate))
     .forEach(c -> System.out.println(c));

Assim, cada predicado é definido separadamente e composto por meio de operações disponíveis na interface Predicate<T>, melhorando a legibilidade, manutenabilidade e reusabilidade do código.

Execução sequencial ou paralela

A API Stream possibilita a execução sequencial ou paralela de suas operações, o que é determinado pela maneira com que o stream é criado a partir de sua fonte.

Enquanto o método default stream() retorna um stream sequencial de uma coleção, seu correspondente parallelStream() retorna um stream cuja execução acontecerá em paralelo. Num stream sequencial as operações acontecerão na ordem em que os elementos do stream são encontrados, ou seja, sequencialmente, sendo executadas em uma thread (e portanto ocupando apenas um processador ou núcleo do sistema). Já num stream paralelo, várias operações serão executadas concorrentemente em múltiplas threads, utilizando os elementos do stream em ordem diversa da sequencial, sendo possivelmente executados por vários processadores (ou núcleos) do sistema. Isto permite explorar melhor os recursos do sistema.

No fragmento que segue, temos uma grande lista com 2 milhões de elementos inteiros gerados aleatoriamente.

// StreamFrag404.java
int MAXIMO = 2000000;
List<String> valores = new ArrayList<>(MAXIMO);
for (int i = 0; i < MAXIMO; i++) {
UUID uuid = UUID.randomUUID();
valores.add(uuid.toString());
}

Os elementos desta lista são ordenados com uso de uma stream sequencial e de uma stream paralela, exibindo diferentes tempos de processamento. Observe que os trechos de código são idênticos, exceto pela forma de obtenção do stream.

// StreamFrag404.java
long tIni, tFim, num, tDecorrido;

// ordenação sequencial
tIni = System.nanoTime();
num = valores.stream().sorted().count();
System.out.println(num);
tFim = System.nanoTime();
tDecorrido = TimeUnit.NANOSECONDS.toMillis(tFim - tIni);
System.out.printf("Ordenacao sequencial: %d ms\n", tDecorrido);
// ordenação paralela
tIni = System.nanoTime();
num = valores.parallelStream().sorted().count();
System.out.println(num);
tFim = System.nanoTime();
tDecorrido = TimeUnit.NANOSECONDS.toMillis(tFim - tIni);
System.out.printf("Ordenacao paralela  : %d ms\n", tDecorrido);

Como esperado, o tempo de processamento com uso de um stream paralelo é menor do que com o uso do stream sequencial.

No próximo fragmento, a lista de modelos de veículos é filtrada para exibição daqueles com tranmissão com 9 velocidades.

// StreamFrag405.java
List<Car> lista = ProcessCSVtoList.deserializeList("2015carlist.ser");
int gears = 9;

System.out.println("==========");
Counter counter = new Counter();
lista.stream()
.filter(car -> car.numberOfGears==gears)
.forEach(s-> { System.out.println(s); counter.inc(); });
System.out.println("count = " + counter.count);
System.out.println("==========");
counter.reset();
lista.parallelStream()
.filter(car -> car.numberOfGears==gears)
.forEach(s-> { System.out.println(s); counter.inc(); });
System.out.println("count = " + counter.count);
System.out.println("==========");

Enquanto que com uso do stream sequencial os modelos são exibidos na mesma ordem em figuram na lista completa, a utilização do stream paralelo produz um resultado com ordenação diversa, pois threads diferentes processaram porções distintas da lista. Mas o mesmo número de registros é exibido pelas duas estratégias.

Conclusões

A API Stream adiciona um conjunto extremamente flexível e poderoso de características às Coleções, facilitando muito a produção de código mais simples, de melhor legibilidade, de manutenção mais fácil e, possivelmente de reuso mais fácil.
No entanto, é bastante provável que não existam ganhos de desempenho por parte da aplicação, pois tais faciilidades oneram os tempos de execução. Ainda assim não devem ser desprezadas, pois em geral, a perda de perfomance não será perceptível para aplicações comuns, tornando substancial o ganho de produtividade do programador. Já aplicações massivas e de alto desempenho terão que utilizar mais cautelosamente esta API, balanceando os ganhos possíveis decorrentes do desenvolvimento simplificado com os requisitos de performance exigidos pela aplicação.
Conhecer e explorar esta API é, desta maneira, mandatório, para que possa ser empregada com sucesso conforme as demandas de cada projeto!

Este artigo faz parte de uma pequena série Coleções e Streams outra vez:

Para saber mais


domingo, 11 de dezembro de 2016

Aprender sempre::educação continuada e pós-graduação

É unânime! Qualquer gestor de recursos humanos; executivo de empresa de qualquer porte ou segmento; profissional de coaching ou mentoring; especialista em desenvolvimento de carreira; e professor vai afirmar categoricamente: precisamos aprender sempre e ao longo de toda vida.


Por que?

Objetivamente, o trabalho faz parte da vida da grande maioria das pessoas, que, por muitas e diversas razões trabalham e buscam o trabalho como maneira de prover o sustento próprio e de suas famílias; mas também como um caminho para que alcancem seus sonhos, nem sempre materiais.

O exercício de qualquer profissão requer dedicação, esforço e capricho, podendo se apropriar de novas tecnologias, materiais e processos para que aconteça de maneira mais eficiente, lucrativa, benéfica e, por que não, mais proveitosa e prazerosa para todos os envolvidos.

Assim, independente da profissão e do ramo de atuação, a educação e o aperfeiçoamento profissional tem que ser vistos como necessidades constantes, daí o termo educação continuada. Segundo Angélica Alvim, diretora do Mackenzie, "É importante que os profissionais façam especialização e aperfeiçoamento para que se mantenham no mercado". Esta afirmação coincide com o depoimento de muitos profissionais de RH que colocam que, para as empresas de hoje, resolver problemas conhecidos já não suficiente, mas a capacidade e a iniciativa de identificar novos problemas, propor e desenvolver soluções para enfrentá-los.

A grande integração econômica proporcionada pela globalização trouxe enormes facilidades para as empresas em termos de acesso a novos mercados, tecnologias e fornecedores. Mas veio acompanhada de desafios. O principal é a manutenção da competitividade numa situação de multiplicidade de concorrentes, consumidores mais exigentes, transformações sociais constantes e crises, muitas crises.

Segundo Leonardo Trevisan, professor da PUC, o vocabulário empresarial inclui expressões como realidade virtual, internet das coisas, big data, entre outras coisas. Ou seja, as novas tecnologias estão transformando a indústria e as corporações, tornando cada vez necessários profissionais qualificados e especializados.

Este cenário complexo cria uma situação bem simples de ser entendida: sem conhecimento, a vida no trabalho será cada vez mais difícil, até mesmo porque as oportunidades, que sempre existirão, serão encontradas por aqueles que estiverem preparados para enfrentá-las.

Então escolher e concluir um bom curso de graduação é um investimento necessário para se colocar favoravelmente em relação as demandas das empresas. E para que sua carreira possa seguir num crescente, a pós-graduação, na forma de cursos de especialização, mestrado e doutorado, é essencial para galgar posições de maiores responsabilidade e, claro, maiores ganhos.

Ao mesmo tempo, exatamente pelo fato do trabalho ocupar uma parte substancial de nossas vidas, é muito importante aprender com nossas experiências, sejam acertos ou erros, para nos fortalecermos para continuar buscando novas metas.

Aprendizado não é a palavra-chave em tudo isso? Finalizo com a frase de Ylana Muller, professora do IBMEC:
 "Trabalhe para viver. A energia do trabalho deve ser para transformar e realizar".


Para Saber Mais