是否有实际上适用于Java的OO原理?


79

Javascript是一种基于原型的面向对象语言,但是可以通过以下多种方式变为基于类的语言:

  • 自己编写要用作类的函数
  • 在框架中使用漂亮的类系统(例如mootools Class.Class
  • 从Coffeescript生成

一开始,我倾向于使用Javascript编写基于类的代码,并高度依赖它。但是最近我一直在使用Javascript框架和NodeJS,它们脱离了类的概念,而更多地依赖于代码的动态特性,例如:

  • 异步编程,使用和编写使用回调/事件的编写代码
  • 使用RequireJS加载模块(以便它们不会泄漏到全局名称空间)
  • 功能编程概念,例如列表推导(映射,过滤器等)
  • 除其他事项外

到目前为止,我所收集的是,我已阅读的大多数OO原理和模式(例如SOLID和GoF模式)都是为基于类的OO语言(例如Smalltalk和C ++)编写的。但是其中有哪些适用于基于原型的语言(例如Javascript)?

是否有特定于Java的原则或模式?避免回调地狱邪恶的eval或任何其他反模式的原则。

Answers:


116

经过多次编辑后,此答案的长度已变得异常庞大。我事先表示歉意。

首先,eval()它并不总是坏的,例如,在用于惰性评估时,可以带来性能上的好处。延迟评估类似于延迟加载,但是您实际上将代码存储在字符串中,然后使用evalnew Function评估代码。如果您使用一些技巧,那么它将变得比邪恶更有用,但是如果您不这样做,则可能导致不良后果。您可以查看使用此模式的模块系统:https : //github.com/TheHydroImpulse/resolve.js。Resolve.js使用eval而不是new Function主要是为每个模块中可用的CommonJS exportsmodule变量建模,并将new Function代码包装在匿名函数中,不过,我最终还是将每个模块包装在一个函数中,并与eval手动结合在一起。

您可以在以下两篇文章中了解更多有关它的内容,以后的文章也将参考第一篇。

谐波发生器

现在,生成器终于在标志(--harmony--harmony-generators)下进入了V8,因此也进入了Node.js。这些大大减少了您拥有的回调地狱的数量。这使得编写异步代码确实很棒。

利用生成器的最佳方法是采用某种控制流库。这将使流量继续在发电机中屈服。

回顾/概述:

如果您不熟悉生成器,则可以使用它们来暂停特殊功能(称为生成器)的执行。这种做法称为使用关键字的收益yield

例:

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

因此,每当您第一次调用此函数时,它将返回一个新的生成器实例。这使您可以调用next()该对象来启动或恢复生成器。

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

您将继续打电话next直到done返回true。这意味着生成器已完全完成其执行,并且不再有其他yield语句。

控制流:

如您所见,控制发电机不是自动的。您需要手动继续每个。这就是为什么使用诸如co之类的控制流库的原因。

例:

var co = require('co');

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

这使得可以用同步样式在Node(以及使用Facebook Regenerator的浏览器中编写所有内容)中,该浏览器将利用和声生成器并分离出完全兼容的ES5代码的源代码作为输入,以同步方式进行。

生成器仍然很新,因此需要Node.js> = v11.2。在我撰写本文时,v0.11.x仍然不稳定,因此许多本机模块已损坏,直到v0.12为止,本机API会平静下来。


要添加到我的原始答案中:

我最近一直更喜欢JavaScript中功能更强大的API。约定确实在需要时在幕后使用OOP,但是它简化了所有操作。

以一个视图系统(客户端或服务器)为例。

view('home.welcome');

比以下内容容易阅读或遵循:

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

view函数只是检查本地地图中是否已经存在相同的视图。如果该视图不存在,它将创建一个新视图并向地图添加一个新条目。

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 = {};

非常基础,对吧?我发现它极大地简化了公共界面并使其更易于使用。我也运用连锁能力...

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

塔,我正在(与其他人)一起开发或开发下一版本(0.5.0)的框架将在大多数公开接口中使用此功能方法。

有些人利用纤维来避免“回叫地狱”。它是JavaScript的完全不同的方法,我不是它的忠实拥护者,但是许多框架/平台都在使用它。包括Meteor,因为他们将Node.js视为线程/每个连接平台。

我宁愿使用抽象方法来避免回调地狱。它可能会很麻烦,但是会大大简化实际的应用程序代码。在帮助构建TowerJS框架时,它解决了很多问题,显然,您仍然会具有一定程度的回调,但是嵌套并不深入。

// 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
  })
});

