企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
这里我展示了一份有待测试的代码。这份代码来自一个简单的应用,用于支持用户查看并调整生产计划。它的(略显粗糙的)界面长得像下面这张图所示的这样。 ![](https://box.kancloud.cn/04021d31e319098e8a4cee94e0cb26a0_566x360.jpeg) 每个行省(province)都有一份生产计划,计划中包含需求量(demand)和采购价格(price)。每个行省都有一些生产商(producer),他们各自以不同的成本价(cost)供应一定数量的产品。界面上还会显示,当商家售出所有的商品时,他们可以获得的总收入(full revenue)。页面底部展示了该区域的产品缺额(需求量减去总产量)和总利润(profit)。用户可以在界面上修改需求量及采购价格,以及不同生产商的产量(production)和成本价,以观察缺额和总利润的变化。用户在界面上修改任何数值时,其他的数值都会同时得到更新。 这里我展示了一个用户界面,是为了让你了解该应用的使用方式,但我只会聚焦于软件的业务逻辑部分,也就是那些计算利润和缺额的类,而非那些生成HTML或监听页面字段更新的代码。本章只是先带你走进自测试代码世界的大门,因而最好是从最简单的例子开始,也就是那些不涉及用户界面、持久化或外部服务交互的代码。这种隔离的思路其实在任何场景下都适用:一旦业务逻辑的部分开始变复杂,我就会把它与UI分离开,以便能更好地理解和测试它。 这块业务逻辑代码涉及两个类:一个代表了单个生产商(`Producer`),另一个用来描述一个行省(`Province`)。`Province`类的构造函数接收一个JavaScript对象,这个对象的内容我们可以想象是由一个JSON文件提供的。 下面的代码能从JSON文件中构造出一个行省对象。 ##### class Province... ``` constructor(doc) {  this._name = doc.name;  this._producers = [];  this._totalProduction = 0;  this._demand = doc.demand;  this._price = doc.price;  doc.producers.forEach(d => this.addProducer(new Producer(this, d))); } addProducer(arg) {  this._producers.push(arg);  this._totalProduction += arg.production; } ``` 下面的函数会创建可用的JSON数据,我可以用它的返回值来构造一个行省对象,并拿这个对象来做测试。 ##### 顶层作用域... ``` function sampleProvinceData() {  return {   name: "Asia",   producers: [    {name: "Byzantium", cost: 10, production: 9},    {name: "Attalia", cost: 12, production: 10},    {name: "Sinope", cost: 10, production: 6},   ],   demand: 30,   price: 20  }; } ``` 行省类中有许多设值函数和取值函数,它们用于获取各类数据的值。 ##### class Province... ``` get name() {return this._name;} get producers() {return this._producers.slice();} get totalProduction() {return this._totalProduction;} set totalProduction(arg) {this._totalProduction = arg;} get demand() {return this._demand;} set demand(arg) {this._demand = parseInt(arg);} get price() {return this._price;} set price(arg) {this._price = parseInt(arg);} ``` 设值函数会被UI端调用,接收一个包含数值的字符串。我需要将它们转换成数值,以便在后续的计算中使用。 代表生产商的`Producer`类则基本只是一个存放数据的容器。 ##### class Producer... ``` constructor(aProvince, data) {  this._province = aProvince;  this._cost = data.cost;  this._name = data.name;  this._production = data.production || 0; } get name() {return this._name;} get cost() {return this._cost;} set cost(arg) {this._cost = parseInt(arg);} get production() {return this._production;} set production(amountStr) {  const amount = parseInt(amountStr);  const newProduction = Number.isNaN(amount) ? 0 : amount;  this._province.totalProduction += newProduction - this._production;  this._production = newProduction; } ``` 在设值函数`production`中更新派生数据的方式有点丑陋,每当看到这种代码,我便想通过重构帮它改头换面。但在重构之前,我必须记得先为它添加测试。 缺额的计算逻辑也很简单。 ##### class Province... ``` get shortfall() {  return this._demand - this.totalProduction; } ``` 计算利润的逻辑则要相对复杂一些。 ##### class Province... ``` get profit() {  return this.demandValue - this.demandCost; } get demandCost() {  let remainingDemand = this.demand;  let result = 0;  this.producers   .sort((a,b) => a.cost - b.cost)   .forEach(p => {    const contribution = Math.min(remainingDemand, p.production);     remainingDemand -= contribution;     result += contribution * p.cost;   });  return result; } get demandValue() {  return this.satisfiedDemand * this.price; } get satisfiedDemand() {  return Math.min(this._demand, this.totalProduction); } ```