ソフトウェアのデザイン哲学:第IX章マージや分解

ソフトウェア設計の一つは、最も基本的な質問です:二つの機能を考えると、彼らは同じ場所で一緒に実施されるべきである、または別々に実装すべきですか?この問題は、このような関数、メソッド、クラス、およびサービスとして、システムのすべてのレベルに適用されます。例えば、バッファは、ストリーム指向のファイルI / Oクラスを提供するために含まれるべきである、または別のクラスに含まれるべきであるか解析し、HTTPリクエストは、完全に一の手法で実装されなければならない、またはプロセスの複数(クラスも複数べき)を行いますか?この章では、これらの決定をする際の要因が考慮されるように説明します。これらの要因のいくつかは、すでに以前の章で説明されているが、完全を期すため、ここではそれらを再訪します。

意思決定を組み合わせたり、別された場合、目標は、システム全体の複雑さを軽減し、そのモジュール性を改善することです。この目標を達成するための最良の方法は、システムがウィジェットの多数に分割されているように見える:小さなコンポーネント、各コンポーネントは、より簡単です。しかし、分解挙動が分裂する前に存在していない追加の複雑さを、作成します。

  • より多くのコンポーネント、より困難な大規模な収集に必要なコンポーネントを見つけるために、それらを追跡することがより困難:一部の複雑さは唯一の構成要素の数からです。内訳は、通常、複数のインターフェースになり、それぞれの新しいインターフェースが複雑になります。
  • 細分化は、追加のコード管理構成要素をもたらすことができます。例えば、コードセグメントの前に単一のオブジェクトは、現在複数の管理オブジェクトを有していてもよいです。
  • より細分化した後に再分割する前に複数の別個の構成部品:別々のセグメントを生成します。別のクラスはまた別のファイルであってもよく、後に例えば、単一クラスの分割方法に先立っては、細分することができます。開発者は両方のコンポーネントを参照するための分離は、それも自分の存在を知っ困難であり、それが困難になります。コンポーネントは、真に独立している場合には、分離が良いです:それは、開発者が他のコンポーネントに気を取られることなく、一度だけ一つの成分に集中することができます。依存コンポーネント間に存在する一方、分離が良好ではない:開発者は、最終的に構成要素間で前後に切り替えます。さらに悪いことに、彼らは、バグにつながる可能性の依存性を実現しない場合があります。
  • これは、繰り返し細分化をもたらすことができる:各コンポーネントの単一のインスタンスに細分存在する前にコードセグメントに存在する必要があるかもしれません。

密接コードスニペットに関連する場合は、それらを組み合わせることは最も有益です。これらの部品が関連していない場合、それは分離するのがベストです。ここでは、2つのコードが関連していることをいくつかの兆候があります:

  • 彼らは、情報を共有し、例えば、文書の文法の特定のタイプに依存することができるコードを2枚。
  • コードの別の部分を使用することができます誰でも利用コードの一片を:彼らは一緒に使用しました。唯一の唯一の魅力双方向の場合の関係のこの形。反例として、ディスクブロックは、ほとんど常にキャッシュハッシュテーブルを必要とする、ハッシュテーブルを使用することができるが、多くの場合、キャッシュブロックを伴わないため、これらのモジュールは独立している必要があります。
  • これら2つのコードを含む単純なより高いレベルのカテゴリがあるので、彼らは、概念的にオーバーラップしています。例えば、文字列の範囲及び動作に属するサブストリング検索ケース変換、信頼性の高いフロー制御および配信は、すべてのネットワーク通信の分野です。
  • あなたは、コードの別の部分を見ていない場合は、コードのどの部分を理解することは困難です。

この章の残りの部分では、ルールを使用し、コードフラグメントが有意である場合、より具体的な例は、一緒に説明し、それは理にかなっている場合、それらを分離します。

9.1あなたが情報を共有している場合、情報まとめ

