Minha experiência com Ruby on Rails a partir do Laravel

Tempo de Leitura: 13 minutos

Em 2018 tive a oportunidade de trabalhar com o framework Ruby on Rails após cerca de 5 anos de PHP e 3 de Laravel. Desejo nesse texto abordar minha experiência com o PHP, como passei a usar o Laravel e por fim as minhas impressões do framework Rails.

De início, Freestyle PHP

Minha primeira experiência com programação profissional foi com PHP “freestyle”. Este estilo de programação é bem conhecido pois vários sites e pequenos sistemas são feitos nessa abordagem. Código duplicado, difícil manutenção e complexidade ciclomática exponencial são problemas muito comuns em sistemas desenvolvidos nessa abordagem. O site brasileiro scriptsbrasil, que contém o código fonte de vários programas desenvolvidos em cerca de 2002 até 2013, exemplifica tais práticas ruins.

Pela minha intuição a popularidade deste estilo de programação se deve ao PHP ter uma curva de aprendizado relativamente pequena pois:

  • Funciona bem em Windows com WAMP/XAMPP, o sistema operacional mais utilizado por usuários finais;
  • Por padrão possui variáveis e funções HTTP como $_GET, $_POST, $_SESSION, cookie(), header(), htmlspecialchars(), htmlentities(), que dispensam o uso de uma biblioteca de roteamento HTTP como o slim framework;
  • Renderiza HTML simples por padrão com as tags <?php ?> e a função includes, apesar da linguagem sozinha não suportar funções específicas de templates como “extends“;
  • Possui várias funções já embutidas na biblioteca padrão ou por extensões geralmente ativas. Por exemplo, para decodificar um JSON basta a função json_decode(), ler o conteúdo de um arquivo – file_get_contents(), validar se uma string é email ou url – filter_var().
  • Ser comum o deploy via FTP em hospedagem compartilhada. Apesar de todas as desvantagens dessa infraestrutura como escalabilidade, segurança e consistência do código, realizar o deploy com apenas o arrastar e soltar dos arquivos no Filezilla em um servidor já configurado pelo cPanel é talvez o método mais simples de deploy de uma aplicação.

Dado às funcionalidades embutidas, é sedutor para o iniciante criar uma aplicação PHP sem frameworks ou bibliotecas, usando arquivos como “autenticacao.php, pesquisa.php, criar.php, editar_post.php, criar_post.php” para o roteamento. Essa aplicação pode funcionar muito bem e resolver um problema imediato, mas gera um alto custo de manutenção à medida que a aplicação e seus requisitos evoluem.

Primeira experiência com Rails

Trabalhando em sites e sistemas desenvolvidos na abordagem acima, em 2014 na faculdade tive a oportunidade de participar do desenvolvimento de um pequeno sistema em Rails. De imediato tive uma impressão muito positiva especialmente do Active Record – o ORM do framework. Se antes no PHP estava acostumado a fazer todas as querys SQL manualmente com o PDO ou mesmo mysqli:

$statement = $pdo->prepare('SELECT * FROM posts WHERE id=:id');
$statement->setFetchMode(PDO::FETCH_NUM);
$statement->execute([':id' => $id])->fetchObject('User');

No Rails isto era (e é) possível apenas alguns caracteres:

Post.find(1)

No caso das querys de insert/update, a diferença é ainda maior, e com o ORM se abstrai a decisão de qual dessas operações efetuar:

$statement = $pdo->prepare('INSERT INTO posts (title, description, author) values (:title, :description, :author)');
$statement->execute([':title' => $title, ':description' => $description, ':author' => $author]);
Post.create({'title': title, 'description': description, 'author': author})

Com isto se evitava muita repetição na maioria das querys simples (CRUDs) e duplicação do código, além ser uma sintaxe muito mais expressiva.

Além do ORM, em geral também agradou-me no Rails a ideia de convenção sobre configuração, por exemplo, do framework automaticamente encontrar o nome de uma tabela de acordo com a classe, do padrão REST para as rotas HTTP, etc.

Busca por algo similar em PHP

Após esta experiência logo passei a buscar por frameworks e ORMs que pudessem ser incorporados nos sistemas que trabalhava.

