Estudar código-fonte pode, sem dúvida, mudar a trajetória da sua carreira de desenvolvimento. Mesmo olhando abaixo da superfície, apenas um nível pode diferenciá-lo da maioria dos desenvolvedores comuns.
É o primeiro passo para a maestria!
Aqui vai uma história pessoal: no meu trabalho atual em uma startup de IA/ML, a equipe não conseguia descobrir como obter dados do Neo4j do servidor para o frontend para uma visualização, e eles tinham uma apresentação em 12 horas. Fui contratado como freelancer, e dava para ver claramente o pânico. O problema era que os dados retornados pelo Neo4j não estavam no formato correto esperado pela ferramenta de visualização, neo4jd3 .
Imagine isso: Neo4jd3 espera um triângulo, e Neo4j retorna um quadrado. Isso é uma incompatibilidade incompatível bem ali!
Podemos fazer ciência de dados de grafos com JavaScript e Neo4j no Mastered em breve! Esta imagem é nostálgica.
Havia apenas duas opções: refazer todo o backend do Neo4j ou estudar o código-fonte do Neo4jd3, descobrir o formato esperado e então criar um adaptador para transformar o quadrado em um triângulo.
neo4jd3 <- adapter <- Neo4j
Meu cérebro começou a ler o código-fonte e criei um adaptador: neo4jd3-ts .
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
O adaptador é NeoDatatoChartData
, e todo o resto é história. Levei essa lição a sério, e toda chance que tenho, vou um nível mais baixo em cada ferramenta que uso. Tornou-se tão predominante que às vezes nem leio a documentação.
Essa abordagem mudou minha carreira tremendamente. Tudo o que faço parece mágica. Em poucos meses, eu estava liderando migrações e projetos críticos de servidores, tudo porque dei um passo em direção à fonte.
É disso que se trata esta série: não se contentar com a API, mas ir além, aprender a recriar essas ferramentas. Sair da média neste mundo de hype de IA é o que torna um desenvolvedor valioso além da média!
Meu plano com esta série é estudar bibliotecas e ferramentas populares de JavaScript, descobrindo juntos como elas funcionam e quais padrões podemos aprender com elas, uma ferramenta de cada vez.
Como sou basicamente um engenheiro de backend (full stack, sim, mas cuidando do backend 90% do tempo), não há ferramenta melhor para começar do que Express.js.
Minha suposição é que você tem experiência em programação e uma boa compreensão dos fundamentos da programação! Você pode ser classificado como um iniciante avançado.
Vai ser muito difícil e tedioso tentar aprender/ensinar código-fonte enquanto ensina os fundamentos. Você pode participar da série, mas espere que seja difícil. Não posso cobrir tudo, mas tentarei o máximo que puder.
Este artigo é anterior ao Express por um motivo: decidi abordar uma biblioteca muito pequena da qual o Express depende, merge-descriptors , que, enquanto escrevo isto, tem 27.181.495 downloads e apenas 26 linhas de código.
Isso nos dará a oportunidade de estabelecer uma estrutura e me permitirá apresentar fundamentos de objetos que são cruciais na construção de módulos JavaScript.
Antes de prosseguirmos, certifique-se de que você tem o código-fonte do Express e os merge-descriptors no seu sistema. Dessa forma, você pode abri-lo em um IDE e eu posso orientá-lo com números de linha sobre onde estamos procurando.
Express é uma biblioteca robusta. Cobriremos o máximo que pudermos em alguns artigos antes de passarmos para outra ferramenta.
Abra o código-fonte do Express no seu IDE, de preferência com números de linha, navegue até a pasta lib
e abra o arquivo express.js
, o arquivo de entrada.
Na linha 17, aqui está nossa primeira biblioteca:
var mixin = require('merge-descriptors');
O uso está nas linhas 42 e 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Antes mesmo de explorarmos o que está acontecendo aqui, precisamos dar um passo para trás e falar sobre objetos em JavaScript, além da estrutura de dados. Discutiremos composição, herança, protótipos e mixins — o título deste artigo.
Feche o código-fonte do Express e crie uma nova pasta em algum lugar para acompanhar enquanto aprendemos esses fundamentos cruciais dos objetos.
Um objeto é um encapsulamento de dados e comportamento, no cerne da Programação Orientada a Objetos (POO) . Fato curioso: quase tudo em JavaScript é um objeto.
const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };
Tudo entre as chaves de abertura e fechamento no objeto person
é chamado de propriedades próprias do objeto . Isso é importante.
Propriedades próprias são aquelas diretamente no objeto. name
, age
e grow
são propriedades próprias da person
.
Isso é importante porque cada objeto JavaScript tem uma propriedade prototype
. Vamos codificar o objeto acima em um blueprint de função para nos permitir criar objetos person
dinamicamente.
function createNewPerson(name, age){ this.name = name; this.age = age; } createNewPerson.prototype.print = function(){ console.log(`${this.name} is ${this.age}`); }; const john = new createNewPerson("John", 32);
O protótipo é como objetos JavaScript herdam propriedades e métodos de outros objetos. A diferença entre Own Properties
e Prototype
é quando acessam uma propriedade em um objeto:
john.name; // access
O JavaScript primeiro procurará em Own Properties
, pois elas têm alta precedência. Se não encontrar a propriedade, ele procura no objeto prototype
do próprio objeto recursivamente até encontrar null e lançar um erro.
Um objeto protótipo pode herdar de outro objeto por meio de seu próprio protótipo. Isso é chamado de cadeia de protótipos.
console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype
No entanto, print
funciona em john
:
john.print(); // "John is 32"
É por isso que JavaScript é definido como uma linguagem baseada em protótipos. Podemos fazer mais com protótipos do que apenas adicionar propriedades e métodos, como herança.
O "hello world" da herança é o objeto mamífero. Vamos recriá-lo com JavaScript.
// our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };
Em JavaScript, há uma função estática dentro do objeto Object
:
Object.create();
Ele cria um objeto de forma semelhante a {}
e new functionBlueprint
, mas a diferença é que create
pode receber um protótipo como parâmetro para herdar.
// we use a cat blueprint function here (implemented below) Cat.prototype = Object.create(Mammal.prototype); // correction after we inherited all the properties Cat.prototype.constructor = Cat;
Agora Cat
terá o método breathe
encontrado em Mammal
, mas o importante a saber é que Cat
está apontando para Mammal
como seu protótipo.
Mammal Blueprint : Primeiro definimos a função Mammal
e adicionamos um método breathe
ao seu protótipo.
Herança Cat : Criamos a função Cat
e definimos Cat.prototype
como Object.create(Mammal.prototype)
. Isso faz com que o protótipo Cat
herde de Mammal
, mas muda o ponteiro constructor
para Mammal
.
Corrigindo Constructor : Corrigimos o Cat.prototype.constructor
para apontar de volta para Cat
, garantindo que o objeto Cat
retenha sua identidade enquanto herda métodos de Mammal
. Por fim, adicionamos um método meow
a Cat
.
Essa abordagem permite que o objeto Cat
acesse métodos tanto de Mammal
(como breathe
) quanto de seu próprio protótipo (como meow
).
Precisamos corrigir isso. Vamos criar o exemplo completo:
function Cat(name, breed) { this.name = name; this.breed = breed; } Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal // correction after we inherited all the properties Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there Cat.prototype.meow = function() { console.log(`${this.name} is meowing.`); };
Para entender o Cat.prototype.constructor = Cat
, você precisa saber sobre ponteiros. Quando herdamos de Mammal
com Object.create
, ele muda o ponteiro do nosso protótipo Cat
para Mammal
, o que está errado. Ainda queremos que nosso Cat
seja seu próprio indivíduo, apesar de ter o pai Mammal
.
É por isso que temos que corrigi-lo.
Neste exemplo, Cat
herda de Mammal
usando a cadeia de protótipos. O objeto Cat
pode acessar os métodos breathe
e meow
.
const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.
Podemos criar um cão também herdando do mamífero:
function Dog(name, breed) { this.name = name; this.breed = breed; } Dog.prototype = Object.create(Mammal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log(`${this.name} is barking.`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.breathe(); // Buddy is breathing. myDog.bark(); // Buddy is barking.
Criamos herança clássica básica, mas por que isso é importante? Pensei que estávamos cobrindo código-fonte!
Sim, verdade, mas protótipos são o cerne da construção de módulos eficientes e flexíveis, além da herança. Até mesmo módulos simples e bem escritos são montados com objetos protótipos. Estamos apenas estabelecendo o básico.
A alternativa à herança é a composição de objetos, que pega dois ou mais objetos e os mescla para formar um "super" objeto.
Mixins permitem que objetos tomem emprestado métodos de outros objetos sem usar herança. Eles são úteis para compartilhar comportamento entre objetos não relacionados.
É isso que nossa primeira exploração faz: a biblioteca merge-descriptors
que prometemos cobrir primeiro.
Já vimos onde e como ele é usado no Express. Agora o conhecemos para composição de objetos.
Na linha 17, aqui está nossa primeira biblioteca:
var mixin = require('merge-descriptors');
O uso está nas linhas 42 e 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Com o que sabemos, já podemos deduzir que o mixin
está compondo EventEmitter.prototype
e proto
em um objeto chamado app
.
Chegaremos ao app
quando começarmos a falar sobre o Express.
Este é o código-fonte completo para merge-descriptors
:
'use strict'; function mergeDescriptors(destination, source, overwrite = true) { if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); } for (const name of Object.getOwnPropertyNames(source)) { if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; } // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor); } return destination; } module.exports = mergeDescriptors;
Desde o início, observe sempre como a função é usada e os parâmetros que ela recebe:
// definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
App
é nosso destino. Sabemos que mixin
significa composição de objetos. A grosso modo, o que este pacote está fazendo é compor o objeto de origem no objeto de destino, com uma opção para sobrescrever.
Substituir, por suposição, é se app
(destino) tiver uma propriedade exata que a fonte tem, em substituição true
, caso contrário, deixe essa propriedade inalterada e pule.
Sabemos que objetos não podem ter a mesma propriedade duas vezes. Em pares chave-valor (objetos), chaves devem ser únicas.
Com o Express, a substituição é false
.
A seguir estão algumas dicas básicas de limpeza: sempre lide com erros esperados:
if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }
É aqui que fica interessante: linha 12.
for (const name of Object.getOwnPropertyNames(source)) {
Pelo exposto acima, sabemos o que significa OwnProperty
, então getOwnPropertyNames
significa claramente obter as chaves das próprias propriedades.
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
Ele retorna as chaves como uma matriz, e estamos fazendo um loop sobre essas chaves na seguinte instância:
for (const name of Object.getOwnPropertyNames(source)) {
O seguinte verifica se o destino e a origem têm a mesma chave que estamos usando no momento:
if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }
Se overwrite for falso, pule essa propriedade; não sobrescreva. É isso que continue
faz — ele impulsiona o loop para a próxima iteração e não executa o código abaixo, que é o seguinte código:
// Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);
Já sabemos o que getOwnProperty
significa. A nova palavra é descriptor
. Vamos testar essa função em nosso próprio objeto person
:
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyDescriptor(person, "grow"); // { // value: [Function: grow], // writable: true, // enumerable: true, // configurable: true // }
Ele retorna nossa função grow
como o valor, e a próxima linha é autoexplicativa:
Object.defineProperty(destination, name, descriptor);
Ele está pegando nosso descritor da fonte e escrevendo-o no destino. Ele está copiando as propriedades próprias da fonte para nosso objeto de destino como suas próprias propriedades.
Vamos fazer um exemplo em nosso objeto person
:
const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);
Agora person
deve ter a propriedade isAlien
definida.
Em resumo, este módulo altamente baixado copia suas próprias propriedades de um objeto de origem para um destino com uma opção de substituição.
Cobrimos com sucesso nosso primeiro módulo nesta camada de código-fonte , com mais coisas interessantes por vir.
Esta foi uma introdução. Começamos cobrindo os fundamentos necessários para entender o módulo e, como subproduto, entender os padrões na maioria dos módulos, que são composição e herança de objetos. Por fim, navegamos no módulo merge-descriptors
.
Esse padrão prevalecerá na maioria dos artigos. Se eu sentir que há fundamentos necessários para cobrir, nós os examinaremos na primeira seção e então cobriremos o código-fonte.
Felizmente, merge-descriptors
é usado no Express, que é nosso foco como nosso estudo de código-fonte inicial. Então espere mais artigos sobre código-fonte do Express.js até que sintamos que tivemos uma execução boa o suficiente do Express, então mude para outro módulo ou ferramenta como o Node.js.
O que você pode fazer enquanto isso como um desafio é navegar até o arquivo de teste em merge descriptors, ler o arquivo inteiro. Ler a fonte por conta própria é importante, tente descobrir o que ele faz e está testando, então quebre-o, sim, e conserte-o novamente ou adicione mais testes!
Se você estiver interessado em conteúdo mais exclusivo, prático e longo para elevar suas habilidades de programação, você pode encontrar mais no Ko-fi .