Alguns dias atrás fiz um post com um desafio extraído do livro The Pragmatic Programmer
. O desafio consistia em desenvolver um parser para uma pseudo-linguagem que descrevia como um programa do tipo turtle teria que realizar um desenho na tela:
Acontece que essa pseudo-linguagem era um meio de criar uma abstração que fosse mais próxima de um determinado domínio, de modo a facilitar o uso para o usuário. A essas lingugagens chamamos de Domain Languages.
Mas que raios é isso?
Normalmente, quando implementamos uma funcionalidade em um conjunto de procedimentos pré-determinados (ou escrevemos um programa), não o fazemos de forma que o programa seja autocontido em si mesmo, e nem fazemos a programação pela programação.
A programação é um meio para chegar a um fim. Normalmente a finalidade é um negócio ou problema que se quer resolver. A este problema, chamamos de "Domínio".
Quando nos referimos a domínio, este domínio possui todo seu jargão próprio, e um workflow que é próprio seu, e para a comunicação com os pares que compõe este domínio, por repetidas vezes usamos estes jargão e linguagens próprios da área.
Entender como se dá essa comunicação é chave para o entendimento do domínio.
Linguagens de domínio, então, são abstrações criadas para aproximar o nosso mundo, o da programação, com o mundo do domínio, para conseguirmos transmitir uma informação mais expressiva no nosso código, e reduzir cargas cognitivas.
Até mesmo dentro da programação há subdomínios
O melhor e mais popular exemplo que temos de uma linguagem de domínio é dado pelo React, através da notação JSX. JSX em si não é uma linguagem de programação, mas um syntax sugar, com o intuito original de criar elementos de forma mais fácil no react:
const element = React.createElement("div",
{ style: { display: "flex" } },
React.createElement("center", {}, "This is a centered text")
);
// |
// |
// |
// \ /
// v
const element = (
<div style={ { display: "flex" } }>
<center>This is a centered text</center>
</div>
)
Para este caso, o JSX é um domain language, direcionado para a área de frontend, o qual inclusive se desenvolveu todo um tooling em volta dessa ferramenta para facilitar e melhorar muito mais o Developer Experience (ou DX).
Como o JSX não é parte da sintaxe do Javascript em si, a mesma é classificada como uma "External Domain Language".
Linguagens de Domínio Internas e Externas
Quando uma linguagem de domínio é criada a partir de uma sintaxe que não segue as semânticas da linguagem de programação alvo, ou que precisa de um processo de parsing antes, é dito que a linguagem de domínio é externa.
Bons exemplos disso são arquivos de configuração do Spring (application.properties/yaml), arquivos de configuração do npm (package.json), JSX, arquivo de configuração do Angular (angular.json), etc.
Normalmente, linguagens de domínio externas trazem um custo maior de desenvolvimento, pois obviamente, além do desenvolvimento da linguagem em si, também acarreta no desenvolvimento do parser da linguagem. Porém são linguagens mais poderosas que não se limitam às semânticas da linguagem-alvo.
Quando uma linguagem de domínio é criada a partir da sintaxe da linguagem alvo e feita com o objetivo de ser usada dentro da linguagem alvo, é dito que a linguagem de domínio é interna.
Nesse caso, toda interface que qualquer biblioteca expõe para uso do programador é um exemplo de linguagem de domínio interna. Isto porque são abstrações que escondem seus detalhes de implementação e expõe funções e classes mais descritivas, com melhor expressividade.
Sendo assim, uma linguagem de domínio interna é muito mais fácil e natural de ser criada, porém, a mesma fica presa às semânticas da linguagem alvo.
Resolvendo o problema proposto
No caso do problema proposto no meu tweet original, este é um exemplo de linguagem de domínio externa, onde o parser em si pode ser implementado em qualquer linguagem.
Eu escolhi implementar o parser em Typescript (até porque, meu ambiente atual já está totalmente configurado para TS). Uma boa implementação dessa semântica é através de um Map de funções:
export const functionObject = {
P: (param?: number) => {
console.log("Select pen " + param);
},
D: () => {
console.log("pen down");
},
W: (param?: number) => {
console.log("draw west " + param + "cm");
},
N: (param?: number) => {
console.log("then north " + param);
},
E: (param?: number) => {
console.log("then east " + param);
},
S: (param?: number) => {
console.log("then back south " + param);
},
U: () => {
console.log("pen up");
},
};
Neste caso, cada letra corresponde a uma entrada dentro do Map. Como object literals no Javascript corresponde a um Map (bom, mais ou menos, né?), só configurei que cada letra fosse uma propriedade desse objeto com uma função atrelada a ele. Assim, consigo estender o rol de funções disponíveis mais facilmente.
Para parsear o input, simplesmente uso um pouco de mágica de manipulação de strings e arrays:
const language = `P 2 # select pen 2
D # pen down
W 2 # draw west 2cm
N 1 # then north 1
E 2 # then east 2
S 1 # then back south
U # pen up
`;
const isArgAFunctionInTable = (
arg: string
): arg is keyof typeof functionObject => {
return arg in functionObject;
};
language
.split("\n")
// remove comments
.map((command) =>
command
.substring(0, command.indexOf("#"))
.trim()
// then separate the function from parameter
.split(" ")
)
.forEach((commandArray) => {
const lengthOfArray = commandArray.length;
if (lengthOfArray < 1) return;
const functionLocator = commandArray[0];
if (!isArgAFunctionInTable(functionLocator)) return;
if (lengthOfArray === 2) {
functionObject[functionLocator](Number(commandArray[1]));
return;
}
functionObject[functionLocator]();
});
E se a linguagem de domínio fosse interna?
Se a linguagem for interna, a implementação é mais fácil.
Já que eu tenho as funções prontas, eu posso usar elas diretamente ou então desestruturá-las, dando a elas um novo nome:
type FunctionInTable = (parse?: number) => void;
type Keyof<T> = keyof T;
type KeyofFunctionObject = Keyof<typeof functionObject>;
const argIsKeyofTable = (arg: string): arg is KeyofFunctionObject => {
return arg in functionObject;
};
const functions = Object.keys(functionObject).reduce<
Record<`do${KeyofFunctionObject}`, FunctionInTable> | undefined
>((finalObj, key) => {
const intermediaryObj: Record<string, FunctionInTable> = finalObj
? finalObj
: {};
if (!argIsKeyofTable(key)) return;
intermediaryObj[`do${key}` as Keyof<typeof intermediaryObj>] =
functionObject[key];
return intermediaryObj;
}, undefined);
if (functions) {
const { doD, doE, doN, doP, doS, doU, doW } = functions;
}
Assim, eu poderia chamar doP
, doE
, etc e ainda contaria com a mesma flexibilidade de adicionar novas funções!
Top comments (0)