Testei inicialmente o Symfony 3, junto com o ORM Doctrine, dos quais tive impressões mistas. Ter um framework que organizasse as Rotas e o banco de dados já era um avanço, porém ele não possuía a mesma facilidade do Rails: muitas coisas eram feitas com annotations (lembro do roteamento, mas sei que haviam outros usos), que não me agradavam pois as anotações, especialmente no PHP, soam diferente do código e inserir lógica nelas parecia gerar inconvenientes para depuração. Este artigo de Tom Butler lista bem tais desvantagens.

O ORM Doctrine usado com o Symfony, por sua vez, talvez como indique o nome, exigia a especificação de todas as colunas do banco de dados, também em annotations, o que me parecia não ser produtivo ao menos para uma prototipação rápida. A separação do código pelos burocráticos “bundles” (padrão desencorajado na recente versão 4 do Symfony) e a grande quantidade de configurações fizeram-me desistir de usar tal framework e seus componentes.

Também testei o ORM Redbean, que se diferencia por magicamente criar e modificar o schema de um banco de dados relacional. Sua simplicidade me atraiu porém a maneira que a biblioteca manipula o schema e a sintaxe dos models me desencorajaram de usar essa biblioteca.

Por fim, o framework Laravel, que já estava se destacando na época (~2014), chamou a minha atenção por ser fortemente inspirado no Ruby on Rails especialmente na abordagem de convenção sobre configuração. Acabei por investir meu tempo e adotar algumas bibliotecas do Laravel como o ORM Eloquent e validações nos projetos que já trabalhava, e em paralelo criando pequenos side-projects nesse framework.

Felizmente muitos aspectos no ecossistema PHP progrediram nos anos que eu trabalhei com essa linguagem de programação. Foi produzido o excelente guia PHP do jeito certo, o gerenciador de pacotes composer se consolidou e várias bibliotecas adotaram tal sistema. As PSRs convencionaram várias práticas recomendadas pela comunidade e além disso o PHP 7 surgiu no final de 2015 com significantes avanços de desempenho, da tipagem e no tratamento de erros.

Rails para valer

Após trabalhar aproximadamente dois anos com Laravel, em 2018 tive a oportunidade de trabalhar com Ruby on Rails a maior parte do ano e também em participar da RubyConf 2018.

De início, ao ver o código de aplicações já existentes a impressão da estrutura geral soou bem familiar com o Laravel. O uso do arquivo de rotas, os controllers Restful, os métodos do ORM similares (find, update, create). Tudo faz sentido considerando que Taylor Otwell se inspirou no Rails para fazer tal framework.

Mas como todo aprendizado, logo surgem as dificuldades e finalmente começa-se a se ter uma ideia das vantagens e desvantagens da linguagem e do framework.

Ruby on Rails – o que gostei

Sintaxe Enxuta

Por padrão muitas coisas podem ser feitas com pouco código. Uma delas por exemplo é fazer cache de um atributo na classe:

def first_post
  self.post ||= Post.first
end

O que o operador “||=” significa? aparentemente temos um “OR” misturado com uma atribuição. Se o código fosse apenas “self.post || Post.first” significaria: retorne a propriedade do object post OU o primeiro post do banco de dados. Mas com a adição do “=” ele acaba por fazer uma atribuição ao mesmo tempo da condicional: retorne a propriedade do object post OU o primeiro post, atribuindo-o àquela propriedade.

Para exemplificar, o mesmo código em php requer uma lógica mais explícita:

function first_post() {
  if (!$this->post) {
    $this->post = Post::first();
  }

  return $this->post;
}

Outra coisa muito interessante é que todos os blocos de código retornam valores. Por, exemplo, a seguinte atribuição é possível:

identification = 
  if type == "PF" 
    pessoa.fisica 
  else 
    pessoa.juridica

O Rails possui uma biblioteca de manipulação de data/hora que funciona de maneira bem expressiva, como “1.hour.ago”. Porém, não tive a oportunidade de usá-la.

Obviamente que dominar ou mesmo compreender todos os operadores Ruby requer uma considerável curva de aprendizado. Leonard Teo exemplifica nesse excelente artigo em 2012 no comando que simplesmente gera uma string de 8 caracteres aleatórios:

(0...8).map{65.+(rand(25)).chr}.join

Percebemos que esta é a solução aceita na pergunta https://stackoverflow.com/a/88341. Soaria melhor que cada passo fosse associado a uma variável para uma melhor compreensão do código. Em alguns momentos tive que lidar com códigos similares.