我们目前正在开发的路由系统和“控制器”的一个示例,尽管与传统的“类似导轨”有很大的不同。但是该示例功能非常强大,可以最大程度地减少回调,并使事情变得显而易见。

这种方法的问题在于所有内容都是抽象的。什么都不能照原样运行,并且需要背后的“框架”。但是,如果在框架内实现这些功能和编码风格,那将是一个巨大的胜利。

老实说,对于JavaScript中的模式而言。仅当使用CoffeeScript,Ember或任何“类”框架/基础结构时,继承才真正有用。当您处于“纯” JavaScript环境中时,使用传统的原型接口就像一个魅力:

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

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

至少对我来说,Ember.js开始使用另一种方法来构造对象。与其独立地构造每个原型方法,不如使用类似模块的接口。

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

所有这些都是不同的“编码”样式,但确实会增加您的代码库。

多态性

多态在纯JavaScript中没有得到广泛使用,在纯JavaScript中使用继承和复制类似“类”的模型需要大量样板代码。

基于事件/组件的设计

基于事件的模型和基于组件的模型是IMO的赢家,或者最容易使用,特别是在使用带有内置EventEmitter组件的Node.js时,尽管实现此类发射器很简单,但这只是一个不错的选择。

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

只是一个例子,但这是一个很好的模型。特别是在面向游戏/组件的项目中。

组件设计本身就是一个单独的概念,但是我认为它与事件系统结合使用时效果非常好。传统上,游戏以基于组件的设计而闻名,在这种情况下,面向对象的编程才可以带您到现在为止。

基于组件的设计有它的用途。这取决于建筑物的系统类型。我敢肯定它可以与Web应用程序一起使用,但是由于对象的数量和单独的系统,它在游戏环境中可以非常好地工作,但是肯定还有其他示例。

发布/订阅模式

事件绑定和pub / sub相似。由于使用统一语言,因此发布/订阅模式确实在Node.js应用程序中大放异彩,但是它可以在任何语言中使用。在实时应用程序,游戏等中非常有效。

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

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

观察者

这可能是一个主观的方法,因为有些人选择将Observer模式视为pub / sub,但是他们之间存在差异。

“观察者是一种设计模式,其中一个对象(称为主题)维护一个依赖于它的对象(观察者)的列表,并自动将状态的任何更改通知它们。” - 观察者模式

观察者模式是超越典型发布/订阅系统的一步。对象之间具有严格的关系或通信方法。对象“主题”将保留依赖项“观察者”列表。该主题将使它的观察者保持最新状态。

反应式编程

响应式编程是一个更小,更未知的概念,尤其是在JavaScript中。(我知道)有一个框架/库公开了一种易于使用的API来使用此“反应式编程”。

反应式编程资源:

基本上,它具有一组同步数据(例如变量,函数等)。

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

 a = 2;

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

我相信反应式编程已被相当多地隐藏起来,尤其是在命令式语言中。这是一个非常强大的编程范例,尤其是在Node.js中。流星已经创建了它自己的反应引擎,该引擎基本上基于该引擎。流星的反应性在幕后如何运作?很好地概述了其内部工作方式。

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

这将正常执行,显示的值name,但是如果我们更改它的值

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

