この記事では、JavaScript を例として、関数を最適化し、明確で読みやすく、より効率的で安定したものにする方法を紹介します。
ソフトウェアの複雑さは増大し続けています。コードの品質は、アプリケーションの信頼性とスケーラビリティを確保するために非常に重要です。
しかし、私を含むほぼすべての開発者は、キャリアの中で低品質のコードを見たことがあるでしょう。これは罠だ。低品質のコードには、次のような非常に有害な特性があります:
関数が非常に長く、あらゆる種類の厄介な関数で埋め尽くされています。
関数には通常、理解するのが難しいだけでなく、デバッグすら不可能な副作用がいくつかあります。
関数と変数のあいまいな名前。
脆弱なコード: 小さな変更により、他のアプリケーション コンポーネントが予期せず破損する可能性があります。
コード カバレッジがありません。
それらは基本的に次のように聞こえます: 「このコードがどのように機能するのか理解できません」、「このコードはただの混乱です」、「このコードを変更するのは難しすぎます」 など。
私はかつてそのような状況に遭遇しました。私の同僚の 1 人は、Ruby ベースの REST API の構築を続けることができなくなったため、退職しました。このプロジェクトは彼が前の開発チームから引き継いだものです。
既存のバグを修正してから、新しいバグを導入し、新しい機能を追加し、一連の新しいバグを追加するなどします (いわゆる脆弱なコード)。顧客はより良い設計でアプリケーション全体をリファクタリングすることを望まなかったので、開発者は現状を維持するという賢明な選択をしました。
わかりました、これはいつも起こります、そしてそれはかなりひどいです。では、何ができるでしょうか?
まず第一に、アプリケーションを実行することとコードの品質を保証することはまったく別のことであることに留意する必要があります。一方で、製品要件を実装する必要があります。しかしその一方で、関数が単純であること、読みやすい変数名と関数名を使用すること、関数の副作用を避けることなどに時間をかけて行う必要があります。
関数 (オブジェクト メソッドを含む) は、アプリケーションを実行するための歯車です。まず、その構造と全体的なレイアウトに注目する必要があります。この記事には、明確で理解しやすく、テストしやすい関数の作成方法を示す優れた例がいくつか含まれています。
多数の関数を含む大きな関数の使用を避け、関数をいくつかの小さな関数に分割します。大規模なブラック ボックス関数は、理解や変更が難しく、特にテストが困難です。
そのようなシナリオを想定すると、配列、マップ、または通常の JavaScript オブジェクトの重みを計算する関数を実装する必要があります。合計の重みは、各メンバーの重みを計算することで取得できます:
null または未定義の変数は 1 ポイントとカウントされます。
基本タイプは2点カウントです。
オブジェクトまたは関数は 4 ポイントを数えます。
たとえば、配列 [null, ‘Hello World’, {}] の重みは次のように計算されます: 1 (null) + 2 (文字列は基本型) + 4 (オブジェクト) = 7。
最悪の例から始めます。すべてのロジックは関数 getCollectionWeight():
でエンコードされますfunction getCollectionWeight(collection) { letcollectionValues; if (collectioninstanceof Array) { collectionValues = collection; } else if (collectioninstanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { if (item == null) { return sum + 1; } if (typeof item === 'object' || typeof item === 'function') { return sum + 4; } return sum + 2; }, 0); } letmyArray = [null, { }, 15]; letmyMap = new Map([ ['functionKey', function() {}] ]); letmyObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
問題は明らかです。getCollectionWeight() 関数は非常に長く、「事故」でいっぱいのブラック ボックスのように見えます。おそらくあなたも、一見しただけでは何をするのか理解できないことに気づいたかもしれません。アプリケーションにそのような関数がたくさんあることをもう一度想像してください。
仕事中にそのようなコードに遭遇すると、時間とエネルギーの無駄です。一方で、高品質なコードは違和感がありません。高品質のコードでは、適切に記述された自己文書化された関数が読みやすく、理解しやすくなります。
さて、私たちの目標は、この巨大な関数を、より小さく独立した再利用可能な関数のセットに分割することです。最初のステップは、タイプに応じて重みを計算するコードを抽出することです。この新しい関数は getWeight() という名前です。
これらの「謎の数字」を見てみましょう: 1、2、4。ストーリー全体の背景が分からない場合、これらの数字だけでは有用な情報は得られません。幸いなことに、ES2015 では静的な読み取り専用参照を定義できるため、いくつかの定数を作成し、それらの「謎の数字」を意味のある名前に置き換えるだけで済みます。 (私は「謎の数字」という表現が特に好きです :D)
新しい小さな関数 getWeightByType() を作成し、それを使って getCollectionWeight() を改善しましょう:
// Code extracted into getWeightByType() function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getCollectionWeight(collection) { letcollectionValues; if (collectioninstanceof Array) { collectionValues = collection; } else if (collectioninstanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } letmyArray = [null, { }, 15]; letmyMap = new Map([ ['functionKey', function() {}] ]); letmyObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
かなり良くなりましたね? getWeightByType() 関数は独立したコンポーネントであり、各タイプの重量値を決定するためにのみ使用されます。再利用可能で、他の機能でも使用できます。
getCollectionWeight() 若干スリムになりました。
WEIGHT_NULL_UNDEFINED、WEIGHT_PRIMITIVE、および WEIGHT_OBJECT_FUNCTION はすべて、自己文書化機能を備えた定数であり、各タイプの重みはその名前によって確認できます。 1、2、4 という数字が何を意味するかを推測する必要はありません。
ただし、このアップグレードされたバージョンにはまだ欠点があります。 Set または他のユーザー定義セットに対して重み計算を実装することを計画しているとします。 getCollectionWeight() には重みを取得するための特定のロジックのセットが含まれているため、すぐに肥大化する可能性があります。
マップの重みを取得するコードを getMapValues() に抽出し、基本的な JavaScript オブジェクトの重みを取得するコードを getPlainObjectValues() に入れましょう。改良版をご覧ください。
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } // Code extracted into getMapValues() function getMapValues(map) { return [...map.values()]; } // Code extracted into getPlainObjectValues() function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionWeight(collection) { letcollectionValues; if (collectioninstanceof Array) { collectionValues = collection; } else if (collectioninstanceof Map) { collectionValues = getMapValues(collection); } else { collectionValues = getPlainObjectValues(collection); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } letmyArray = [null, { }, 15]; letmyMap = new Map([ ['functionKey', function() {}] ]); letmyObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
ここで getCollectionWeight() 関数を見てみると、その仕組みが理解しやすく、興味深いストーリーであることがわかります。
すべての機能はシンプルかつ明確です。コードがどのように機能するかを理解するためにコードを詳しく調べる必要はありません。新しいコードは次のようになります。
このレベルでも、最適化の余地はまだたくさんあります。
if/else ステートメントを使用して、コレクション内の型を区別するスタンドアロン関数 getCollectionValues() を作成できます:
function getCollectionValues(collection) { if (collectioninstanceof Array) { return collection; } if (collectioninstanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); }
そうすれば、getCollectionWeight() は、その唯一の仕事であるため、非常に純粋になるはずです: getCollectionValues() の値コレクションを呼び出してから、順番に加算アキュムレーターを呼び出します。
你也可以创建一个独立的累加器函数:
function reduceWeightSum(sum, item) { return sum + getWeightByType(item); }
理想情况下 getCollectionWeight() 函数中不应该定义函数。
最后,最初的巨型函数,已经被转换为如下一组小函数:
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getMapValues(map) { return [...map.values()]; } function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionValues(collection) { if (collectioninstanceof Array) { return collection; } if (collectioninstanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); } function reduceWeightSum(sum, item) { return sum + getWeightByType(item); } function getCollectionWeight(collection) { return getCollectionValues(collection).reduce(reduceWeightSum, 0); } letmyArray = [null, { }, 15]; letmyMap = new Map([ ['functionKey', function() {}] ]); letmyObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
这就是编写简单精美函数的艺术!
除了这些代码质量上的优化之外,你也得到不少其他的好处:
通过代码自文档,getCollectionWeight() 函数的可读性得到很大提升。
getCollectionWeight() 函数的长度大幅减少。
如果你打算计算其他类型的权重值,getCollectionWeight() 的代码不会再剧烈膨胀了。
这些拆分出来的函数都是低耦合、高可复用的组件,你的同事可能希望将他们导入其他项目中,而你可以轻而易举的实现这个要求。
当函数偶发错误的时候,调用栈会更加详细,因为栈中包含函数的名称,甚至你可以立马发现出错的函数。
这些小函数更简单、易测试,可以达到很高的代码覆盖率。与其穷尽各种场景来测试一个大函数,你可以进行结构化测试,分别测试每一个小函数。
你可以参照 CommonJS 或 ES2015 模块格式,将拆分出的函数创建为独立的模块。这将使得你的项目文件更轻、更结构化。
这些建议可以帮助你,战胜应用的复杂性。
原则上,你的函数不应当超过 20 行——越小越好。
现在,我觉得你可能会问我这样的问题:“我可不想将每一行代码都写为函数。有没有什么准则,告诉我何时应当停止拆分?”。这就是接下来的议题了。
让我们稍微放松一下,思考下应用的定义到底是什么?
每一个应用都需要实现一系列需求。开发人员的准则在于,将这些需求拆分为一些列较小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。
一个组件又由其他更小的组件构成。如果你希望编写一个组件,你只能从抽象层中低一级的组件中,选取需要的组件用于创建自己的组件。
换言之,你需要将一个函数分解为若干较小的步骤,并且保证这些步骤都在抽象上,处于同一级别,而且只向下抽象一级。这非常重要,因为这将使得函数变得简单,做到“做且只做好一件事”。
为什么这是必要的?因为简单的函数非常清晰。清晰就意味着易于理解和修改。
我们来举个例子。假设你需要实现一个函数,使数组仅保留 素数 (2, 3, 5, 7, 11 等等),移除非素数(1, 4, 6, 8 等等)。函数的调用方式如下:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如何用低一级抽象的若干步骤实现 getOnlyPrime() 函数呢?我们这样做:
为了实现 getOnlyPrime() 函数, 我们用 isPrime() 函数来过滤数组中的数字。
非常简单,只需要对数字数组执行一个过滤函数 isPrime() 即可。
你需要在当前抽象层实现 isPrime() 的细节吗?不,因为 getOnlyPrime() 函数会在不同的抽象层实现一些列步骤。否则,getOnlyPrime() 会包含过多的功能。
在头脑中谨记简单函数的理念,我们来实现 getOnlyPrime() 函数的函数体:
function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
如你所见, getOnlyPrime() 非常简单,它仅仅包含低一级抽象层的步骤:数组的 .filter() 方法和 isPrime() 函数。
现在该进入下一级抽象。
数组的 .filter() 方法由 JavaScript 引擎提供,我们直接使用即可。当然,标准已经 准确描述 了它的行为。
现在你可以深入如何实现 isPrime() 的细节中了:
为了实现 isPrime() 函数检查一个数字 n 是否为素数,只需要检查 2 到 Math.sqrt(n) 之间的所有整数是否均不能整除n。
有了这个算法(不算高效,但是为了简单起见,就用这个吧),我们来为 isPrime() 函数编码:
function isPrime(number) { if (number === 3 || number === 2) { return true; } if (number === 1) { return false; } for (letpisor = 2; pisor <= Math.sqrt(number); pisor++) { if (number % pisor === 0) { return false; } } return true; } function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime() 很小也很清晰。它只从更低一级抽象中获得必要的一组步骤。
只要你按照这些规则,将函数变的简洁清晰,复杂函数的可读性将得到很大提升。将代码进行精确的抽象分级,可以避免出现大块的、难以维护的代码。
函数名称应该非常简练:长短适中。理想情况下,名称应当清楚的概括函数的功用,而不需要读者深入了解函数的实现细节。
对于使用 骆驼风格 的函数名称,以小写字母开始: addItem(), saveToStore() 或者 getFirstName() 之类。
由于函数都是某种操作,因此名称中至少应当包含一个动词。例如 deletePage(), verifyCredentials() 。需要 get 或 set 属性的时候,请使用 标准的 set 和 get 前缀: getLastName() 或 setLastName() 。
避免在生产代码中出现有误导性的名称,例如 foo(), bar(), a(), fun() 等等。这样的名称没有任何意义。
如果函数都短小清晰,命名简练:代码读起来就会像诗一样迷人。
当然了,这里假定的例子都非常简单。现实中的代码更加复杂。你可能要抱怨,编写清晰的函数,只在抽象上一级一级下降,实在太没劲了。但是如果从项目一开始就开始你的实践,就远没有想象中复杂。
如果应用中已经存在一些功能繁杂的函数,希望对它们进行重构,你可能会发现困难重重。而且在很多情况下,在合理的时间内是不可能完成的。但千里之行始于足下:在力所能及的前提下,先拆分一部分出来。
もちろん、最も正しい解決策は、プロジェクトの最初から正しい方法でアプリケーションを実装することです。実装に時間を費やすことに加えて、関数を適切に構造化することにも時間を費やす必要があります。推奨しているように、関数を短く明確にすることです。
ES2015 は、小さな機能が優れたエンジニアリング実践であることを明確に推奨する優れたモジュール システムを実装しています。
きれいでよく整理されたコードには、通常、多大な時間の投資が必要であることを覚えておいてください。これを行うのは難しいと思われるかもしれません。多くの試行が必要になる場合があり、関数を何度も反復して変更することもあります。
しかし、乱雑なコードほど面倒なものはないので、それだけの価値はあります。
以上が小さくてわかりやすい JavaScript 関数の書き方を教えますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。