我正在研究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);
您对如何正确实施有什么想法吗?
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
实施
通过实施点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>