Validações

Além das validações como obrigatório, tamanho de string, quantidade de itens do array, valores interos. Criar validações específicas é muito simples com o “validate :validator_function”. Também notei alguns recursos interessantes como a validação condicional com if: -> (). Em geral as validações funcionaram muito bem comigo. Exemplo:

validates :cnpj, presence: true,
                 uniqueness: true,
                 length: { is: 14 },
                 cnpj: true
validates :number, length: { is: 11 }, 
                   if: ->() { type == 'mobile' }

validate :contains_mobile_phone?

def contains_mobile_phone?
  return true if phones&.map { |phone| phone.type == 'mobile' }&.any?

  errors.add(:phones, :contains_no_mobile_phone)
  false
end

Migrations

De início observei que algumas operações podem ser feitas usando o método “change” que automaticamente insere o “down” quando possível.

class CreateTasksTable < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|
      t.string :title, limit: 15
      t.references :project, foreign_key: true
      t.timestamps
    end
  end
end

Outro recurso interessante é este “t.references”, que automaticamente cria a coluna “project_id” na tabela task. No Laravel, é necessário especificar tal coluna e depois adicionar a foreign key:

$table->unsignedInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');

RSpec e Simplecov

Para quem estava acostumado com o PHPUnit, a oportunidade de organizar hierarquicamente os testes no RSpec através de contexts deixa os resultados mais legíveis. Por exemplo, no CMS October, temos os seguintes testes num arquivo selecionado aleatoriamente: “testLoad, testLoadFromSubdirectory, testFillFillable, testFillNotFillable, testFillInvalidFileNameSymbol…”. No rspec é possível agrupá-los em “contexts” como “load” e “fill”, e cada comportamento um teste com “it” (is loaded from subdirectory, is fillable, is not fillable, etc…).

Já sobre code coverage, o Simplecov analisa a cobertura de código muito mais rapidamente que PHPUnit + XDebug. Muito provavelmente aqui é mais uma deficiência dessas ferramentas do PHP do que uma vantagem do simplecov. Nesse caso pode-se usar a combinação PHPUnit + PHPdbg para resolver o problema de desempenho, porém é uma opção mais instável e difícil de configurar.

FactoryBot

Apesar do Laravel ter uma boa ferramenta de “factories”, o FactoryBot consegue manipular bem os relacionamentos dos objetos sem precisar persisti-los. Por exemplo:

factory :task
  project
end

factory :post
  comments { [build(:comment)] }
end

task = factory(:task)
task.save

Assim, ao ser criada uma task através do FactoryBot, automaticamente um project é criado. Assim que chamamos o save() da task, o project também é persistido, e o mesmo vale para o post e seus comentários – ao salvar o post, um comentário de exemplo é criado. Outra vantagem dessa ferramenta é que caso não seja necessário persistir o model nos testes, os relacionamentos continuam funcionando. Em models com muitos relacionamentos isso pode fazer uma grande diferença nos testes em termos de menos repetições.

No Laravel, este tipo de relacionamento só é possível persistindo os models, estando a função create presente em cada relacionamento conforme explicado por Thibaud:

$factory->define(App\Phone::class, function (Faker\Generator $faker) {
    return [
        'user_id' => function() {
            return factory(App\User::class)->create()->id;
        },
        'phone' => $faker->phoneNumber,
    ];
});

Console PRY

Sendo este para mim mais um aprendizado para mim pois não conhecia o psy/psysh ou qualquer outro REPLs para depuração, a ferramenta do Ruby demonstrou ser superior com funcionalidades como hot-reload, navegação e outras demonstradas por Fabio Perrella.

A única desvantagem que encontrei nele é que o autocomplete várias vezes mostrou milhares de opções que não faziam sentido naquele contexto.

Ruby on Rails – O que confunde ou dificulta

Várias características da linguagem ou do framework levam consigo suas vantagens, mas possuem uma curva de aprendizado alta ou algum inconveniente que pode afastar novatos.

Símbolo X strings

Apesar de ser relativamente simples entender o que é um símbolo e seu propósito – como explicado nesse artigo do Akita, não é muito claro quando este deve ser usado numa biblioteca/framework. Exemplos: o nome da tabela deve ser declarado como string no model? Como que os parâmetros da requisição HTTP são acessados? Uma biblioteca que faça parser de um JSON, retorna os itens indexados por strings ou símbolos? Seguem tais variações:

class Country < ActiveRecord::Base
    self.table_name = "countries" # "countries" 
end

class RenameCountryTable < ActiveRecord::Migration
   def self.up
     rename_table :countries, :countries_renamed # :countries
   end
end 

class ClientsController < ApplicationController
   def index
     if params[:status] == "activated" # :status
       @clients = Client.activated
     else
       @clients = Client.inactivated
     end
  end
end 

hash = JSON.parse('{"key":"value"}') 
hash['key']  # 'key'

Autoloading bizarro

Durante o desenvolvimento tive vários problemas de autoloading que exigiam reinício do servidor Rails ou do servidor Spring. O mais recente foi o Spring não recarregando as variáveis de ambiente após alteração do .env ou mesmo ao serem passados como parâmetro do console, como em “MYSQL_DB_PASSWORD=outra_senha rails c”.

Também tive vários problemas de resolução de namespace os quais requeriam o uso do :: antes da classe, o que equivaleria ao primeiro \ do namespace em PHP (exemplo ::Vehicle::Engine em vez de Vehicle::Engine). Nesses casos não encontrei a causa exata do problema, mas aparentemente eram conflitos entre módulos e classes.

No PHP, o autoloading é regido pela PSR4 e possui uma regra simples: coloque seu arquivo numa mesma estrutura de pastas do namespace declarado. Nesta abordagem só tive problemas com o case do namespace errado, mas geralmente não precisei reiniciar o servidor web, até porque em cada requisição todas as classes são carregadas do sistema de arquivos novamente sob demanda.

Ao estudar o comportamento do Rails, compreendi o contexto desses problemas acontecerem, embora não as causas específicas: fora do ecossistema PHP é comum termos um application server, e é o caso do Ruby on Rails: basicamente seu papel é carregar todo o código da aplicação em memória e interpretar diretamente as requisições http, tornando os servidores web (nginx, apache, etc) opcionais no ambiente de desenvolvimento. E por estes applications servers carregarem todo o código em memória, precisam de um mecanismo de recarregamento das classes, e nesse momento ocorreram os problemas citados aqui.

Validações nos Models

A ideia de self-validating models soa muito interessante no sentido de que não importa de que fonte o dado veio, se por uma rota HTTP, por um Command ou uma Fila, sempre ao salvar realizamos a validação do mesmo para garantir sua integridade. Podemos validar até mesmo após buscar os dados do banco de dados, para aplicar novas validações por exemplo.

No Laravel, os Validators podem ser construídos em qualquer momento, mas é comum que estejam associados a uma request que os deixa restritos apenas à chamadas HTTP. Há bibliotecas que acoplam os validadores aos models da mesma forma como Rails, é o caso da ardent, mas este não parece ser um uso muito comum.

Aplicar validações apenas nos models traz alguns problemas periféricos. Um que encontrei é que ao se declarar um campo do tipo enum, não é possível validar a inserção de um valor inválido no enumerador pois uma exceção é disparada, conforme descrito nessa questão. Por exemplo, no seguinte código:

class Project < ActiveRecord::Base
  validates :color, inclusion: { in: colors }

  enum color: { blue: 0, green: 1, red: 2 }
end

project.color = :orange

É disparada uma exceção que exige um tratamento para que não seja retornado erro 500 ao client, que normalmente esperaria um erro 422 da validação “inclusion”. No caso tivemos que realizar essa validação fora do model antes de realizar as atribuições.

Outro problema no qual não recordo todos os detalhes é que pode ocorrer de que para validar um relacionamento é necessário associá-lo e persisti-lo, sendo então necessário rollback de tal persistência caso a validação final falhe.

Já outra dificuldade que encontrei apenas recentemente sua solução é a aplicação de validações em diversos contextos. Há um recurso não muito bem documentado que é a validação condicional, explicada por Justin Weiss. Sem ela é necessário ou adicionar flags para cada validação ou removê-las dos models.

Sintaxe singular

O Ruby permite em vários momentos utilizar várias sintaxes para o mesmo resultado. Uma que gostei foi o if após a declaração:

return post if post.public # em vez de: if post.public return post
disable post # em vez de: disable(post),