5.4節は、HTTPサーバを実装するためのプロジェクトのコンテキストでは、この原理を説明しています。最初の実装では、異なるクラスに2つの異なる方法を使用してプロジェクトを読み込み、HTTPリクエストを解析します。第一の方法は、ネットワークソケットからの着信要求のテキストを読み、文字列オブジェクト内に配置します。要求を抽出するために、各コンポーネントの文字列を解析する第二の方法。分解、最後の2つの方法がHTTPリクエスト形式のかなりの知識を持っている:最初の方法だけでそれを解析し、要求を読みたいが、仕事のほとんどは最後の要求を解決するためには、それが認識しない実行しない(例えば、それは頭を解決しますリクエストを識別するためのタイトル行)は、全体の長さが含まれています。1つのクラスコードにこれら2つのクラスが短く、簡単になったときに、なぜなら、この共有情報を、読み取られ、同じ場所要求を解析することが好ましいです。

9.2インターフェイスを簡素化することができれば、それが一緒に使用されています

2つの以上のモジュールは、モジュールに結合されている場合、あなたは、元のインターフェイスよりも簡単か、新しいモジュールのインターフェイスを使用する方が簡単多くを定義することができます。これは、多くの場合、解決の際に元のモジュールの実装上の問題の一部を発生します。前の例のHTTPサーバは、元のメソッドは、HTTPリクエストへのインターフェイスは、文字列を返し、第一の方法から、第二の方法に渡す必要があります。これらの方法を組み合わせた場合、これらのインタフェースは排除されます。

さらに、ときに、2つの以上のクラスの組み合わせの機能なので、ほとんどのユーザーがそれらを知っている必要はありません、自動的に特定の機能を実行することができます。JavaのI / Oライブラリは、この機会を示しています。FileInputStreamのとBufferedInputStreamを一緒にクラス、およびデフォルトのバッファを提供した場合は、ユーザーの大多数であっても、バッファの存在を知っている必要はありません。FileInputStreamのクラスの組み合わせは無効にする方法を提供するか、デフォルトのバッファリング機構を交換するが、ほとんどのユーザーは、これらの方法を知っている必要はありませんがあります。

重複を排除9.3

あなたは同じコード繰り返しパターンを見つけた場合は、重複を排除するためにコードを再編成してみてください。一つの方法は、単一のメソッドにコードを複製することであり、このメソッド呼び出しは、重複コードフラグメントに置き換えられます。重複コード長セグメント、および、単純な代替方法署名を有する場合、この方法が最も効果的です。コードセグメントの1つまたは2つのラインならば、それは何か良いをしないかもしれ交換のメソッドを呼び出します。複雑な方法でコードセグメントは、その環境(例えば、ローカル変数の数にアクセスすることによって)と相互作用する場合には、別の方法は、複雑な署名を必要とするかもしれない(例えば、参照によって渡されるパラメータの数)、その値を減少させます。

重複コードを除去する別の方法は、問題のコードフラグメントが唯一の場所で行うことが必要ように、再構成されています。(図中の例を参照してください。9.1)あなたは、いくつかの異なるポイントでエラーを返す必要があるメソッドを書いている、と同じ洗浄操作を実行する必要があるこれらの点に戻る前に、と仮定します。プログラミング言語のサポートが後藤場合は、図9.2に示すように、プロセスの最後に移動するには、コードをクリーンアップして、返された各点の誤差のニーズに行くことができます。私たちは、それらを使用することを選択していないコードの解読不能につながる可能性があるが、彼らは、ネストされたコードを回避するために使用することができるので、この場合には、それらが有用である場合にGOTO文は、一般的に、悪いアイデアと考えられています。

9.4別の共通のコードと特定コード

