【问题标题】:How to define private variables with behavior delegation (OLOO) in JS?如何在 JS 中使用行为委托(OLOO)定义私有变量?
【发布时间】:2018-02-03 00:38:00
【问题描述】:

我正试图围绕对象链接其他对象来编写节点模块。这是我目前所拥有的(灵感来自this answer):

'use strict'

// Composable prototype object
var parent = {
  publicVar: 1,
  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

// Composable prototype object
var child = {
  doSomethingChild() {
    return this.publicVar + 20
  } 
}

// an external method
function externalMethod(arg) {
  return arg
}

// the parent factory
function Parent() {
  let privateVar = 2

  return Object.assign({
    getPrivate() {
      return privateVar
    }
  }, parent)
}

// the child factory
function Child() {
  let privateVar = 4
  let parent = Parent() // call to the Parent factory
  return Object.assign(parent, child, {
    getPrivateChild() {
        return privateVar
    }
  })
}

// Node export
module.exports = {
  Parent: Parent(),
  Child: Child()
}

稍后,我将需要这样的模块:

Parent = require('./my-module').Parent
Child = require('./my-module').Child
Parent.getPrivate() // 2
Parent.doSomething() // 11
Child.getPrivateChild() // 4
Child.doSomethingChild() // 21

恐怕使用 OLOO 可能会有更优雅的方式来做到这一点。我主要担心的是我认为我应该在 Child 工厂做let parent = Object.create(Parent),但如果我这样做它就行不通。

那么,1)我错过了什么,2)可以重构吗?

【问题讨论】:

  • 我不确定链接的答案是否是一个应该遵循的好例子。它声称它确实“组合而不是继承”,但实际上它只是混合模式。考虑到我们有类,这在 ES6 中看起来很残缺。您是否有尝试以这种方式解决的类和原型惯性问题?由于问题有ecmascript-6 标签,显而易见的答案是“不要在 ES6 中那样做”。
  • @estus 你推荐使用 ES6 类吗?有几个人对他们提出了很好的理由:Kyle SimpsonEric Elliott。你会如何重构它?
  • 对于这种情况,绝对是。两个类,两个实例,没有什么可考虑的。一旦你掌握了这两种方法并知道它们如何影响设计和性能,你就可以选择其中一种——但大多数时候你会选择课程,因为它们是有效的并且可以完成工作。我觉得 YDKJS 系列很有启发性,但也有不合理的固执己见。上述章节抨击原型,虽然它们对 JS 来说是惯用的,并且可能是 perform betterIRL。
  • 真的不应该导出Parent()Child()实例,而是导出ParentChild函数本身。
  • @nachocab 如果你导出实例,它们就会变成单例,而你所有复杂的工厂东西都可以被一个微不足道的对象字面量替换。

标签: javascript ecmascript-6 stampit.js


【解决方案1】:

你绝对应该更喜欢组合(包括混合)而不是单祖先类继承,所以你走在正确的轨道上。也就是说,JavaScript 没有您可能从其他语言中知道的私有属性。我们在 JS 中使用闭包来保护数据隐私。

对于具有真正数据隐私(通过闭包)的可组合原型,您正在寻找的是功能混合,它们是接受一个对象并返回一个添加了新功能的对象的函数。 p>

但是,在我看来,使用可组合工厂(例如stamps)进行功能继承通常是更好的做法。 AFAIK,Stampit 是使用最广泛的可组合工厂实现。

戳记是一个可组合的工厂函数,它根据其描述符返回对象实例。邮票有一个称为.compose() 的方法。当调用.compose() 方法时,使用当前标记作为基础创建新标记,并由作为参数传递的可组合列表组成:

const combinedStamp = baseStamp.compose(composable1, composable2, composable3);

可组合是一个标记或 POJO(普通旧 JavaScript 对象)标记描述符。

.compose() 方法兼作图章的描述符。换句话说,描述符属性附加到标记.compose() 方法,例如stamp.compose.methods.

可组合描述符(或仅仅是描述符)是一个元数据对象,它包含创建对象实例所需的信息。 一个描述符包含:

  • methods — 将添加到对象的委托原型中的一组方法。
  • properties — 将通过赋值添加到新对象实例的一组属性。
  • initializers — 将按顺序运行的函数数组。标记详细信息和参数被传递给初始化程序。
  • staticProperties — 一组静态属性,将通过分配复制到图章。

诸如“我如何继承特权方法和私有数据?”之类的基本问题以及“继承层次结构有哪些好的替代方案?”是许多 JavaScript 用户的障碍。