Elas trazem consigo uma curva de aprendizado que podem afastar novatos (conforme já explicado em sintaxe enxuta) pois são bem diferentes da maioria das linguagens de programação. O iniciante, como foi o meu caso, pode começar usando uma sintaxe mais familiar, por exemplo, usando sempre o return e parênteses nas funções, mas o padrão imposto pelo rubocop converterá para esta sintaxes mais singulares.

Alguns exemplos são:

  • return post: return omitido.
  • return post if post.public: código then antes do if.
  • publish post: omissão de parêntesis.
  • persisted?: uso de interrogação em vez de “is_persisted”.
  • title.downcase!: uso de exclamação para indicar modificação do próprio objeto.
  • class Account < ApplicationRecord
    • has_many :phones: método invocado diretamente na classe para definição dos relacionamentos.
  • .permit(:category, attach: %i[id base name]): omissão de chaves para indicar o único objeto passado, o que confunde com dois parâmetros de chamada

Ruby on Rails – O que desagrada

Alguns aspectos do framework ou da linguagem (alguns objetivos outros mais subjetivos) decepcionaram-me de alguma forma pois poderiam ser melhor elaborados.

Query Builder para consultas complexas

Quando tive a necessidade de criar consultas não triviais, percebi como os métodos do query builder do Active Record são limitados se comparados com o query Builder do Laravel.

Por exemplo, se queremos trazer os posts que possuem data de publicação posterior a 2018 ou aqueles que possuem mais de 100 comentários, podemos estruturar a consulta desta forma:

Post::query()
     ->where('publish_date', '>', '2018-01-01')
     ->orWhereHas('comments', '>', '100')

Como no Rails não há um método equivalente ao whereHas do Eloquent, o qual automaticamente adiciona uma subquery para obter a quantidade de registros no relacionamento, temos que manualmente fazer o relacionamento com join e adicionar o group para poder usar o count() e evitar que retornemos registros de post duplicados pelo inner join:

Post.joins(:comments).group('post.id')
    .where('publish_date > ?', '2018-01-01')  
    .or(
        Post.joins(:comments).group('post.id')
            .having('count(comments) > 100')
    )

Um outro problema aqui é a sintaxe do or. Temos que repetir a query com a mesma assinatura de joins e group by para não disparar o erro “Relation passed to #or must be structurally compatible”. Caso aumente a quantidade de relacionamentos tal sintaxe se torna muito repetitiva e pouco expressiva.

Outra vantagem para o query builder do Laravel é que ele utiliza extensivamente funções anônimas para aninhar querys, o que o torna muito flexível. Podemos desta adicionar condições via flags, misturar ORs com ANDs de maneira legível, conforme demonstrado nesses exemplos:

Já no Rails é possível fazer algumas dessas querys complexas usando SQL diretamente ou os métodos do Arel conforme explicado por Alex Gorkunov, mas a sintaxe continua muito verbosa. É possível usar também o query builder sequel, mas ele exige a reescrita dos models e por isto não cheguei a estudá-lo.

Documentação pesada

Em vários momentos a documentação oficial Rails Guides soou-me excessivamente extensa e em alguns momentos pouco didática. Por exemplo, há uma página “configuring” de 39 folhas impressas que engloba a documentação de vários componentes. Estava procurando especificamente pela parte de como criar uma configuração “customizada” que está numa das últimas seções da página, a qual foi difícil localizar.

Por outro lado, a documentação do Laravel em vários momentos deixa de explicar ou exemplificar comportamentos periféricos de alguns componentes relevantes. Nesse mesmo caso, há uma explicação simples de como acessar configurações, mas não foi explicado como criá-las.

Um pouco mais difícil de criar configurações

Seguindo o tópico anterior, no Laravel basta criar um arquivo direto em config ou numa pasta aninhada retornando um array chaveado e acessá-lo através do método config(‘arquivo.chave’) ou config(‘pasta.pasta.arquivo.chave.chave’).

Já no Rails é necessário modificar diretamente o objeto global Rails.configuration. Se intentamos criar um arquivo separado para estas configurações, podemos usar um .yml mas temos que modificar também o config/application.rb com o “config_for”.

Nesse caso em específico considero que o Laravel provê uma API mais simples e abstrata para essa funcionalidade.

Falta de container para injeção de dependência

