重构。从6到180行。
#javascript #编程 #重构 #refactorit

嗨!

在本文中,我将尝试展示解决方案对一个不是超级艰巨的任务的不同,并且还想向您展示一些技巧(也许是hacky,但它在JS而不是TS;)中使用reduce函数。

首先,让我们看一下任务,一个超长,有点疯狂的解决方案,我尝试

初始任务(书店)

像往常一样 - 这是来自可爱且有用的exercism.org门户的任务,并且有一个指令:

要尝试鼓励流行5本书系列的不同书籍的更多销售,一家书店决定为多本书购买提供折扣。
五本书中的任何一本副本的价格为$8。
但是,如果您购买了两本不同的书,那么这两本书就可以享受5%的折扣。
如果您购买了3本不同的书,则获得10%的折扣。
如果您购买了4本不同的书,则获得20%的折扣。
如果您购买了全部5,则获得25%的折扣。
...其余的说明是here
目标 - 找到更好的客户折扣取决于他或她正在购买的书籍。

BookDispenser类的宽松解决方案

因此,在这个解决方案中,我尝试将重点放在下降算法上,而是使用BookDispenser Machine
使用OO方法

const getDiscountMultiplier = (discountInPercent) => 100 - discountInPercent;

const ONE_COPY_COST = 8;
const TWO_COPIES_DISCOUNT_PERCENT = 5;
const TWO_COPIES_DISCOUNT = getDiscountMultiplier(TWO_COPIES_DISCOUNT_PERCENT);

const TWO_COPIES_COST = 2 * ONE_COPY_COST * TWO_COPIES_DISCOUNT;
const THREE_COPIES_DISCOUNT_PERCENT = 10;
const THREE_COPIES_DISCOUNT = getDiscountMultiplier(THREE_COPIES_DISCOUNT_PERCENT);

const THREE_COPIES_COST = 3 * ONE_COPY_COST * THREE_COPIES_DISCOUNT;
const FOUR_COPIES_DISCOUNT_PERCENT = 20;
const FOUR_COPIES_DISCOUNT = getDiscountMultiplier(FOUR_COPIES_DISCOUNT_PERCENT);

const FOUR_COPIES_COST = 4 * ONE_COPY_COST * FOUR_COPIES_DISCOUNT;
const FIVE_COPIES_DISCOUNT_PERCENT = 25;
const FIVE_COPIES_DISCOUNT = getDiscountMultiplier(FIVE_COPIES_DISCOUNT_PERCENT);

const FIVE_COPIES_COST = 5 * ONE_COPY_COST * FIVE_COPIES_DISCOUNT;
const bestPriceForOneBook = Math.min(1 * ONE_COPY_COST * 100);
const bestPriceForTwoBooks = Math.min(
  2 * bestPriceForOneBook,
  1 * TWO_COPIES_COST
)
const bestPriceForThreeBooks = Math.min(
  3 * bestPriceForOneBook,
  1 * bestPriceForTwoBooks + 1 * bestPriceForOneBook,
  1 * THREE_COPIES_COST
)
const bestPriceForFourBooks = Math.min(
  4 * bestPriceForOneBook,
  2 * bestPriceForTwoBooks,
  1 * bestPriceForThreeBooks + 1 * bestPriceForOneBook,
  1 * FOUR_COPIES_COST
)
const bestPriceForFiveBooks = Math.min(
  5 * bestPriceForOneBook,
  2 * bestPriceForTwoBooks + 1 * bestPriceForThreeBooks,
  1 * bestPriceForThreeBooks + 1 * bestPriceForTwoBooks,
  1 * bestPriceForFourBooks + 1 * bestPriceForOneBook,
  1 * FIVE_COPIES_COST
)
//TODO: try to make it countable itself
const bestPricePerBookAmount = new Map([
  [1, bestPriceForOneBook],
  [2, bestPriceForTwoBooks],
  [3, bestPriceForThreeBooks],
  [4, bestPriceForFourBooks],
  [5, bestPriceForFiveBooks],
])
class BookDispenser {
  _booksByCount;
  _dispenserOrder;
  constructor(books) {
    this._booksByCount = BookDispenser._getBooksByCount(books);
    this._dispenserOrder = BookDispenser._countDispenserOrder(this._booksByCount);
  }
  _booksLeft() {
    let stillBooksLeft = false;
    for (let book = 1; book <= 5; book++) {
      const booksLeft = this._booksByCount.get(book);
      if (booksLeft > 0) {
        stillBooksLeft = true;
        break;
      }
    }
    return stillBooksLeft;
  }

  dispense(numberOfBooks) {
    let numberOfDispersedBooks = 0;
    for (let index = 0; index < this._dispenserOrder.length; index++) {
      const bookToDispense = this._dispenserOrder[index];
      numberOfDispersedBooks += this.dispenseBook(bookToDispense);

      if (numberOfDispersedBooks == numberOfBooks) break;
    }
    return numberOfDispersedBooks;
  }
  dispenseBook(bookNumber) {
    const booksCount = this._booksByCount.get(bookNumber);
    if (booksCount > 0) {
      this._booksByCount.set(bookNumber , booksCount - 1);
      return 1;
    }
    return 0;
  }

  getBatch(booksPerBatch, numberOfDispenses) {
    const booksBatches = [];
    let dispenses = 0;
    let batch;
    while ((!numberOfDispenses || dispenses < numberOfDispenses) &&
           (batch = this.dispense(booksPerBatch))
          ) {
      dispenses++;
      // log({booksPerBatch, batch, dispenses, still: dispenses < numberOfDispenses });
      booksBatches.push(batch);
    }
    return booksBatches;
  }

  getBatchReduced(booksPerBatch,  dispenses = 1) {
    const basketVariants = [];
    const batch = this.getBatch(booksPerBatch, dispenses);
    if (booksPerBatch >= 1) {
      const reducedBatch = this.getBatch(
        booksPerBatch === 1 ? 1 : booksPerBatch - 1
      );
      if (reducedBatch.length > 0) {
        batch.push(...reducedBatch);
      }
    }
    if (batch.length !== 0) {
      basketVariants.push(...batch);
    }
    return basketVariants;
  }
  static _getBooksByCount = (books) => {
    const booksByCount = new Map();
    for (let book = 1; book <= 5; book++) {
      const booksInBasket = books.filter(bookInBasket => bookInBasket === book).length;
      booksByCount.set(book, booksInBasket);
    }

    return booksByCount;
  }
  static _countDispenserOrder(booksByCount) {
    const bookEntries = [];
    for (let entry of booksByCount.entries()) {
      bookEntries.push(entry)
    }
    return bookEntries
      .sort((a, b) => b[1] - a[1] )
      .map(({0:a}) => a)
  }
}

const cost = (books) => {
  if (books.length === 0) return 0;

  const basketVariants = [];
  const addBatchIfNotAlreadyInVariants = (batch) => {
    const isExists = basketVariants.some(basket => {
        return ''+basket === ''+batch;
      })
    if (!isExists) {
      basketVariants.push(batch);
    }
  }

  for (let booksBatch = 5; booksBatch >= 1; booksBatch--) {
    // TODO: fix dispenses; should be calculated, not constant
    for (let dispenses = 1; dispenses < 10; dispenses++) {
      const bookDispenser = new BookDispenser(books);

      const reducedBatch = bookDispenser.getBatchReduced(booksBatch, dispenses);
      addBatchIfNotAlreadyInVariants(reducedBatch);
    }
    const batch = new BookDispenser(books).getBatch(booksBatch);
    addBatchIfNotAlreadyInVariants(batch);
  }

  log({ basketVariants });
  return Math.min(...basketVariants.map(basket => {
    return basket.reduce((sum, b) => sum + bestPricePerBookAmount.get(b), 0);
  }))
};

const log = value => console.log(value);

是的,有点结束,我知道:)
也许在这里可以指出的是地图不是对象的使用,您可以在Mozilla web site上阅读的好处/不同。不同的...

超短解决方案

基于LittleArmstrong的出色超级简洁解决方案!

const cost = (books) => books.reduce((grouped, book) => {
    grouped[book-1] = (grouped?.[book-1] ?? 0) + 1; return grouped;
  }, []).reduce((grouped, amount) => {
    for (let i = 0; i < amount; i++) grouped[i] = (grouped?.[i] ?? 0) + 1; return grouped;
  }, []).map(
    (amount, _ , group)  => (amount === 5 & group.includes(3)) ? (group[group.indexOf(3)] += 1, amount - 1) : amount).map(amount => 800 * amount * (1 - ({2: 0.05, 3: 0.1, 4: 0.2, 5: 0.25}?.[amount] ?? 0))).reduce((acc, cur) => acc += cur, 0);

是的,这几乎是不可读的,我知道:)

结论

在此示例中,我向您展示了两个针对同一任务的极性解决方案,当然,事实介于:)

之间

请注意,reduce功能的使用初始值 累加器。这绝对不是微不足道的,但可能是可能的。
pros ?该代码看起来更功能,并链接。
cons ?在MDN规范中,它看起来只是骇人听闻,很难阅读,并且没有突出显示。

请随时在评论中提供解决方案,甚至在exercism.org网站上更好。

快乐的编程和重构!让我们继续关注!