TypeScript,在可读性方面进行多重比较排序的最佳实践



我有以下排序功能:

orderForReports = (a: KeyValue<number, ProjectReport>, b: KeyValue<number, ProjectReport>): number => {
return a.value.project.order.customer.name.toLowerCase() > b.value.project.order.customer.name.toLowerCase() ? 1 :
b.value.project.order.customer.name.toLowerCase() > a.value.project.order.customer.name.toLowerCase() ? -1 :
a.value.project.order.name.toLowerCase() > b.value.project.order.name.toLowerCase() ? 1 :
b.value.project.order.name.toLowerCase() > a.value.project.order.name.toLowerCase() ? -1 :
a.value.project.name.toLowerCase() > b.value.project.name.toLowerCase() ? 1 :
b.value.project.name.toLowerCase() > a.value.project.name.toLowerCase() ? -1 : 0;
};

首先我按customer.name订购。如果customer.name相等,则按order.name排序。如果order.name相等,我最终按project.name排序。所以我有多达 3 个级别的比较。

代码都工作正常,并且可能尽可能高效,但正如您所看到的,可读性存在问题。我想知道,在 TypeScript 中进行多重比较排序是否有任何最佳实践?有没有其他排序方法更易读、更容易理解?

可读性是主观的,但可以想象改进事物的一种方法是制作一些辅助函数/类/等,将检查对象片段的函数转换为比较对象的函数,并且可以组合多个这些函数以形成单个比较函数。 这些帮助程序函数的实现可能不那么可读,但它们可以存储在某个位置的库中。 目标是使此类函数的使用具有可读性。

下面是一个示例。 假设我有一个Person接口和该接口的实例数组:

interface Person {
surname: string;
givenName: string;
}
const people: Person[] = [
{ givenName: "John", surname: "Smith" },
{ givenName: "Jane", surname: "Smith" },
{ givenName: "June", surname: "Smith" },
{ givenName: "John", surname: "Doe" },
{ givenName: "Jane", surname: "Doe" },
{ givenName: "June", surname: "Doe" },
]
console.log(people.map(p => p.givenName + " " + p.surname).join(", "));
// John Smith, Jane Smith, June Smith, John Doe, Jane Doe, June Doe

我想先按姓氏排序,然后按名字排序,两者都不区分大小写(和不区分区域设置(。 如果我能写这个不是很好吗?

people.sort(compareOn(p => p.surname.toLowerCase(), p => p.givenName.toLowerCase()));
console.log(people.map(p => p.givenName + " " + p.surname).join(", "));
// Jane Doe, John Doe, June Doe, Jane Smith, John Smith, June Smith

也许我什至想按姓氏升序排序,然后按降序排列名字(谁知道我为什么要用名字这样做;也许我并不完全理智(。 也许我可以像这样修改代码:

people.sort(compareOn(
p => p.surname.toLowerCase(),
{ fn: p => p.givenName.toLowerCase(), dir: "desc" }
));
console.log(people.map(p => p.givenName + " " + p.surname).join(", "));
// June Doe, John Doe, Jane Doe, June Smith, John Smith, Jane Smith

好吧,我们可以通过适当的实现来做这两件事compareOn(). 这是一种可能性:

type ToOrderable<T> = ((x: T) => string | number | boolean) | {
fn: ((x: T) => string | number | boolean);
dir: "asc" | "desc"
}
const compareOn =
<T>(...toOrderables: Array<ToOrderable<T>>): (x: T, y: T) => number =>
toOrderables.map(
o => (x: T, y: T) => {
const [sgn, f] = "fn" in o ? [o.dir === "desc" ? -1 : 1, o.fn] : [1, o];
const fx = f(x);
const fy = f(y);
return sgn * (fx < fy ? -1 : fy < fx ? 1 : 0);
}
).reduce(
(a, f) => (x: T, y: T) => a(x, y) || f(x, y)
);

compareOn()为某些类型T采用可变数量的ToOrderable<T>值。ToOrderable<T>要么是将T转换为string | number | boolean的函数(这是JavaScript实际上可以与<进行比较的函数(,要么是一个包含该类型的属性fn以及"asc""desc"dir属性的对象。

它的实现在这里并不是特别重要;只要它有效,你可以随心所欲地编写它。 我所做的是获取toOrderables数组,map每个元素安装到返回-1 | 0 | 1的比较函数中,然后将这些元素reduce到单个函数中。a(x, y) || f(x, y)的缩减步骤只是说"a现有的比较函数并比较a(x, y)。 如果是-11,那就是答案。 否则,请查阅下一个函数f并返回f(x, y)


无论如何,希望这能为如何进行提供另一种想法。 祝你好运!

操场链接到代码

在几乎所有情况下,"可读性"和"条件运算符"都是不一致的。在某些情况下,条件运算符对于查看源代码的人来说更短且更容易处理,但在绝大多数情况下,if语句更好。因此,重构的第一步是将其转换为正常的if语句:

orderForReports = (a: KeyValue <number, ProjectReport>, b: KeyValue <number, ProjectReport>): number => {
if (a.value.project.order.customer.name.toLowerCase() > b.value.project.order.customer.name.toLowerCase())
return 1;
if (b.value.project.order.customer.name.toLowerCase() > a.value.project.order.customer.name.toLowerCase())
return -1;
if (a.value.project.order.name.toLowerCase() > b.value.project.order.name.toLowerCase())
return 1;
if (b.value.project.order.name.toLowerCase() > a.value.project.order.name.toLowerCase())
return -1
if (a.value.project.name.toLowerCase() > b.value.project.name.toLowerCase())
return 1;
if (b.value.project.name.toLowerCase() > a.value.project.name.toLowerCase())
return -1;
return 0;
};

我们不需要else部分,因为if的每个主体只是一个return,所以代码不会继续向下。我们可以放下身体周围的{},因为它无论如何都是一条线。它确实使阅读更容易,而不是一半的行只是一个右括号。

尽管如此,这现在突出了另一个问题 - 每次都有两次比较 - 一个用于a > b另一个用于b > a.所有这些toLowerCase()调用使行更长,因此更难阅读。相反,您可以使用具有以下好处的String#localeCompare

  • 它返回-101,所以你不必调用它两次
  • 您可以指定不区分大小写的比较,而不是每行添加两次.toLowerCase()

因此,如果我们只使用它,我们可以使代码更短。我们还可以添加提取变量重构,使其更易于阅读:

orderForReports = (a: KeyValue <number, ProjectReport>, b: KeyValue <number, ProjectReport>): number => {
const comparisonOptions = {sensitivity: "accent"};
const customerNameOrder = a.value.project.order.customer.name
.localeCompare         (b.value.project.order.customer.name, undefined, comparisonOptions);
if (customerNameOrder !== 0)
return customerNameOrder;
const orderNameOrder = a.value.project.order.name
.localeCompare      (b.value.project.order.name, undefined, comparisonOptions);
if (orderNameOrder !== 0)
return orderNameOrder;
const projectNameOrder = a.value.project.name
.localeCompare        (b.value.project.name, undefined, comparisonOptions);
if (projectNameOrder !== 0)
return projectNameOrder;
return 0;
};

现在,只有 3 例,而不是 6 例。我们已经将每个比较提取到一个变量中,变量名称告诉我们要比较的内容。该代码是自我记录的,因此它比扫描整个表达式以找出要比较的内容要容易得多。像用空格填充这样绝对简单的格式使我们能够一目了然地验证比较是否正确,因为这两个表达式彼此相邻。如果有人犯了错误并写了

const orderNameOrder = a.value.project.order.name
.localeCompare      (b.value.project.name, undefined, comparisonOptions); 

很明显有问题。

最新更新