モジュールは、機構の複数の異なる目的のために使用することができるが含まれている場合、それが唯一の一般的なメカニズムを提供しなければなりません。これは、特定の目的のために特別なコード機構を含むべきではない、またそれは、他の一般的な機構を含むべきです。そして、共通のメカニズム通常関連する特定のコードが(典型的に関連付けられているモジュールの特定の使用を有する)異なるモジュール内に配置されるべきです。第6章GUIエディタの議論は、この原則を示しています。最高のデザインは、テキストクラスは、一般的なテキスト操作を提供していますが、(例えば、選択削除など)のユーザインタフェースの具体的な操作は、ユーザ・インターフェース・モジュールに実装されています。この方法は、設計情報の漏洩が早期に発生し、追加のインタフェースは、初期の設計では、ユーザーインターフェースの特別な操作は、テキストカテゴリに実装されてなくなります。

危険信号:繰り返しの
コードの同じ部分(またはほぼ同じコード)を繰り返した場合、これはあなたが右の抽象化を見つけていないという危険信号です。

図9.1:このコードは、インバウンドネットワークパケットの種類を扱う、種類ごとに、パケットは、タイプ、録音されたメッセージに合わせて短すぎます。コードのこのバージョンでは、ログ文はパッケージのいくつかの異なるタイプにコピーされます。

図9.2:コードリストラの図9.1、ログステートメントの唯一のコピー。

上部専用されている間、一般的に、低レベルのシステムは、しばしば一般的です。例えば、組成物の適用特性に非常に特定のアプリケーションによってトップレベル。特定のコードが、共通のコードから分離する方法は、個々のコードは、上位層にプルアップされている一般的なコードとして下位層ままです。

あなたは同じ一般と特別が含まれてクラスが発生した場合には、抽象参照クラスは、特定の機能を提供するために、他の上に、二つのクラス、共通の機能を含むものに分けることができます。

実施例9.5:挿入カーソルと選択

次のセクションでは、三つの例上述した原理によって説明します。両方の場合において、最良の方法は、関連するコードフラグメントを分離することである。第3の例では、それらを一緒に接続することが好ましいです。

挿入カーソルの最初の例と組成のGUI編集プロジェクトの第6章を選択します。エディタは、ユーザが入力したテキストがどこ文書に表示されることを示し、点滅する縦線が表示されます。また、テキストをコピーまたは削除するための選択と呼ばれる強調表示文字の範囲を、示しています。挿入カーソルが常に表示され、時にはテキストを選択しない場合があります。選択された項目が存在する場合、挿入カーソルは常に選択項目の一方の端部に配置されています。

选择和插入游标在某些方面是相关的。例如,光标总是停留在一个选择,和光标选择往往是一起操作:点击并拖动鼠标设置他们两人,和文本插入第一个删除选中的文本,如果有任何,然后在光标位置插入新的文本。因此,使用单个对象来管理选择和游标似乎是合理的,一个项目团队采用了这种方法。该对象在文件中存储了两个位置,以及布尔值,布尔值指示哪一端是游标,以及选择是否存在。

然而,组合的对象是尴尬的。它没有为高级代码提供任何好处,因为高级代码仍然需要知道选择和游标是不同的实体,并且需要分别操作它们(在文本插入期间,它首先调用组合对象上的一个方法来删除所选的文本;然后,它调用另一个方法来检索光标位置,以便插入新文本)。组合对象实际上比单独的对象更复杂。它避免将游标位置存储为单独的实体,而是必须存储一个布尔值,指示选择的哪一端是游标。为了检索光标位置,组合对象必须首先测试布尔值,然后选择适当的选择结束。

危险信号:特殊和一般的混合物

当通用机制还包含专门用于该机制特定用途的代码时,就会出现此警告。这使得机制更加复杂,并在机制和特定用例之间产生信息泄漏:未来对用例的修改可能也需要对底层机制进行更改。

