Existem princípios OO que são praticamente aplicáveis ​​ao Javascript?


79

Javascript é uma linguagem orientada a objeto baseada em protótipo, mas pode se tornar baseada em classe de várias maneiras, seja por:

  • Escrevendo as funções a serem usadas como classes sozinho
  • Use um sistema de classes bacana em uma estrutura (como mootools Class.Class )
  • Gere a partir do Coffeescript

No começo, eu costumava escrever código baseado em classe em Javascript e confiava bastante nele. Recentemente, no entanto, tenho usado estruturas Javascript e NodeJS , que se afastam dessa noção de classes e dependem mais da natureza dinâmica do código, como:

  • Programação assíncrona, usando e escrevendo código de escrita que usa retornos de chamada / eventos
  • Carregando módulos com o RequireJS (para que eles não vazem para o espaço de nome global)
  • Conceitos de programação funcional, como compreensão de lista (mapa, filtro, etc.)
  • Entre outras coisas

O que eu reuni até agora é que a maioria dos princípios e padrões de OO que li (como os padrões SOLID e GoF) foram escritos para linguagens de OO baseadas em classes, como Smalltalk e C ++. Mas existem alguns aplicáveis ​​a uma linguagem baseada em protótipo como o Javascript?

Existem princípios ou padrões que são apenas específicos para Javascript? Princípios para evitar o inferno de retorno de chamada , avaliação ruim ou qualquer outro antipadrão etc.

Respostas:


116

Depois de muitas edições, essa resposta se tornou um monstro de comprimento. Peço desculpas, com antecedência.

Antes de tudo, eval()nem sempre é ruim e pode trazer benefícios no desempenho quando usado na avaliação lenta, por exemplo. A avaliação preguiçosa é semelhante ao carregamento lento, mas você essencialmente armazena seu código em seqüências de caracteres e, em seguida, usa evalou new Functionpara avaliar o código. Se você usar alguns truques, se tornará muito mais útil que o mal, mas se não o fizer, poderá levar a coisas ruins. Você pode olhar para o meu sistema de módulos que usa esse padrão: https://github.com/TheHydroImpulse/resolve.js . O Resolve.js usa eval em vez de new Functionprimariamente para modelar o CommonJS exportse as modulevariáveis ​​disponíveis em cada módulo e new Functionagrupa seu código em uma função anônima; no entanto, acabo envolvendo cada módulo em uma função que faço manualmente em combinação com eval.

Você leu mais sobre isso nos dois artigos a seguir, e o último também se refere ao primeiro.

Geradores de harmonia

Agora que os geradores finalmente chegaram à V8 e, portanto, ao Node.js, sob uma bandeira ( --harmonyou --harmony-generators). Isso reduz bastante a quantidade de retorno de chamada que você tem. Torna realmente excelente escrever código assíncrono.

A melhor maneira de utilizar geradores é empregar algum tipo de biblioteca de controle de fluxo. Isso permitirá que o fluxo continue conforme você produz dentro dos geradores.

Resumo / Resumo:

Se você não estiver familiarizado com geradores, é uma prática de pausar a execução de funções especiais (chamadas geradores). Essa prática é chamada de rendimento usando a yieldpalavra - chave.

Exemplo:

function* someGenerator() {
  yield []; // Pause the function and pass an empty array.
}

Portanto, sempre que você chamar essa função pela primeira vez, ela retornará uma nova instância do gerador. Isso permite que você chame next()esse objeto para iniciar ou retomar o gerador.

var gen = someGenerator();
gen.next(); // { value: Array[0], done: false }

Você continuaria ligando nextaté o doneretorno true. Isso significa que o gerador concluiu completamente sua execução e não há mais yieldinstruções.

Controle de fluxo:

Como você pode ver, o controle de geradores não é automático. Você precisa continuar manualmente cada um. É por isso que bibliotecas de fluxo de controle como co são usadas.

Exemplo:

var co = require('co');

