使用 Node EventEmitter,如何从服务列表中删除对象,同时取消订阅服务的事件调度?

问题描述 投票:0回答:1

我正在研究Node.js的EventEmitter。我正在尝试创建一家面包店,如果顾客在烘烤面包时有钱,他们会自动购买面包。尽管如此,我还想创建一个可以添加或删除客户的客户列表,并且他们只会在添加时监听事件,而在删除时不会再监听。我就是这么做的:

面包店.js

const events = require('./events.js');
const eventEmitter = require('./event-emitter.js');

class Bakery {
  _breads;
  _customers;
  _breadPrice;

  constructor(breadPrice) {
    this._breads = 0;
    this._customers = [];
    this._breadPrice = breadPrice;
  }

  get breadPrice() {
    return this._breadPrice;
  }

  bakeBreads(breadsQuantity) {
    this._breads += breadsQuantity;
    eventEmitter.emit(events.BREAD_BAKED, breadsQuantity);
  }

  addCustomer(customer) {
    // How would i do?
    this._customers.push(customer);

    eventEmitter.on(events.BREAD_BAKED, (breads) => {
      customer.buyBread(breads);
    })

    customer.bakery = this;
  }

  removeCustomer(customer) {
    // How would i do?
  }
}
module.exports = Bakery;

customer.js

const events = require('./events.js');
const eventEmitter = require('./event-emitter.js');

class Customer {
  _customerName;
  _maxPrice;
  _moneyAmount;
  _bakery;

  constructor(customerName, maxPrice, moneyAmount, bakery) {
    this._maxPrice = maxPrice;
    this._moneyAmount = moneyAmount;
    this._bakery = bakery;
    this._customerName = customerName;
  }

  set bakery(bakery) {
    this._bakery = bakery;
  }

  buyBread() {
    if (this._moneyAmount >= this._bakery.breadPrice) {
      this._moneyAmount -= this._bakery.breadPrice;
      console.log(`Customer ${this._customerName} bought a bread costing ${this._bakery._breadPrice}`);
      return;
    }

    console.log(`${this._customerName} doesn't have enough money to buy a bread`);
  }
}
module.exports = Customer;

main.js

const events = require('./events.js');
const Bakery = require('./bakery.js');
const Customer = require('./customer.js');
const eventEmitter = require('./event-emitter.js');

const bakery = new Bakery(2.5)
const johnRich = new Customer('John', 1, 10);
const martinPoor = new Customer('Martin', 0.3, 1);

您对如何正确实施有什么想法吗?

javascript oop model event-handling observer-pattern
1个回答
0
投票

OP 问题的可能解决方案包括两个主要的设计更改,这又会导致模型和实现中出现更多更改。

  • 客户对象存储在客户名称下的

    Map
    实例中,其中客户名称应该是唯一的。

  • 客户要么获得自己的函数,如何处理从面包店购买面包的行为,该函数只是通知新烘焙的面包,要么这个处理函数在实例化时被分配为每个客户对象的默认实现。

因此,对

Customer
类的更改比对
Bakery
类的更改的侵入性更小。

customer.js

class Customer {

  _customerName;
  _maxPrice;
  _moneyAmount;
  _handlePurchaseBread;

  constructor(customerName, maxPrice, moneyAmount, handlePurchaseBread) {
  
    this._customerName = customerName;

    this._maxPrice = maxPrice;
    this._moneyAmount = moneyAmount;

    this._handlePurchaseBread = (typeof handlePurchaseBread === 'function')
      // - either a customer instance gets provided its own function which
      //   handles specifically how much bread a customer is going to buy.
      //    - in this case the just created customer instance needs to be
      //      explicitly bound to the provided handler.
      && handlePurchaseBread.bind(this)
      // - or one does fall back to a hanlder where each customer instance
      //   does purchase exactly one bread each time the handler gets invoked.
      //    - in this case the customer's correct `this` handling gets achieved
      //      via the lexical binding of the utilized arrow function expression.
      || (({ bakery, quantity }) => this.buyBread(bakery, 1));
  }