本例中,选择和游标之间的关系不够紧密,无法将它们组合在一起。当修改代码以将选择和游标分隔开时,使用和实现都变得更简单了。与必须从中提取选择和游标信息的组合对象相比,分离对象提供了更简单的接口。游标实现也变得更简单了,因为游标位置是直接表示的,而不是通过选择和布尔值间接表示的。事实上,在修订版本中,选择和游标都没有使用特殊的类。相反,引入了一个新的Position类来表示文件中的一个位置(行号和行中的字符)。选择用两个位置表示,游标用一个位置表示。这些职位在项目中还有其他用途。这个示例还演示了较低级但更通用的接口的好处,这在第6章中讨论过。

9.6示例:日志记录的单独类

第二个例子涉及到学生项目中的错误日志记录。一个类包含如下代码序列:

try {
      rpcConn = connectionPool.getConnection(dest);
} catch (IOException e) {
      NetworkErrorLogger.logRpcOpenError(req, dest, e);
      return null;
}

不是在错误被检测到的地方记录错误,而是调用一个特殊的错误日志类中的一个单独的方法。错误日志类是在同一个源文件的末尾定义的:

private static class NetworkErrorLogger {
     /**
      *  Output information relevant to an error that occurs when trying
      *  to open a connection to send an RPC.
      *
      *  @param req 
                The RPC request that would have been sent through the connection
      *  @param dest
      *       The destination of the RPC
      *  @param e
      *       The caught error
      */
     public static void logRpcOpenError(RpcRequest req, AddrPortTuple dest, Exception e) {
         logger.log(Level.WARNING, "Cannot send message: " + req + ". \n" + "Unable to find or open connection to " + dest + " :" + e);
      }
...

}

NetworkErrorLogger类包含几个方法,如logRpcSendError和logRpcReceiveError,每个方法都记录不同类型的错误。

这种分离增加了复杂性,但没有带来任何好处。日志记录方法很简单:大多数都是由一行代码组成的,但是它们需要大量的文档。每个方法只在一个地方调用。日志记录方法高度依赖于它们的调用:读取调用的人很可能会切换到日志记录方法,以确保记录了正确的信息;类似地,阅读日志记录方法的人可能会转到调用站点以了解方法的用途。

在本例中,最好消除日志记录方法,并将日志语句放置在检测到错误的位置。这将使代码更易于阅读,并消除日志方法所需的接口。

9.7示例:编辑器撤销机制

在6.2部分的GUI编辑器项目中,其中一个需求是支持多级撤销/重做,不仅是对文本本身的更改,还包括对选择、插入游标和视图的更改。例如,如果用户选择某个文本,删除它,滚动到文件中的另一个位置,然后调用undo,编辑器必须将其状态恢复到删除之前的状态。这包括恢复被删除的文本,再次选择它,并使选择的文本在窗口中可见。

一些学生项目将整个撤销机制作为text类的一部分实现。text类维护了一个所有可撤销更改的列表。当文本被更改时,它会自动向这个列表添加条目。对于选择、插入游标和视图的更改,用户界面代码调用text类中的其他方法,然后这些方法将这些更改的条目添加到撤消列表中。当用户请求撤消或重做时,用户界面代码调用text类中的一个方法,然后由该方法处理撤消列表中的条目。对于与文本相关的条目,它更新了文本类的内部结构;对于与其他内容(如选择)相关的条目,文本类将调用回用户界面代码以执行撤消或重做。

这种方法导致文本类中出现一组令人尴尬的特性。撤销/重做的核心是一种通用机制,用于管理已执行的操作列表,并在撤消和重做操作期间逐步执行这些操作。核心位于text类中,与特殊用途的处理程序一起,这些处理程序为特定的事情(比如文本和选择)实现撤销和重做。用于选择和游标的特殊用途的撤消处理程序与文本类中的任何其他内容无关;它们导致文本类和用户界面之间的信息泄漏,以及每个模块中来回传递撤消信息的额外方法。如果将来向系统中添加了一种新的可撤消实体,则需要对text类进行更改,包括特定于该实体的新方法。此外,通用撤销核心与类中的通用文本工具几乎没有什么关系。

这些问题可以通过提取撤销/重做机制的通用核心并将其放在一个单独的类中来解决:

public class History {
        public interface Action {
               public void redo();
                       public void undo();
        }

        History() {...}

        void addAction(Action action) {...}

        void addFence() {...}

        void undo() {...}

        void redo() {...}
}

在本设计中,History类管理实现接口History. action的对象集合。每一个历史。Action描述单个操作,例如文本插入或光标位置的更改,并提供可以撤消或重做操作的方法。History类不知道操作中存储的信息,也不知道它们如何实现撤销和重做方法。History维护一个历史列表,该列表描述了在应用程序的生命周期中执行的所有操作,它提供了undo和redo方法,这些方法在响应用户请求的undos和redos时来回遍历列表,调用History. actions中的undo和redo方法。

历史。操作是特殊用途的对象:每个操作都理解一种特定的可撤消操作。它们在History类之外的模块中实现,这些模块理解特定类型的可撤销操作。text类可以实现UndoableInsert和UndoableDelete对象来描述文本插入和删除。每当插入文本时,text类都会创建一个新的UndoableInsert对象来描述插入并调用历史记录。addAction将其添加到历史记录列表。编辑器的用户界面代码可能创建UndoableSelection和UndoableCursor对象,它们描述对选择和插入游标的更改。

History类还允许对操作进行分组,例如,来自用户的单个undo请求可以恢复已删除的文本、重新选择已删除的文本和重新定位插入光标。

有很多方法来组织动作;History类使用fence,它是历史列表中的标记,用于分隔相关操作的组。每次遍历历史。redo向后遍历历史记录列表,撤消操作,直到到达下一个围栏。fence的位置由调用History.addFence的高级代码决定。

这种方法将撤销的功能分为三类,分别在不同的地方实现:

  • 一种通用的机制,用于管理和分组操作以及调用undo/redo操作(由History类实现)。
  • 特定操作的细节(由各种类实现,每个类理解少量的操作类型)。
  • 分组操作的策略(由高级用户界面代码实现,以提供正确的整体应用程序行为)。

这些类别中的每一个都可以在不了解其他类别的情况下实现。历史课不知道哪些行为被撤销了;它可以用于各种各样的应用。每个action类只理解一种action,而History类和action类都不需要知道分组action的策略。

关键的设计决策是将撤消机制的通用部分与专用部分分离,并将通用部分单独放在类中。一旦完成了这一步,剩下的设计就自然而然地结束了。

注意: 将通用代码与专用代码分离的建议是指与特定机制相关的代码。例如,特殊用途的撤消代码(例如撤消文本插入的代码)应该与通用用途的撤消代码(例如管理历史记录列表的代码)分开。然而,将一种机制的专用代码与另一种机制的通用代码组合起来通常是有意义的。text类就是这样一个例子:它实现了管理文本的通用机制,但是它包含了与撤销相关的专用代码。撤消代码是专用的,因为它只处理文本修改的撤消操作。将这段代码与History类中通用的undo基础结构结合在一起是没有意义的,但是将它放在text类中是有意义的,因为它与其他文本函数密切相关。

9.8 分解和连接方法

何时细分的问题不分解仅适用于类,也适用于方法:是否存在将现有方法划分为多个较小的方法更好的时机?或者,两个较小的方法应该合并成一个较大的方法吗?长方法往往比短方法更难理解,因此许多人认为,长度本身就是分解方法的一个很好的理由。学生在课堂上经常被给予严格的标准,如“分解任何超过20行的方法!”

但是,长度本身很少是拆分方法的好理由。 一般来说,开发人员倾向于过多地分解方法。拆分方法会引入额外的接口,增加了复杂性。它还分离了原始方法的各个部分,如果这些部分实际上是相关的,就会使代码更难读取。你不应该破坏一个方法,除非它使整个系统更简单;我将在下面讨论这是如何发生的。