co(function*() {
  yield query();
  yield query2();
  yield query3();
  render();
});

Isso permite a possibilidade de escrever tudo no Node (e no navegador com o Regenerator do Facebook, que recebe, como entrada, código-fonte que utiliza geradores de harmonia e divide o código ES5 totalmente compatível) com um estilo síncrono.

Os geradores ainda são bastante novos e, portanto, requerem Node.js> = v11.2. Enquanto escrevo isso, a v0.11.x ainda é instável e, portanto, muitos módulos nativos estão quebrados e permanecerão até a v0.12, onde a API nativa se acalmará.


Para adicionar à minha resposta original:

Recentemente, preferi uma API mais funcional em JavaScript. A convenção usa OOP nos bastidores quando necessário, mas simplifica tudo.

Tomemos, por exemplo, um sistema de exibição (cliente ou servidor).

view('home.welcome');

É muito mais fácil ler ou seguir do que:

var views = {};
views['home.welcome'] = new View('home.welcome');

A viewfunção simplesmente verifica se a mesma visualização já existe em um mapa local. Se a visualização não existir, ela criará uma nova visualização e adicionará uma nova entrada ao mapa.

function view(name) {
  if (!name) // Throw an error

  if (view.views[name]) return view.views[name];

  return view.views[name] = new View({
    name: name
  });
}

// Local Map
view.views = {};

Extremamente básico, certo? Acho que simplifica drasticamente a interface pública e facilita o uso. Eu também emprego capacidade de cadeia ...

view('home.welcome')
   .child('menus')
   .child('auth')

Tower, uma estrutura que estou desenvolvendo (com outra pessoa) ou desenvolvendo a próxima versão (0.5.0) utilizará essa abordagem funcional na maioria das interfaces expostas.

Algumas pessoas aproveitam as fibras como forma de evitar o "inferno de retorno de chamada". É uma abordagem bastante diferente do JavaScript, e eu não sou muito fã dele, mas muitas estruturas / plataformas o usam; incluindo Meteor, pois tratam o Node.js como uma plataforma de thread / por conexão.

Prefiro usar um método abstrato para evitar o inferno de retorno de chamada. Pode se tornar complicado, mas simplifica bastante o código real do aplicativo. Ao ajudar na construção da estrutura do TowerJS , ele resolveu muitos dos nossos problemas, mas obviamente você ainda terá algum nível de retorno de chamada, mas o aninhamento não é profundo.

// app/config/server/routes.js
App.Router = Tower.Router.extend({
  root: Tower.Route.extend({
    route: '/',
    enter: function(context, next) {
      context.postsController.page(1).all(function(error, posts) {
        context.bootstrapData = {posts: posts};
        next();
      });
    },
    action: function(context, next) {
      context.response.render('index', context);
      next();
    },
    postRoutes: App.PostRoutes
  })
});

Um exemplo de nosso sistema de roteamento e "controladores", atualmente em desenvolvimento, embora seja bem diferente dos tradicionais "rails-like". Mas o exemplo é extremamente poderoso e minimiza a quantidade de retornos de chamada e torna as coisas bastante aparentes.

O problema com essa abordagem é que tudo é abstraído. Nada funciona como está e requer uma "estrutura" por trás dele. Mas se esses tipos de recursos e estilo de codificação forem implementados em uma estrutura, será uma grande vitória.

Para padrões em JavaScript, isso depende honestamente. A herança só é realmente útil ao usar o CoffeeScript, Ember ou qualquer estrutura / infraestrutura de "classe". Quando você está em um ambiente JavaScript "puro", o uso da interface tradicional de protótipo funciona como um encanto:

function Controller() {
    this.resource = get('resource');
}

Controller.prototype.index = function(req, res, next) {
    next();
};

O Ember.js começou, pelo menos para mim, usando uma abordagem diferente para construir objetos. Em vez de construir cada método de protótipo independentemente, você usaria uma interface semelhante a um módulo.