让我们使用stamp-utils 库中的init()compose() 同时回答这两个问题。

  • compose(…composables: [...Composable]) => Stamp 接受任意数量的可组合并返回一个新图章。
  • init(…functions: [...Function]) => Stamp 接受任意数量的初始化函数并返回一个新标记。

首先,我们将使用闭包来创建数据隐私:

const a = init(function () {
  const a = 'a';

  Object.assign(this, {
    getA () {
      return a;
    }
  });
});

console.log(typeof a()); // 'object'
console.log(a().getA()); // 'a'

它使用函数范围来封装私有数据。请注意,getter 必须在函数内部定义才能访问闭包变量。

这是另一个:

const b = init(function () {
  const a = 'b';

  Object.assign(this, {
    getB () {
      return a;
    }
  });
});

那些a 不是拼写错误。重点是证明ab 的私有变量不会冲突。

但这是真正的享受:

const c = compose(a, b);

const foo = c();
console.log(foo.getA()); // 'a'
console.log(foo.getB()); // 'b'

什么?是的。您只是同时从两个来源继承了特权方法和私有数据。

在使用可组合对象时,您应该遵守一些经验法则:

  1. 组合不是类继承。不要试图建模 is-a 关系或从父/子关系的角度考虑事物。相反,使用基于特征的思维。 myNewObject 需要 featureAfeatureBfeatureC,所以:myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory()。请注意,myNewObject 不是featureAfeatureB 等的实例...相反,它实现使用,或包含这些功能。
  2. Stamps 和 mixin 不应该相互了解。 (没有隐式依赖)。
  3. Stamps 和 mixin 应该很小。尽可能少地引入新属性。
  4. 在组合时,您可以并且应该有选择地只继承您需要的道具,并重命名道具以避免冲突。
  5. 尽可能选择模块以进行代码重用(大部分时间应该如此)。
  6. 更喜欢域模型和状态管理的函数式编程。避免共享可变状态。
  7. 更喜欢高阶函数和高阶组件,而不是任何类型的继承(包括 mixin 或标记)。

如果您遵守这些准则,您的 stamps 和 mixin 将不会受到常见继承问题的影响,例如脆弱的基类问题、大猩猩/香蕉问题、必要性重复问题等...

【讨论】:

  • 这是一组出色的邮票。谢谢你!我一直在探索这种方法——虽然与我在其他 oop 语言中习惯的经典方法非常不同——但我可以看到它是一种非常强大的工作方式!
【解决方案2】:

有了 ES6 类,就这么简单

class Parent {
  constructor() {
    this.publicVar = 1;
    this._privateVar = 2;
  }

  getPrivate() {
    return this._privateVar;
  }

  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

class Child extends Parent {
  constructor() {
    super();
    this._privateVar = 4;
  }

  doSomethingChild() {
    return this.publicVar + 20
  } 
}

module.exports = {
  parent: new Parent(),
  child: new Child()
}

根据publicVar_privateVar 的角色,它们可能是静态属性。

_privateVar 属性的使用并非偶然。通常_ 命名约定(以及可能不可编号的描述符)足以将成员指定为私有/受保护的。

Object.assign 在 ES6 中作为主要继承技术无效,但可以附加用于实现多态继承。

【讨论】:

  • 这里的问题是._privateVar 并不是真正的私密。
  • @EricElliott 真的很麻烦吗?我之前尝试过为私有成员使用局部变量和符号,封装是唯一的优点 - 并且本身就是目的。虽然缺乏可测试性和可扩展性是一个主要缺点。由于封装在 JS 中从来都不是安全问题,因此通常 _ 命名约定(可能还有不可编号的描述符)就足够了。当然,上面的代码可以为每个类设置_privateVar变量而不是属性,结果是子类应该定义自己的getPrivate方法,如OP所示。
  • @EricElliott 如果您对此感到担心,可以将var privateVar = 2; this.getPrivate = () => privateVar; 放入构造函数中。但正如 estus 所解释的,这很少值得。
  • 封装是最初发明 OOP 的主要原因之一。通过仅公开公共 API,封装有助于创建更具自我记录性的对象。同样,如果您在测试或扩展中依赖私有道具,那么您依赖的是实现而不是接口。 “编程到接口,而不是实现”是四个“设计模式”帮的关键建议,这是有充分理由的。实施细节可能会改变和破坏依赖它们的东西。
  • @Bergi 请参阅我的评论以获取有关将闭包用于可组合 mixin 的更详细说明。 @estus 小心不要将一些 lib 作者糟糕的设计选择归咎于封装。 _ 不起作用的主要原因之一是新手不知道这意味着什么,而有经验的开发人员认为,“我知道我在做什么,这种隐私不适用于我。”当没有人尊重隐私时,事情就会变得更加糟糕。
【解决方案3】:

在仔细阅读所有帖子并尝试了解 OP 的问题后,我认为 OP 的方法已经非常接近可靠的解决方案。毕竟,JS 中的可靠封装主要依赖于一些基于闭包的技术。基于纯对象和工厂的方法也很精简。

为了完全理解所提供的示例,我确实重构了它,更明确地命名了不同的可组合部分,尤其是“行为”部分。我也对孩子与父母的关系以及如何出口工厂感到不舒服。但由于此类示例大多是精简代码,因此必须经常猜测 OP 的现实世界问题。

这是一个运行的重构代码,它确实保留了 OP 的方法,看起来像......

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable object based behavior
const withGetPublicValueIncrementedByTen = {
  getPublicValueIncrementedByTen() {

    return (externalPassThroughMethod(this.publicValue) + 10);
  }
};

// another composable object based behavior
const withGetPublicValueIncrementedByTwenty = {
  getPublicValueIncrementedByTwenty() {

    return (externalPassThroughMethod(this.publicValue) + 20);
  }
};

// the parent factory
function createParent(publicOptions = {}) {
  var localValue = 2;

  // `publicValue` via `publicOptions`
  return Object.assign({}, publicOptions, withGetPublicValueIncrementedByTen, {
    getLocalValue() {

      return localValue;
    }
  });
}

// the child factory
function createChild(parent) {
  var localValue = 4;

  // `publicValue` via `parent`
  return Object.assign({}, parent, withGetPublicValueIncrementedByTwenty, {
    getLocalValue() {

      return localValue;
    },
    getLocalValueOfParent() { // object linking other object ...

      return parent.getLocalValue(); // ... by forwarding.
    }
  });
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }

下一个给出的示例代码采用刚刚提供的重构示例,但使用基于函数而不是基于对象的混合和工厂创建基于类的类型而不是基于普通对象(文字)的类型。然而,这两个示例在如何处理封装和组合方面有一个共同点且相同的方法......

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable function based behavior
const withGetPublicValueIncrementedByTen = (function () {
  function getPublicValueIncrementedByTen() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 10);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTen = getPublicValueIncrementedByTen;
  };
}());

// another composable function based behavior
const withGetPublicValueIncrementedByTwenty = (function () {
  function getPublicValueIncrementedByTwenty() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 20);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTwenty = getPublicValueIncrementedByTwenty;
  };
}());

class Parent {
  constructor(publicOptions = {}) {

    function getLocalValue() {
      return localValue;
    }
    var localValue = 2;

    // `publicValue` via `publicOptions`
    Object.assign(this, publicOptions);

    withGetPublicValueIncrementedByTen.call(this);

    this.getLocalValue = getLocalValue;
  }
}

class Child {
  constructor(parent) {

    function getLocalValue() {
      return localValue;
    }
    function getLocalValueOfParent() {  // object linking other object ...
      return parent.getLocalValue();    // ... by forwarding.
    }
    var localValue = 4;

    // `publicValue` via `parent`
    Object.assign(this, parent);

    withGetPublicValueIncrementedByTwenty.call(this);

    this.getLocalValue          = getLocalValue;
    this.getLocalValueOfParent  = getLocalValueOfParent;
  }
}

function createParent(publicOptions = {}) {
  return (new Parent(publicOptions));
}
function createChild(parent) {
  return (new Child(parent));
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }

【讨论】:

    【解决方案4】:

    为了完整起见,使用Eric Elliott 的答案非常简单:

    var stampit = require('stampit')
    
    function externalMethod(myVar) {
      return myVar
    }
    
    parent = stampit().init(function({value}){ 
        this.privateVar = 2
      }).props({
        publicVar: 1
      }).methods({
        doSomething() {
          return externalMethod(this.publicVar) + 10
        },
        getPrivate() {
          return this.privateVar
        }
      })
    
    child = parent.init(function({value}){
        this.privateVar = 4
      }).methods({
      doSomethingChild() {
        return this.publicVar + 20
      }
    })
    
    parent().getPrivate() // 2
    parent().doSomething() // 11
    child().getPrivate() // 4
    child().doSomething() // 11
    child().doSomethingChild() // 21
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-08-27
      • 1970-01-01
      • 2010-10-13
      • 1970-01-01
      • 1970-01-01
      • 2021-02-12
      • 2013-03-16
      • 1970-01-01
      相关资源
      最近更新 更多