干净的代码冒险
这篇文章的想法是从一个没有干净代码原则的函数开始,用干净清晰的方式重写它。 整个过程将由测试驱动。
目的是验证可用于重构旧代码库并提高其质量的作案手法。
我们将创建一组函数,将原始函数算法分解为不同抽象级别,从旧代码中汲取灵感并尽可能重用它。
此外,我们将跟踪每个新引入的特性覆盖了哪些旧代码行,以确保我们实现了所有旧特性。
我们从我在一篇旧文章中描述的一个功能开始:自然排序的高性能比较算法。
这是旧代码:
const natOrdCmp2 = (a,b) => { let i for (i=0; i<a.length; i++) { const ai = a.charCodeAt(i) const bi = b.charCodeAt(i) if (!bi) return 1 if (isDigit(ai) && isDigit(bi)) { const k = skipDigit(a,i) const m = skipDigit(b,i) if (k > m) return 1 if (k < m) return -1 // Same number of digits! Compare them for (let j=i; j < k; j++) { const aj = a.charCodeAt(j) const bj = b.charCodeAt(j) if (aj < bj) return -1 if (aj > bj) return 1 } // Same number! Update the number of compared chars i = k - 1 } else { // Compare alphabetic chars. if (ai > bi) return 1 if (ai < bi) return -1 } } return b.charCodeAt(i) ? -1 : 0}是的,它是模糊的。 我们掌握在手中的权力的阴暗面。
是的,该代码不适用于包含前导零的数字部分。 但是很容易修改它来处理它们。 并且在干净的代码重构之后会更容易!
我理想的高级功能如下:
const natural_sort_comparison = (a: string, b: string) => compare_chars_or_numbers_of( a, with_corresponding_char_or_number_of(b) )这与维基百科中自然排序的定义非常接近:
在计算中,自然排序顺序(或自然排序)是按字母顺序对字符串进行排序,除了多位数字被原子处理,即,就好像它们是单个字符一样。
它准确地反映了算法的本质,但包含一个技术难点:它需要在另一个有效执行任务的函数中使用一个关闭参数 b 的函数。
可以说闭包的使用使代码晦涩难懂。 所以我会选择一个更简单、更不优雅的版本:
const natural_sort_comparison = (a: string, b: string) => compare_chars_or_numbers(a, b)但我的最终解决方案将使用我的第一个想法,因为我不认为优雅和复杂会影响清晰度。
下面我将使用这三个常量:
const EQUAL = 0const SMALLER = -1const BIGGER = 1我们最初的一组测试将验证循环对字符串 a 的字符的正确“移植”。 为了这个简单的目的,测试字符串只包含字母字符,并且每个字符串不是另一个字符串的前缀。
test('Basic string comparisons', () => { expect(natural_sort_comparison('abc', 'bc')).toBe(SMALLER) expect(natural_sort_comparison('bc', 'abc')).toBe(BIGGER) expect(natural_sort_comparison( 'abcde', 'abcde')).toBe(EQUAL)))我们覆盖了原始代码的第 2、3、4、5、24、25 行和部分第 28 行。
代码是这样的:
const compare_chars_or_numbers = (a, b) => { let charIndex = 0 while ( charIndex < a.lenght ) { const comparisonResult = compare_one_char_or_number(a, b, charIndex) if (comparisonResult !== EQUAL) return comparisonResult charIndex++ } return EQUAL}const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) return aCode - bCode}我已经用 while 替换了 for 循环,因为在最终代码中,我想将第 3 行中的 for 增量和第 21 行中的增量合并到一条指令中。
下一步是重新组织前缀的解决方案。
这些是额外的测试:
test('Strings that are prefixed by the other', () => { expect(natural_sort_comparison('abc', 'abcd')).toBe(SMALLER) expect(natural_sort_comparison('abcd', 'abc')).toBe(BIGGER)))第 6 行和第 28 行的检查应该插入到 compare_one_char_or_number 函数中,但我们需要找到一种方法将第 28 行的测试包含在元素的循环中:
const compare_chars_or_numbers = (a, b) => { let charIndex = 0 const maxComparisonChars = a.length + OneMoreToCheckIfAisPrefixOfB while ( charIndex < maxComparisonChars ) { const comparisonResult = compare_one_char_or_number(a, b, charIndex) if (comparisonResult !== EQUAL) return comparisonResult charIndex++ } return EQUAL}const OneMoreToCheckIfAisPrefixOfB = 1现在该函数必须处理可能超过字符串大小的 charIndex。 这不是一个大问题,因为在这种情况下 chartAt(i) 返回 NaN。 还要注意,现在比较相等的字符串,我们最终会得到一个超过两个字符串大小的 charIndex。
const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) return compare_char_codes(aCode, bCode)}const compare_char_codes = (aCode, bCode) => { if (are_strings_equal(aCode, bCode)) return EQUAL if (is_the_string_prefix_of_the_other(aCode)) return SMALLER if (is_the_string_prefix_of_the_other(bCode)) return BIGGER return aCode - bCode}const is_the_string_prefix_of_the_other = charCode => isNaN(charCode)const are_strings_equal = (aCode, bCode) => isNaN(aCode) && isNaN(bCode)compare_one_char_or_number 的这个改进版本涵盖了第 5 行和第 28 行的其余部分。
下一步是处理带有数字部分的字符串。 第一个考虑是 compare_one_character_or_one_number 现在可以一次比较多个字符(一个数字可以由多个数字组成)。在相等子字符串的情况下,比较字符的数量用于递增 compare_chars_or_numbers 中的循环计数器,因此 函数必须向调用者返回两个值。在这些情况下(广泛用于 React 钩子)的一个众所周知的习惯是返回一个向量并使用 ES6 语法来提取值:
const compare_chars_or_numbers = (a, b) => { let charIndex = 0 const maxComparisonChars = a.length + OneMoreToCheckIfAisPrefixOfB while ( charIndex < maxComparisonChars ) { const [comparisonResult, comparedChars] = compare_one_char_or_number(a, b, charIndex) if (comparisonResult !== EQUAL) return comparisonResult charIndex += comparedChars } return EQUAL}const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) const comparedChars = 1 return [compare_char_codes(aCode, bCode), comparedChars]}请注意,charIndex 会递增,直到两个子字符串相等,但当它们变得不同时,不使用 compareChars。
重新运行单元测试可确保更改不会引入损坏。
现在我们准备好为带有数字部分的字符串定义测试。 我们从长度为 1 的数字部分开始。
test('Strings that are prefixed by the other', () => { expect(natural_sort_comparison('ab2c1', 'ab2c2')).toBe(SMALLER) expect(natural_sort_comparison('ab2c2', 'ab2c1')).toBe(BIGGER)))函数 compare_one_char_or_number 应该区分标准字符和数字序列:
const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) if (is_digit(aCode) && is_digit(bCode)) { return compare_digits(a, b, charIndex) } const comparedChars = 1 return [compare_char_codes(aCode, bCode), comparedChars]}const compare_digits = (a, b, charIndex) => { return [a - b, 1]}const is_digit = charCode => charCode>=48 && charCode<=57请注意,is_digit(NaN) 为 false,因此前缀和相等字符串的处理仅发生在 compare_char_codes 内部。
compare_one_char_or_number 中的两个返回语句有一个问题:我们混合了两个不同级别的抽象:谁负责返回两个函数值? 我们无法从 compare_digits 中提取此责任,因此即使在字母字符的情况下我们也将其委派。
const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) if (is_digit(aCode) && is_digit(bCode)) { return compare_digits(a, b, charIndex) } return compare_one_char_code_pair(aCode, bCode)}const compare_one_char_code_pair = (aCode, bCode) => { const comparedChars = 1 return [compare_char_codes(aCode, bCode), comparedChars]}compare_one_char_or_number 的最终版本也涵盖了旧代码的第 7 行。
下一步是比较不同长度的数量。
test('Strings that are prefixed by the other', () => { expect(natural_sort_comparison('abc2', 'abc11')).toBe(SMALLER) expect(natural_sort_comparison('abc111', 'abc21')).toBe(BIGGER)))位数越多的数字越大。 因此,计算两个字符串中连续数字的数量并比较计数器就足够了。
const compare_digits = (a, b, charIndex) => { const aDigits = number_of_consecutive_digits(a, charIndex) const bDigits = number_of_consecutive_digits(b, charIndex) if (aDigits > bDigits) return [BIGGER] if (aDigits < bDigits) return [SMALLER] // cannot be here for the moment}const number_of_consecutive_digits = (str, startIndex) => { let lastIndex for (lastIndex=startIndex+1; lastIndex<str.length; lastIndex++) if (!is_digit(str.charCodeAt(lastIndex))) return lastIndex - startIndex return lastIndex - startIndex}好的,现在是最后一步:使用具有相同长度的数字部分的字符串进行测试。
test('Strings that are prefixed by the other', () => { expect(natural_sort_comparison('abc12', 'abc12')).toBe(EQUAL) expect(natural_sort_comparison('abc11', 'abc12')).toBe(SMALLER) expect(natural_sort_comparison('abc13', 'abc12')).toBe(BIGGER)))如果两个数字部分的长度相同,只需检查其数字的字符代码顺序。
const compare_digits = (a, b, charIndex) => { const aDigits = number_of_consecutive_digits(a, charIndex) const bDigits = number_of_consecutive_digits(b, charIndex) if (aDigits > bDigits) return [BIGGER] if (aDigits < bDigits) return [SMALLER] return compare_equal_length_numbers(a, b, charIndex, aDigits)}const compare_equal_length_numbers = (a, b, startIndex, numberOfDigits) => { for (let charIndex = startIndex; charIndex < startIndex + numberOfDigits; charIndex++) { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) if (aCode < bCode) return [SMALLER] if (aCode > bCode) return [BIGGER] } return [EQUAL, numberOfDigits]}函数 compare_equal_length_numbers 覆盖旧代码的第 13-18 行,而 compare_digits 覆盖第 8-11 行
就这些。 但在呈现整个重构代码之前,请记住我在开始时所说的……我更喜欢优雅和复杂的代码。 毕竟,闭包是语言的一部分……
const natural_sort_comparison = (a: string, b: string) => compare_chars_or_numbers_of( a, with_corresponding_char_or_number_of(b) )const compare_chars_or_numbers_of = (a, compare_with_b) => { let charIndex = 0 const maxComparisonChars = a.length + OneMoreToCheckIfAisPrefixOfB while ( charIndex < maxComparisonChars ) { const [comparisonResult, comparedChars] = compare_with_b(a, charIndex) if (comparisonResult !== EQUAL) return comparisonResult charIndex += comparedChars } return EQUAL}const with_corresponding_char_or_number_of = b => { const compare_with_b = (a, charIndex) => compare_one_char_or_number(a, b, charIndex) return compare_with_b}我只更改了主函数的名称和签名,并在其主体中更改了对 compare_with_b 的调用:一个在 b 上关闭的函数,它使用所有需要的参数调用 compare_one_char_or_number。
一个黑暗的片段在干净的代码天空中幸存下来,以允许对顶级函数进行富有表现力的调用。
const EQUAL = 0const SMALLER = -1const BIGGER = 1const OneMoreToCheckIfAisPrefixOfB = 1const natural_sort_comparison = (a: string, b: string) => compare_chars_or_numbers_of(a, with_corresponding_char_or_number_of(b))const compare_chars_or_numbers_of = (a, compare_with_b) => { let charIndex = 0 const maxComparisonChars = a.length + OneMoreToCheckIfAisPrefixOfB while ( charIndex < maxComparisonChars ) { const [comparisonResult, comparedChars] = compare_with_b(a, charIndex) if (comparisonResult !== EQUAL) return comparisonResult charIndex += comparedChars } return EQUAL}const with_corresponding_char_or_number_of = b => { const compare_with_b = (a, charIndex) => compare_one_char_or_number(a, b, charIndex) return compare_with_b}const compare_one_char_or_number = (a, b, charIndex) => { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) if (is_digit(aCode) && is_digit(bCode)) { return compare_digits(a, b, charIndex) } return compare_one_char_code_pair(aCode, bCode)}const is_digit = charCode => charCode>=48 && charCode<=57const compare_one_char_code_pair = (aCode, bCode) => { const comparedChars = 1 return [compare_char_codes(aCode, bCode), comparedChars]}const compare_char_codes = (aCode, bCode) => { if (are_strings_equal(aCode, bCode)) return EQUAL if (is_the_string_prefix_of_the_other(aCode)) return SMALLER if (is_the_string_prefix_of_the_other(bCode)) return BIGGER return aCode - bCode}const is_the_string_prefix_of_the_other = charCode => isNaN(charCode)const are_strings_equal = (aCode, bCode) => isNaN(aCode) && isNaN(bCode)const compare_digits = (a, b, charIndex) => { const aDigits = number_of_consecutive_digits(a, charIndex) const bDigits = number_of_consecutive_digits(b, charIndex) if (aDigits > bDigits) return [BIGGER] if (aDigits < bDigits) return [SMALLER] return compare_equal_length_numbers(a, b, charIndex, aDigits)}const number_of_consecutive_digits = (str, startIndex) => { let lastIndex for (lastIndex=startIndex+1; lastIndex<str.length; lastIndex++) if (!is_digit(str.charCodeAt(lastIndex))) return lastIndex - startIndex return lastIndex - startIndex}const compare_equal_length_numbers = (a, b, startIndex, numberOfDigits) => { for (let charIndex = startIndex; charIndex < startIndex + numberOfDigits; charIndex++) { const aCode = a.charCodeAt(charIndex) const bCode = b.charCodeAt(charIndex) if (aCode < bCode) return [SMALLER] if (aCode > bCode) return [BIGGER] } return [EQUAL, numberOfDigits]}export default natural_sort_comparison好吧,代码行数增加了一倍(还考虑到旧列表中未包含的 skipDigit 函数),但我们添加了很多信息,现在代码可以作为小说阅读。
我很好奇你对这个实验的看法,尤其是黑暗片段。 请随时发表评论。
关注七爪网,获取更多APP/小程序/网站源码资源!
原文地址:https://m.toutiao.com/i7128031193689391631/ |