Ember.Controller.extend({
   index: function() {
      this.hello = 123;
   },
   constructor: function() {
      console.log(123);
   }
});

Todos esses são estilos diferentes de "codificação", mas são adicionados à sua base de código.

Polimorfismo

O polimorfismo não é amplamente usado em JavaScript puro, onde trabalhar com herança e copiar o modelo semelhante à "classe" requer muito código padrão.

Projeto Baseado em Evento / Componente

Modelos baseados em eventos e componentes são os vencedores da IMO, ou os mais fáceis de trabalhar, especialmente quando se trabalha com o Node.js, que possui um componente EventEmitter interno, no entanto, a implementação de tais emissores é trivial, é apenas uma boa adição .

event.on("update", function(){
    this.component.ship.velocity = 0;
    event.emit("change.ship.velocity");
});

Apenas um exemplo, mas é um bom modelo para se trabalhar. Especialmente em um projeto orientado a jogos / componentes.

O design de componentes é um conceito separado por si só, mas acho que funciona extremamente bem em combinação com sistemas de eventos. Os jogos são tradicionalmente conhecidos pelo design baseado em componentes, onde a programação orientada a objetos leva você apenas até agora.

O design baseado em componentes tem seus usos. Depende do tipo de sistema do seu prédio. Tenho certeza de que funcionaria com aplicativos da Web, mas funcionaria extremamente bem em um ambiente de jogos, devido ao número de objetos e sistemas separados, mas outros exemplos certamente existem.

Padrão Pub / Sub

A associação de eventos e pub / sub é semelhante. O padrão pub / sub realmente brilha nos aplicativos Node.js. devido ao idioma unificador, mas pode funcionar em qualquer idioma. Funciona extremamente bem em aplicativos em tempo real, jogos, etc.

model.subscribe("message", function(event){
    console.log(event.params.message);
});

model.publish("message", {message: "Hello, World"});

Observador

Isso pode ser subjetivo, pois algumas pessoas optam por pensar no padrão Observador como pub / sub, mas elas têm suas diferenças.

"O Observer é um padrão de design em que um objeto (conhecido como assunto) mantém uma lista de objetos dependendo dele (observadores), notificando-os automaticamente sobre qualquer alteração de estado". - O Padrão do Observador

O padrão de observador está um passo além dos típicos sistemas pub / subs. Os objetos têm relacionamentos estritos ou métodos de comunicação entre si. Um objeto "Assunto" manteria uma lista de dependentes "Observadores". O assunto manteria seus observadores atualizados.

Programação Reativa

A programação reativa é um conceito menor e mais desconhecido, especialmente em JavaScript. Há uma estrutura / biblioteca (que eu conheço) que expõe um fácil de trabalhar com a API para usar essa "programação reativa".

Recursos em programação reativa:

Basicamente, é ter um conjunto de dados de sincronização (sejam variáveis, funções, etc.).

 var a = 1;
 var b = 2;
 var c = a + b;

 a = 2;

 console.log(c); // should output 4

Acredito que a programação reativa esteja consideravelmente oculta, especialmente em linguagens imperativas. É um paradigma de programação incrivelmente poderoso, especialmente no Node.js. O Meteor criou seu próprio mecanismo reativo, no qual a estrutura se baseia basicamente. Como a reatividade do Meteor funciona nos bastidores? é uma ótima visão geral de como funciona internamente.

Meteor.autosubscribe(function() {
   console.log("Hello " + Session.get("name"));
});

Isso será executado normalmente, exibindo o valor de name, mas se alterarmos o

Session.set ('nome', 'Bob');

Ele retornará a exibição do console.log Hello Bob. Um exemplo básico, mas você pode aplicar essa técnica a modelos e transações de dados em tempo real. Você pode criar sistemas extremamente poderosos por trás desse protocolo.

Meteoro ...