Uma ferramenta que senti falta para o Ruby, e que me ajudou bastante no Laravel é o Container de Injeção de Dependências: Basicamente uma classe com dependências injetadas tende a ter um design melhor no sentido de suas responsabilidades e fronteiras estarem melhor definidas e a ser mais testável pois podemos substituir suas dependências por instâncias dublês no código de teste.

Já o container de dependências, como o service container do Laravel, bem exemplificado no Laracasts, tem por função instanciar todas as dependências que uma classe necessita, facilitando o manuseio das mesmas. Seu uso não é essencial para a implementação de injeção de dependência, mas sem ele as opções que encontrei para a instância de um objeto com todas suas dependências foi ou espalhar o código de instância de classes nas classes consumidoras (o que claramente viola a injeção de dependência nestas) ou criar classes factories para lidar com a criação de instâncias, que me pareceu trabalhoso e repetitivo.

No Ruby os containers de dependência são pouco utilizados, talvez pela falta de uma sintaxe que os viabilizem como tipagem estática, type hinting ou annotations. Em geral pela linguagem ser bastante dinâmica, possibilitando até a substituição de objetos através da sobrescrita do método .new da classe por exemplo, é possível simular alguns dos comportamentos de injeção de dependências em testes.

Exemplificando o service container do Laravel, testamos numa classe “Car” com a dependência “Engine” se esta recebe a mensagem “applyForce” ao acelerarmos o carro:

class Car {
  function __construct(Engine $engine, Steering $steering, Brake $brake) 
  {
    $this->engine = $engine; // or just resolve(Engine::class);
    $this->steering = $steering;
    $this->brake = $brake;
  }

  function accelerate() {
    $this->engine->applyForce();
  }
  
  function turn(Direction $direction) {
    $this->steering->turn($direction);
  }

  function brake() {
    $this->brake->brake();
  }
}

class CarTest extends TestCase 
{
  function testCarMotorReceivesForceWhenAccelerate() 
  {
    app()->bind(Engine::class, function() {
      $engine = $this->createMock(Engine::class);

      $engine
        ->expects($this->once())
        ->method('applyForce')
        ->willReturn(true);

      return $engine;
    });

    resolve(Car::class)->accelerate();
  }
}

Neste exemplo, sem o container de dependências teríamos que ter uma classe “factory” para instanciar todas as partes necessárias para o carro, tanto para o código da aplicação tanto para o código de testes. Com o container, podemos substituir apenas o motor que é a parte interessante neste teste. Note que com resolve(Car::class)temos todas as dependências (e eventualmente dependências de dependências) automaticamente instanciadas.

Por fim, com o container de dependências do Laravel podemos aplicar singletons elegantemente com o “instance”, sem precisar necessariamente se preocupar em desenvolver tal padrão na classe desejada.

Orientação à Objetos Incompleta

A ausência de uma orientação a objetos “completa” não é exclusividade do Ruby pois outras linguagens de amplo uso como Python e Node também são limitadas nesse contexto. Em alguns momentos senti falta de classes abstratas, métodos abstratos e interfaces, sendo necessário simular tais comportamentos disparando exceções:

def buy_car
  raise "Method not implemented"
end

Menos helpers “polêmicos”

Um helper interessante no Laravel é o request() – com ele é possível acessar o objeto da requisição com todos seus parâmetros em qualquer parte do sistema. No Rails, o objeto da request é acessível somente no controller.

Por um lado, a disponibilidade desse helper, que utiliza o service container, dá mais flexibilidade para o desenvolvedor aplicá-lo em qualquer parte do sistema de acordo com sua solução. Por outro lado pode incentivá-lo a adicionar dependências ao contexto http indevidamente (em models por exemplo).

Conclusão

Em geral continuo optando pelo PHP e Laravel nos meus side-projects – o mais recente é o Community RSS Directory. Essa escolha não é dada pela vantagem técnica do framework, porém muito mais pela minha preferência pessoal influenciada pelo maior tempo que dediquei nessa linguagem acompanhando sua evolução, e também por essas ferramentas estarem em evolução num ecossistema vivo: certamente não desejaria estar programando da mesma maneira como iniciei neste artigo.

Aprender uma linguagem e um framework novo foi muito positivo para mim em 2018 pois ajudou a identificar conhecimentos que precisava e que preciso adquirir e na maneira como compreendo a programação de software.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *