我们在程序中编写了如此之多的函数,以至于它们不知不觉就成了我们的第二天性。就像蚁群中的蚂蚁一样,它们的数量之多超出想象,它们聚集在一起形成了一些令人惊讶的复杂系统。
它回避了一个问题:我们如何编写好的函数?这可能看起来微不足道:毕竟它们就像蚂蚁一样。但答案中有杠杆作用:正确的决策会在整个代码库中成倍增加,并形成伟大的设计。
我认为有三个关键的想法可以用来制作良好的功能。我想和你分享它们。
让我们从一个例子开始。我们有一个应用程序,我们想以JSON格式导出一些数据。下面是它的函数可能是什么样子:
函数exportFile(){setLoding(True);try{const data=getData();//[data,data,data]const exportableData=toExportableData(Data);//exportableData const jsonStr=JSON.stringify(ExportableData);//';{";data";:{...。Const fileURL=saveFile(";export.json";,jsonStr);//https://foo.com/export.json设置文件URL(FileURL);}最终{setLoadding(False)}}。
看起来很简单:要导出为JSON,我们首先要获得数据。现在,此数据可能包含一些敏感信息,因此我们将其清理并转换为可导出的数据;ExportableData。一旦我们有了它,我们就得到一个字符串表示,保存文件,然后Badabing,Badboom,我们就完成了。
但是生活还在继续,我们的项目需要发展。不只是导出JSON,我们还需要做更多的工作:我们还需要导出CSV文件。
我们注意到的第一件事是,导出CSV与导出JSON非常相似。我们可以抽象exportFile吗?
我们可以做的一件事是引入一个新标志:类似于exportFile(/*isCSV=*/true)。
函数导出文件(IsCSV){...。Let fileURL if(IsCSV){const csvStr=toCSVStr(ExportableData)fileURL=saveFile(";export.csv";,jsonStr);}Else{const jsonStr=JSON.stringify(ExportableData);fileURL=saveFile(";export.json";,jsonStr);}...。
通过引入该标志,我们可以有条件地生成不同的fileURL:一个用于CSV,一个用于JSON。这样,我们就可以看到抽象的第一个概念:配置。您传递一些配置,然后将其留给您的函数来决定要做什么。
通过配置,调用者可以做的事情受到限制:他们只能提供标志。所有真正的逻辑都保存在exportFile中。这意味着函数的调用者不能疯狂地做一些不受支持的事情。这可能会给我们带来一些心灵的安宁。
这会行得通的,但让我们考虑一下。首先,请注意,为了现在理解exportFile,我们需要同时理解CSV和JSON案例。想象一下,如果有人打开exportFile来了解它的功能:如果他们只关心JSON,那么他们现在必须理解比他们需要的更多的逻辑。任何更改CSV逻辑的人都可能最终破坏JSON。**exportFile**已经完成。
还要注意,因为该函数的调用方只能提供标志,所以他们对您不支持的用例束手束脚。这应该会让你安心,但它肯定会让打电话的人感到沮丧。想象一下,如果他们想要支持XML,他们能做什么呢?他们必须编辑exportFile才能支持这种情况。(但愿他们不会把它编辑成类似于exportFile(isCSV,isXML)的东西--现在您手中有了不变的条件)。通过如此具体,您已经选择使您的函数不那么抽象-这当然意味着它不那么强大。**exportFile**难以扩展。
如果您设想一个排序的功率谱,其中调用者在左侧的功率最小,而在右侧的功率最大,则配置将在左侧。您非常严格地控制调用者的操作,这会给您带来确定性,但会使您的函数变得更复杂,用处更小。
假设您想要解决问题,并移到此频谱的右侧,您可以做什么?
如果你看看我们写的东西,我们会注意到唯一真正不同的部分,就是获取exportData并创建一个fileURL。
..。Const exportableData=toExportableData(Data);//ExportableData...//*可以不同!我们需要以某种方式获取fileURL*setFileURL(FileURL);...。
因此,我们可以做的一件事是:我们可以提供一个函数,而不是提供一个标志:
函数exportFile(ExportableDataToFileURL){setLoding(True);try{const data=getData();//[data,data,data]const exportableData=toExportableData(Data);//ExportableData const fileURL=exportableDataToFileURL(ExportableData)setFileURL(FileURL);}Finally{setLoding(False)}}。
有了这个,我们就解决了我们在配置方面遇到的两个问题。现在,如果有人查看exportFile,他们不会看到关于CSV的无关代码。如果他们想要扩展到XML,他们可以简单地提供不同的功能。我们已经给了来电者更多的权力。
我们已经进一步抽象了,但这是有代价的。首先,我们认为我们知道我们真正需要向外传递的是exportableData,而我们需要返回的是fileURL。如果我们错了呢?例如,有些可能需要稍微不同的数据格式-他们需要一些OtherKindOfExportableData而不是exportableData。当我们弄清楚这一点时,可能会有许多exportFile的新用法,我们在发展此功能时将不得不支持这些新用法。
我们本可以避免这种情况的一种方法是坚持使用配置。这样,任何想要支持某些东西的人都必须通过这个函数,这会让我们有时间思考最好的抽象是什么。
另一种方式是,如果该函数被进一步抽象,这样调用者就可以很容易地支持某些OtherKindOfExportableData。
反转比配置更强大,但它不是最强大的方法。这可能是一个很好的选择,但您可能会冒着过于强大而暴露错误的风险,或者不够强大并限制调用者的风险。
我们知道不太强大的选项:配置。最强大的会是什么样子呢?
我们可能会注意到的下一件事是,我们的exportFile函数实际上构建了一些构建块,这些构建块可能对许多不同的事情有用。例如,许多函数可能需要加载状态,或者只需要获取exportableData等。我们可以创建这些构建块:
我们刚刚建造的积木可以有多种用途。用户可以支持CSV、XML,可以将isLoding与其他一些功能结合使用,还可以选择提供不同类型的exportableData。我们已经为用户提供了很大的动力。
然而,缺点是,就像在倒置的情况下一样,我们会让自己容易犯很多错误。如果isLoding实际上是针对文件的,而其他事情应该使用不同的标志,那该怎么办呢?如果人们开始使用saveJSONFile,并且传递的数据实际上不是导出,该怎么办呢?这些都是我们在抽象中隐含允许的情况。
还有一个问题:注意,在我们的第一个exportFile示例中,代码更加具体:您可以看到实际发生的情况。当代码更抽象时,就更难推断实际发生了什么。现在,获得的功率可能是值得的,但是如果您过早地进行了优化,那么您只是徒劳地付出了这个代价。这种不必要的代价的一个例子是saveJSONFile和saveCSVFile--如果我们内联了它们,总体结构仍然是抽象的,但是更容易理解。当您在这个级别抽象时,这些都是需要注意的事情。
有了这些,我们看到构图给了我们最大的力量,但也给了我们最多的机会去搬起石头砸自己的脚。不过,孩子们,这是值得的。
有趣的是,注意到每一种选择,支持者就是反对者。那我们该怎么选呢?我认为你可以使用的一个启发式方法是:选择你的信心所能限制的最有力的选项。例如,如果您对问题的了解较少,请停留在抽象光谱的较低端。随着您理解的更多(比如,是时候介绍XML了),您可以发展到更强大的一面。当您非常自信,并且您可以看到构建块的良好用例时,请选择范围中最强大的一侧。