O padrão reativo e o padrão Observador são bastante semelhantes. A principal diferença é que o padrão observador geralmente descreve o fluxo de dados com objetos / classes inteiros versus a programação reativa, mas descreve o fluxo de dados para propriedades específicas.

Meteor é um ótimo exemplo de programação reativa. Seu tempo de execução é um pouco complicado devido à falta de eventos de alteração de valor nativo do JavaScript (os proxies do Harmony mudam isso). Outras estruturas do lado do cliente, o Ember.js e o AngularJS também utilizam programação reativa (até certo ponto).

As duas estruturas posteriores usam o padrão reativo mais notavelmente em seus modelos (atualização automática). O Angular.js usa uma técnica simples de verificação suja. Eu não chamaria isso de programação exatamente reativa, mas está perto, pois a verificação suja não é em tempo real. O Ember.js usa uma abordagem diferente. Uso de brasas set()e get()métodos que lhes permitem atualizar imediatamente os valores dependentes. Com seu runloop, é extremamente eficiente e permite valores mais dependentes, onde angular tem um limite teórico.

Promessas

Não é uma correção para retornos de chamada, mas remove um pouco do recuo e mantém as funções aninhadas no mínimo. Ele também adiciona uma sintaxe agradável ao problema.

fs.open("fs-promise.js", process.O_RDONLY).then(function(fd){
  return fs.read(fd, 4096);
}).then(function(args){
  util.puts(args[0]); // print the contents of the file
});

Você também pode espalhar as funções de retorno de chamada para que não fiquem alinhadas, mas isso é outra decisão de design.

Outra abordagem seria combinar eventos e promessas para onde você teria uma função para despachar eventos adequadamente, então as funções funcionais reais (aquelas que possuem a lógica real nelas) se vinculariam a um evento específico. Você passaria o método do despachante dentro de cada posição de retorno de chamada, porém, teria que resolver alguns problemas que viriam à mente, como parâmetros, sabendo para qual função enviar, etc ...

Função única Função

Em vez de ter uma enorme bagunça no inferno de retorno de chamada, mantenha uma única função em uma única tarefa e faça-a bem. Às vezes, você pode se antecipar e adicionar mais funcionalidades a cada função, mas pergunte-se: isso pode se tornar uma função independente? Nomeie a função e isso limpa seu recuo e, como resultado, limpa o problema do inferno de retorno de chamada.

No final, sugiro desenvolver ou usar uma pequena "estrutura", basicamente apenas uma espinha dorsal do seu aplicativo, e dedicar um tempo para fazer abstrações, decidir sobre um sistema baseado em eventos ou "vários módulos pequenos que são sistema independente ". Eu trabalhei com vários projetos Node.js. em que o código era extremamente confuso, especialmente com o retorno de chamadas, mas também com uma falta de pensamento antes que eles começassem a codificar. Não se apresse em pensar nas diferentes possibilidades em termos de API e sintaxe.

Ben Nadel fez alguns posts muito bons sobre JavaScript e alguns padrões bastante rígidos e avançados que podem funcionar na sua situação. Algumas boas postagens que vou enfatizar:

Inversão de controle

Embora não esteja exatamente relacionado ao inferno de retorno de chamada, ele pode ajudá-lo à arquitetura geral, especialmente nos testes de unidade.

As duas principais sub-versões da inversão de controle são Injeção de Dependência e Localizador de Serviço. Acho o Service Locator o mais fácil no JavaScript, em oposição à Injeção de Dependências. Por quê? Principalmente porque o JavaScript é uma linguagem dinâmica e não existe digitação estática. Java e C #, entre outros, são "conhecidos" pela injeção de dependência, porque você é capaz de detectar tipos e eles têm interfaces, classes, etc. integrados ... Isso facilita bastante as coisas. Você pode, no entanto, recriar essa funcionalidade no JavaScript, no entanto, não será idêntica e um pouco hacky; prefiro usar um localizador de serviço dentro dos meus sistemas.