  // - the arguments signature of `buyBread` needs to be changed
  //   in order to allow a customer to purchase bread from more
  //   than just a single bakery.
  // - In addition the quantity of to be purchased pieces of bread
  //   has to be taken into consideration as well.
  buyBread(bakery, quantity = 1) {
    if (this._moneyAmount >= bakery.breadPrice) {

      this._moneyAmount -= bakery.breadPrice;
      
      console.log(
        `Customer ${ this._customerName } bought a bread costing ${ bakery.breadPrice }.`
      );
      return;
    }
    console.log(`${ this._customerName } doesn't have enough money to buy a bread.`);
  }
}
module.exports = Customer;

面包店需要经历的改变如下......

  • 实现客户对象的基于地图的存储。
  • 根据
    addCustomer
    /
    removeCustomer
    实施
    1. 基于地图的存储。
    2. 与事件相关的必要变更
      • a) 如何发出事件和传递数据。
      • b) 如何取消/注册客户的面包采购处理程序。

通过实施点2),我们发现顾客应该能够从任何一家面包店购买面包。

这是 OP 设计的直接结果,将客户添加/删除到面包店的客户列表中,并向每个注册客户发送 “面包烘焙” 事件。因此,将单个面包店实例分配给客户对象没有任何价值。另一方面,应该清楚的是,发送的数据不仅必须提供新鲜出炉的面包片的数量,还必须提供负责烘烤所有面包的面包店......

这直接导致了客户

buyBread
方法的参数签名的改变。它的第一个参数必须是面包店参考,第二个参数必须是要购买的面包片的数量,默认值为 1。

此外,还有一些针对隐私和字段保护的更改。 OP 使用下划线来注释伪私有字段。另一方面,OP 使用 get 方法来访问此类已经公共的字段,例如面包店的

_breadPrice
breadPrice
。在这种情况下,以及其他一些情况下,使用真正的私有属性

是完全可以的

此外,最重要的是,OP 似乎对每个要创建的

EventEmitter
实例使用单个
Bakery
实例。但是,正确定位的事件/数据调度依赖于面包店实例与其所有注册客户之间的松散关系。因此,每个面包店对象都需要自己的
EventEmitter
实例。

面包店.js

const EventEmitter = require('node:events');
const events = require('./events.js');

class Bakery {
  _breads;

  // - since a `breadPrice` getter already does exist for the
  //   former `_breadPrice`, make it a true private field.
  #breadPrice;

  // - likewise for customers; but here due to storing
  //   customer references within a map which via the
  //   getter can be accessed as an array of customer objects.
  #customers;
  
  #eventEmitter;

  constructor(breadPrice) {
    this._breads = 0;

    this.#breadPrice; = breadPrice;

    // store customers within a `Map` instance.
    this.#customers = new Map;

    // any bakery object needs its own `EventEmitter` instance.
    this.#eventEmitter = new EventEmitter;
  }