它将重新输出显示的console.log Hello Bob。一个基本示例,但是您可以将此技术应用于实时数据模型和事务。您可以在此协议后面创建功能非常强大的系统。

流星的...

反应模式和观察者模式非常相似。主要区别在于观察者模式通常用整个对象/类描述数据流,而反应式编程则用特定属性描述数据流。

流星是反应式编程的一个很好的例子。由于JavaScript缺乏本机值更改事件(和谐代理更改了该值),因此运行时有点复杂。其他客户端框架Ember.jsAngularJS也利用了反应式编程(在某种程度上)。

后两个框架最显着地在模板上使用反应模式(即自动更新)。Angular.js使用一种简单的脏检查技术。我不会称其为完全反应式编程,但是它很接近,因为脏检查不是实时的。Ember.js使用不同的方法。灰烬用法set()get()使它们能够立即更新依赖值的方法。凭借其运行循环,它非常高效,并且可以在角度有理论极限的情况下提供更多相关值。

承诺

不能解决回调问题,但是可以减少一些缩进,并将嵌套函数保持在最低限度。它还为该问题添加了一些不错的语法。

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
});

您还可以扩展回调函数,使它们不内联,但这是另一个设计决定。

另一种方法是将事件和promise组合到可以适当地分配事件的位置,然后实际的功能函数(其中包含真实逻辑的函数)将绑定到特定事件。不过,您需要在每个回调位置内传递dispatcher方法,但是您必须弄清楚一些麻烦,例如参数,知道要分配给哪个函数等。

单功能功能

不必使单个回调函数陷入混乱,而应将单个函数保持在单个任务上,并很好地完成该任务。有时您可以超越自己,在每个功能中添加更多功能,但是请问自己:这可以成为一个独立的功能吗?命名函数,这将清理您的缩进,并因此清理回调地狱问题。

最后,我建议开发或使用一个小的“框架”,该框架基本上只是您的应用程序的骨干,并花一些时间进行抽象,决定基于事件的系统或“大量的小模块”。独立”系统。我曾在多个Node.js项目中工作过,这些项目的代码特别令人头疼,尤其是回调地狱,但在他们开始编码之前也缺乏思想。花些时间思考一下API和语法方面的各种可能性。

Ben Nadel发表了一些非常不错的有关JavaScript的博客文章,以及一些可能适用于您的情况的非常严格和高级的模式。我将重点介绍一些不错的帖子:

控制反转

尽管与回调地狱不完全相关,但它可以帮助您总体架构,尤其是在单元测试中。

控制反转的两个主要子版本是依赖注入和服务定位器。与依赖注入相反,我发现Service Locator在JavaScript中最简单。为什么?主要是因为JavaScript是一种动态语言,并且不存在静态类型。Java和C#等因依赖项注入而“闻名”,因为您能够检测类型,并且它们内置了接口,类等。这使事情变得相当容易。但是,您可以在JavaScript中重新创建此功能,尽管它并不完全相同,而且有点笨拙,但我更喜欢在系统内使用服务定位器。

任何类型的控制反转都将极大地将您的代码分离为可以随时模拟或伪造的单独模块。设计了渲染引擎的第二版?太棒了,只需用旧界面替换新界面即可。服务定位器对于新的Harmony Proxies尤其有趣,尽管它只能在Node.js中有效使用,但它提供了更好的API,而Service.get('render');不是使用and Service.render。我目前正在使用这种系统:https : //github.com/TheHydroImpulse/Ettore