长方法并不总是坏事。例如,假设一个方法包含五个按顺序执行的20行代码块。如果这些块是相对独立的,则可以一次读取和理解一个块;将每个块移动到一个单独的方法中没有什么好处。如果代码块具有复杂的交互,那么将它们放在一起更重要,这样读者就可以一次看到所有代码;如果每个块位于一个单独的方法中,读者将不得不在这些展开的方法之间来回切换,以了解它们是如何协同工作的。如果方法具有简单的签名并且易于阅读,那么包含数百行代码的方法就很好。这些方法很深奥(功能很多,接口简单),这很好。

图9.3:一个方法(A)可以通过提取一个子任务(b)或者通过将其功能划分为两个单独的方法(c)来分解。

在设计方法时,最重要的目标是提供简洁而简单的抽象。 每一种方法都应该做一件事,而且要做得彻底。 这个方法应该有一个干净简单的界面,这样用户就不需要在他们的头脑中有太多的信息来正确地使用它。方法应该是深度的:它的接口应该比它的实现简单得多。 如果一个方法具有所有这些属性,那么它是否长可能并不重要。

总的来说,分解方法只有在产生更清晰的抽象时才有意义。有两种方法可以做到这一点,如图9.3所示。最好的方法是将一个子任务分解成单独的方法,如图9.3(b)所示。细分产生包含子任务的子方法和包含原始方法其余部分的父方法;父调用子调用。新父方法的接口与原始方法相同。这种形式的细分有意义如果有干净地分离的子任务的原始方法,这意味着(a)有人阅读孩子的方法不需要知道任何关于父法和(b)有人阅读父法不需要理解孩子的实现方法。通常这意味着子方法是相对通用的:它可以被父方法之外的其他方法使用。如果您对这个表单进行拆分,然后发现自己在父类和子类之间来回切换,以了解它们是如何协同工作的,那么这就是一个危险信号(“联合方法”),表明拆分可能不是一个好主意。

分解一个方法的第二种方法是将它分解成两个单独的方法,每个方法对于原始方法的调用者都是可见的,如图9.3(c)所示。如果原始方法有一个过于复杂的接口,这是有意义的,因为它试图做许多不密切相关的事情。如果是这种情况,可以将方法的功能划分为两个或多个更小的方法,每个方法只具有原始方法的一部分功能。如果像这样分解,每个结果方法的接口应该比原始方法的接口简单。理想情况下,大多数调用者应该只需要调用两个新方法中的一个;如果调用者必须同时调用这两个新方法,那么这就增加了复杂性,从而降低了拆分的可能性。新方法将更专注于它们所做的事情。如果新方法比原来的方法更通用,这是一个好迹象。你可以想象在其他情况下分别使用它们)。

图9.3(c)中所示的表单分解通常没有意义,因为它们导致调用者必须处理多个方法,而不是一个。当您以这种方式进行划分时,您可能会得到几个浅层方法,如图9.3(d)所示。如果调用者必须调用每个单独的方法,在它们之间来回传递状态,那么分解不是一个好主意。如果您正在考虑类似图9.3(c)中的拆分,那么您应该根据它是否简化了调用者的工作来判断它。

在某些情况下,可以通过将方法连接在一起来简化系统。例如,连接方法可以用一个较深的方法代替两个较浅的方法;它可以消除重复的代码;它可以消除原始方法或中间数据结构之间的依赖关系;它可能导致更好的封装,因此以前在多个地方出现的知识现在被隔离在一个地方;或者,它可能导致一个更简单的接口,如9.2节中所讨论的那样。

危险信号:联合方法

应该能够独立地理解每种方法。如果你不能理解一个方法的实现而不理解另一个方法的实现,那就是一个危险信号。此微信型号也可以出现在其他上下文中:如果两段代码在物理上是分开的,但是每段代码只能通过查看另一段代码来理解,这就是危险信号。

9.9 结论

拆分或联接模块的决策应该基于复杂性。选择能够隐藏最佳信息、最少依赖和最深接口的结构。

おすすめ

転載: www.cnblogs.com/peida/p/12071197.html