  get breadPrice() {
    // public, but with write protecting.
    return this.#breadPrice;
  }
  get customers() {
    // public, but with write protecting and presented as array.
    return [...this.#customers.values()];
  }

  // - change the wording of `breadsQuantity` to `quantity`
  // - within the context of newly baked breads its clear
  //   what `quantity` refers to.
  // - and consider changing the wording of `this._breads`
  //   to e.g. `this._breadCount`.
  bakeBreads(quantity) {
    this._breads += quantity;

    // - emitting just the newly baked quantity does not help.
    // - one in addition needs the reference of the bakery
    //   which did the baking job.
    this.#eventEmitter.emit(events.BREAD_BAKED, { bakery: this, quantity });
  }

  addCustomer(customer) {
    const { _customerName: name } = customer;

    // - identify new customers by name.
    if (!this.#customers.has(name)) {

      // - store a new customer under its name based key.
      this.#customers.set(name, customer);

      // - register a customer's handler of how to purchase bread.
      this.#eventEmitter.on(events.BREAD_BAKED, customer._handlePurchaseBread);
    }
  }
  removeCustomer(customer) {
    const { _customerName: name } = customer;

    // - identify the to be removed customer by name.
    if (this.#customers.has(name)) {

      // - delete a new customer via its name based key.
      this.#customers.delete(name);

      // - un-register a customer's handler of how to purchase bread.
      this.#eventEmitter.off(events.BREAD_BAKED, customer._handlePurchaseBread);
    }
  }
}
module.exports = Bakery;

编辑

在下一个代码迭代中,有关买卖的建模方法确实需要改进。

一个可能的解决方案是实施面包店的

sellBread
方法,该方法通过客户参考和要购买的面包块的数量。该方法将控制买方/卖方交易是否成功的整个验证。客户还可以使用“
buyBread
”方法,该方法会传递面包店参考信息和已经提到的要购买面包的数量。此方法只会转发到传递的面包店引用的
sellBread
方法,但会返回交易结果,该结果要么成功,要么附带交易失败的原因。

下一个提供的示例代码实现了一个

Bakery
类,该类还扩展了
EventTarget
,以演示如何直接在
addEVentListener
实例中利用
removeEventListener
/
dispatchEvent
Bakery

最重要的是还有其他代码改进,例如验证传递给

addCustomers
/
removeCustomers
的项目是否是有效的客户对象。

// main.js

// const Bakery = require('./bakery.js');
// const { Customer } = require('./customer.js');

const klugesherz =
  new Bakery({ name: 'Pâtisserie Klugesherz', breadPrice: 1.5 });
const hanss =
  new Bakery({ name: 'Boulangerie Patisserie Hanss', breadPrice: 2.7 });

const johnRich = new Customer({
  name: 'John Rich',
  maxPrice: 5,
  moneyTotal: 20,
  handlePurchaseBread: function ({ currentTarget: bakery }) {
    this.buyBread(bakery, 3);
  },
});
const martinPoor = new Customer({
  name: 'Martin Poor',
  maxPrice: 3,
  moneyTotal: 10,
  handlePurchaseBread: function ({ currentTarget: bakery }) {
    const quantity = (
      ((bakery.name === 'Boulangerie Patisserie Hanss') && 1) ||
      ((bakery.name === 'Pâtisserie Klugesherz') && 2) || 0
    );
    this.buyBread(bakery, quantity);
  },
});

klugesherz.addCustomers(johnRich, martinPoor);
hanss.addCustomers(johnRich, martinPoor);

console.log({
  bakeries: {
    klugesherz: klugesherz.valueOf(),
    hanss: hanss.valueOf(),
  },
  customers: {
    johnRich: johnRich.valueOf(),
    martinPoor: martinPoor.valueOf(),
  },
});

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n... remove John Rich from the customer list of Patisserie Hanss.\n\n');
hanss.removeCustomers(johnRich);

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n... remove Martin Poor from the customer list of Patisserie Hanss.');
hanss.removeCustomers(martinPoor);

console.log('... remove John Rich and Martin Poor from the customer list of Pâtisserie Klugesherz.\n\n');
klugesherz.removeCustomers(johnRich, martinPoor);

console.log('+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// customer.js

class Customer {
  #name;
  #maxPrice;
  #moneyTotal;
  #handlePurchaseBread;

  constructor({ name, maxPrice, moneyTotal, handlePurchaseBread }) {
    this.#name = name;
    this.#maxPrice = maxPrice;
    this.#moneyTotal = moneyTotal;
    this.#handlePurchaseBread = (typeof handlePurchaseBread === 'function')
      && handlePurchaseBread.bind(this)
      || (({ currentTarget: bakery }) => { this.buyBread(bakery, 1); });
  }
  get name() {
    return this.#name;
  }
  get maxPrice() {
    return this.#maxPrice;
  }
  get moneyTotal() {
    return this.#moneyTotal;
  }
  get handlePurchaseBread() {
    return this.#handlePurchaseBread;
  }

  buyBread(bakery, quantity = 1) {
    const { approved, reason } = bakery.sellBread(this, quantity);

    if (approved === true) {

      this.#moneyTotal = this.moneyTotal - (bakery.breadPrice * quantity);

      console.log(
        `Customer ${ this.name } bought ${ quantity } piece/s of bread for a total of ${ (bakery.breadPrice * quantity).toFixed(2) } at ${ bakery.name }.`
      );
    } else if (typeof reason === 'string') {

      console.log('Buying a bread did fail, due to ...', reason);
    }
  }

  valueOf() {
    const { name, maxPrice, moneyTotal, handlePurchaseBread } = this;
    return { name, maxPrice, moneyTotal, handlePurchaseBread };
  }
}

function isCustomer(value) {
  return ((value instanceof Customer) || (
    Object
      .keys(value.valueOf())
      .sort()
      .join('_') === 'handlePurchaseBread_maxPrice_moneyTotal_name'
  ));
}

// module.exports = { Customer, isCustomer };
</script>

<script>
// bakery.js

// const { isCustomer } = require('./customer.js');

class Bakery extends EventTarget {
  #name;
  #breadPrice;
  #breadCount;
  #moneyTotal;
  #customers;

  constructor({ name, breadPrice }) {
    super();

    this.#name = name;
    this.#breadPrice = breadPrice;
    this.#breadCount = 0;
    this.#moneyTotal = 0;
    this.#customers = new Map;
  }

  get name() {
    return this.#name;
  }
  get breadPrice() {
    return this.#breadPrice;
  }
  get breadCount() {
    return this.#breadCount;
  }
  get moneyTotal() {
    return this.#moneyTotal;
  }
  get customers() {
    return [...this.#customers.values()];
  }

  addCustomers(...customers) {
    customers
      .flat()
      .filter(isCustomer)
      .forEach(customer => {
        const { name } = customer;
        
        if (!this.#customers.has(name)) {
          this.#customers.set(name, customer);

          this.addEventListener('bread-baked', customer.handlePurchaseBread);
        }
      });
  }
  removeCustomers(...customers) {
    customers
      .flat()
      .filter(isCustomer)
      .forEach(customer => {
        const { name } = customer;
        
        if (this.#customers.has(name)) {
          this.#customers.delete(name);

          this.removeEventListener('bread-baked', customer.handlePurchaseBread);
        }
      });
  }

  bakeBread(quantity = 10) {
    this.#breadCount = this.#breadCount + quantity;

    this.dispatchEvent(
      new CustomEvent('bread-baked', { detail: { quantity } })
    );
  }

  sellBread(customer, quantity = 1) {
    const transaction = { approved: false };

    if (quantity >= 1) {
      if (this.breadCount >= quantity) {

        const isWithinPriceLimit = this.breadPrice <= customer.maxPrice;
        const canEffortPurchase = (this.breadPrice * quantity) <= customer.moneyTotal;

        if (isWithinPriceLimit) {
          if (canEffortPurchase) {

            this.#breadCount = this.breadCount - quantity;

            transaction.approved = true;
          } else {
            transaction.reason =
              `Customer ${ customer.name } doesn't have enough money for buying a bread at ${ this.name }.`;
          }
        } else {
          transaction.reason =
            `Customer ${ customer.name } does have a price limit which just did exceed at ${ this.name }.`;
        }
      } else {
        transaction.reason =
          `The ${ this.name } bakery is too low on bread stock in order to fulfill ${ customer.name }'s order.`;
      }
    } else {
      transaction.reason =
        `Customer ${ customer.name } did not provide a valid quantity for purchasing bread at ${ this.name }.`;
    }
    return transaction;
  }

  valueOf() {
    const { name, breadPrice, breadCount, moneyTotal, customers } = this;
    return {
      name, breadPrice, breadCount, moneyTotal,
      customers: customers.map(customer => customer.valueOf()),
    };
  }
}

// module.exports = Bakery;
</script>

© www.soinside.com 2019 - 2024. All rights reserved.