尽管缺少静态类型(静态类型可能是Java,C#,PHP中依赖注入有效使用的可能原因-它不是静态类型,但具有类型提示。)可能会被视为否定点,您可以肯定会把它变成一个优势。因为一切都是动态的,所以您可以设计一个“伪”静态系统。结合服务定位器,您可以将每个组件/模块/类/实例绑定到一个类型。

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'));

一个简单的例子。对于现实世界中的有效用法,您需要进一步推广此概念,但是如果您确实想要传统的依赖项注入,则可以帮助您的系统脱钩。您可能需要弄一点这个概念。在前面的示例中,我并没有考虑太多。

模型视图控制器

最明显的模式,也是网络上使用最多的模式。几年前,JQuery风靡一时,因此,JQuery插件诞生了。您不需要客户端上的完整框架,只需使用jquery和一些插件即可。

现在,有一场巨大的客户端JavaScript框架大战。其中大多数使用MVC模式,并且使用方式各不相同。MVC并非总是实现相同的。

如果您使用的是传统的原型接口,那么在使用MVC时可能很难获得语法糖或不错的API,除非您想做一些手工工作。Ember.js通过创建“类” /“对象”系统来解决此问题,控制器可能类似于:

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

大多数客户端库还通过引入视图助手(成为视图)和模板(成为视图)来扩展MVC模式。


新的JavaScript功能:

这仅在您使用Node.js时才有效,但是,它是无价的。Brendan Eich在NodeConf上的演讲带来了一些很棒的新功能。建议的函数语法,尤其是Task.js js库。

这可能会解决函数嵌套中的大多数问题,并且由于缺少函数开销而将带来更好的性能。

我不太确定V8是否本身支持此功能,最后我检查了您是否需要启用一些标志,但这在使用SpiderMonkey的Node.js端口中有效

额外资源:


2
不错的文章。我个人没有使用MV吗?库。我们拥有组织大型更复杂应用程序的代码所需的一切。他们都让我想起Java和C#太多了,它们试图抛出自己的各种废话,以解决服务器-客户端通信中实际发生的事情。我们有一个DOM。我们有活动委托。我们得到了OOP。我可以将自己的事件绑定到数据更改tyvm。
Erik Reppen 2013年

2
“与其将混乱的回调弄得一团糟,不如将一个函数保留在单个任务上,并很好地完成该任务。” - 诗歌。
CuriousWebDeveloper 2014年

1
当Javascript在2000年代初期到中期处于非常黑暗的时代时,很少有人了解如何使用Javascript编写大型应用程序。就像@ErikReppen所说的,如果您发现JS应用程序看起来像Java或C#应用程序,那么您做错了。
背包

3

添加到丹尼尔斯答案:

可观察的值/组成

这个想法是从MVVM框架Knockout.JSko.observable)借来的,该想法是值和对象可以是可观察的主题,并且一旦一个值或对象发生更改,它将自动更新所有观察者。它基本上是用Javascript实现的观察者模式,而不是大多数pub / sub框架的实现方式,“键”本身就是主题,而不是任意对象。

用法如下:

// 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);

这个想法是观察者通常知道主题在哪里以及如何订阅。如果您必须进行大量更改,则此代码代替pub / sub的优势非常明显,因为在重构步骤中,删除主题很容易。我的意思是因为一旦删除某个主题,所有依赖该主题的人都会失败。如果代码快速失败,那么您知道在哪里删除其余的引用。这与完全解耦的主题(例如在pub / sub模式中使用字符串键)形成对比,并且有更大的机会保留在代码中,特别是在使用了动态键并且维护程序员没有意识到这一点的情况下(死机)维护编程中的代码是一个烦人的问题)。

在游戏编程,这减少了需要的叶奥尔德更新循环模式等多个项目为事件触发/无功编程成语,因为一旦事情是话锋一转将自动更新的变化所有的观察者,而不必等待更新循环执行。更新循环有很多用途(用于需要与游戏时间同步的事物),但是有时您只是不想在组件本身可以使用此模式自动更新时将其弄乱。

observable函数的实际实现实际上非常容易编写和理解(特别是如果您知道如何使用javascript和观察者模式处理数组):

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;
};

我已经在JsFiddle实现了可观察对象的实现,该实现在此基础上继续观察组件并能够删除订户。随意尝试JsFiddle。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.