Qualquer tipo de inversão de controle desacoplará dramaticamente seu código em módulos separados que podem ser zombados ou falsificados a qualquer momento. Desenhou uma segunda versão do seu mecanismo de renderização? Impressionante, basta substituir a interface antiga pela nova. Os localizadores de serviço são especialmente interessantes com os novos Proxies Harmony, porém, apenas efetivamente utilizáveis ​​no Node.js., ele fornece uma API melhor, em vez de usar Service.get('render');e substituir Service.render. Atualmente, estou trabalhando nesse tipo de sistema: https://github.com/TheHydroImpulse/Ettore .

Embora a falta de digitação estática (a digitação estática seja uma possível razão para o uso efetivo da injeção de dependência em Java, C #, PHP - não é digitada estática, mas possui dicas de tipo), pode ser vista como um ponto negativo, você pode definitivamente transformá-lo em um ponto forte. Como tudo é dinâmico, você pode criar um sistema estático "falso". Em combinação com um localizador de serviço, você pode ter cada componente / módulo / classe / instância vinculado a um tipo.

var Service, componentA;

function Manager() {
  this.instances = {};
}

Manager.prototype.get = function(name) {
  return this.instances[name];
};

Manager.prototype.set = function(name, value) {
  this.instances[name] = value;
};

Service = new Manager();
componentA = {
  type: "ship",
  value: new Ship()
};

Service.set('componentA', componentA);

// DI
function World(ship) {
  if (ship === Service.matchType('ship', ship))
    this.ship = new ship();
  else
    throw Error("Wrong type passed.");
}

// Use Case:
var worldInstance = new World(Service.get('componentA'));

Um exemplo simplista. Para um uso efetivo no mundo real, você precisará levar esse conceito adiante, mas pode ajudar a dissociar o sistema se você realmente deseja a injeção de dependência tradicional. Você pode precisar mexer um pouco com esse conceito. Não pensei muito no exemplo anterior.

Model-View-Controller

O padrão mais óbvio e o mais usado na web. Alguns anos atrás, o JQuery era a moda e, portanto, os plugins do JQuery nasceram. Você não precisava de uma estrutura completa no lado do cliente, basta usar o jquery e alguns plugins.

Agora, há uma enorme guerra de frameworks JavaScript do lado do cliente. A maioria usa o padrão MVC, e todos usam de maneira diferente. MVC nem sempre é implementado da mesma forma.

Se você estiver usando as interfaces prototípicas tradicionais, pode ser difícil obter um açúcar sintático ou uma API agradável ao trabalhar com o MVC, a menos que queira fazer algum trabalho manual. O Ember.js resolve isso criando um sistema de "classe" / objeto ". Um controlador pode se parecer com:

 var Controller = Ember.Controller.extend({
      index: function() {
        // Do something....
      }
 });

A maioria das bibliotecas do lado do cliente também estende o padrão MVC introduzindo auxiliares de exibição (tornando-se visualizações) e modelos (tornando-se visualizações).


Novos recursos de JavaScript:

Isso só será eficaz se você estiver usando o Node.js, mas, mesmo assim, é inestimável. Esta palestra no NodeConf de Brendan Eich traz alguns novos recursos interessantes. A sintaxe da função proposta e, especialmente, a biblioteca js Task.js.

Provavelmente, isso corrigirá a maioria dos problemas com o aninhamento de funções e trará um desempenho um pouco melhor devido à falta de sobrecarga de função.

Não tenho muita certeza se a V8 suporta isso nativamente, a última vez que verifiquei que você precisava ativar alguns sinalizadores, mas isso funciona em uma porta do Node.js que usa o SpiderMonkey .

Recursos extras:


2
Boa redação. Eu pessoalmente não tenho uso para o MV? bibliotecas. Temos tudo o que precisamos para organizar nosso código para aplicativos maiores e mais complexos. Todos eles me lembram muito de Java e C # tentando abrir suas próprias cortinas porcaria sobre o que realmente estava acontecendo na comunicação servidor-cliente. Temos um DOM. Temos delegação de eventos. Temos OOP. Posso vincular meus próprios eventos a alterações de dados tyvm.
Erik Reppen

2
"Em vez de ter uma enorme bagunça no inferno de retorno de chamada, mantenha uma única função em uma única tarefa e faça essa tarefa bem." Poesia.
CuriousWebDeveloper

1
Javascript quando em uma idade muito sombria no início e meados dos anos 2000, quando poucos entendiam como escrever grandes aplicativos usando-o. Como o @ErikReppen diz, se você encontrar seu aplicativo JS parecido com um aplicativo Java ou C #, estará fazendo errado.
backpackcoder

3

Adicionando à resposta de Daniels:

Valores / componentes observáveis

Essa idéia é emprestada da estrutura MVVM Knockout.JS ( ko.observable ), com a idéia de que valores e objetos podem ser assuntos observáveis ​​e, quando a mudança ocorre em um valor ou objeto, ele atualiza automaticamente todos os observadores. É basicamente o padrão de observador implementado em Javascript e, em vez disso, como a maioria das estruturas de pub / sub é implementada, a "chave" é o próprio assunto em vez de um objeto arbitrário.

O uso é o seguinte:

// the subjects
// plain old javascript object with observable values
var shipComponent = {
    velocity : observable(0)
};

// the observer, a player user interface
// implemented with revealing module pattern
var playerUi = (function(ship) {

  var module = {
    setVelocity: function (x) { 
      // ... sets the velocity on the player user interface
    },

    // only called once
    init: function() {

      // subscribe to changes on the velocity value
      // using the module's function as callback
      module.velocity.onChange(playerUi.setVelocity);
    }
  };

  return module;
})(shipComponent).init();

// the player ui will change when the velocity value is changed
shipComponent.velocity.set(10);

A idéia é que os observadores geralmente saibam onde o assunto está e como se inscrever. A vantagem disso, em vez do pub / sub, é perceptível se você precisar alterar muito o código, pois é mais fácil remover assuntos como uma etapa de refatoração. Quero dizer isso, porque depois que você remove um assunto, todos os que dependem dele irão falhar. Se o código falhar rapidamente, você saberá onde remover as referências restantes. Isso contrasta com o assunto completamente dissociado (como uma chave de seqüência de caracteres no padrão pub / sub) e tem uma chance maior de permanecer no código, especialmente se chaves dinâmicas foram usadas e o programador de manutenção não foi informado (morte código na programação de manutenção é um problema irritante).

Na programação de jogos, isso reduz a necessidade de um padrão de loop de atualização antigo e mais para um idioma de programação reativo / evento, porque assim que algo é alterado, o assunto atualiza automaticamente todos os observadores na alteração, sem ter que esperar pelo loop de atualização executar. Existem usos para o loop de atualização (para coisas que precisam ser sincronizadas com o tempo decorrido do jogo), mas às vezes você não deseja desorganizá-lo quando os próprios componentes podem se atualizar automaticamente com esse padrão.

A implementação real da função observável é surpreendentemente fácil de escrever e entender (especialmente se você souber como lidar com matrizes em javascript e o padrão do observador ):

var observable = function(v) {
    var val = v, subscribers = [];

    // the observable object,
    // as revealing module
    var output = {

        // subscribes to event
        onChange : function(func) {
            // idiomatic JS to add object to the
            // subscribers array
            subscribers.push(func);

            return output: // enables chaining
        },

        // the method that changes the observable object
        // and emits the event
        set : function(v) {
            var i;
            val = v;
            for (i = 0, i < subscribers.length; i++) {
                // this is hardly fault tolerant but as long
                // as subscribers are functions it'll work
                subscribers[i](v);
            }

            return output;
        }

    };

    return output;
};

Fiz uma implementação do objeto observável no JsFiddle que continua com a observação de componentes e a remoção de assinantes. Sinta-se livre para experimentar o JsFiddle.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.