結合が高すぎる-このクラスをより良く設計する方法は?
-
06-07-2019 - |
質問
コードでFxCopを実行すると、次の警告が表示されます。
Microsoft.Maintainability: 'FooBar.ctorは99と結合されています 9種類の異なるタイプ 名前空間。リライトまたはリファクタリング クラス結合を減らすメソッド、 または、メソッドを1つに移動することを検討してください 他のタイプのそれはしっかりです と相まって。上記のクラスカップリング 40は保守性が悪いことを示し、 40と30の間のクラスカップリング 中程度の保守性を示し、 および30未満のクラスカップリング 保守性が良いことを示します。
私のクラスは、サーバーからのすべてのメッセージのランディングゾーンです。サーバーは、さまざまなEventArgsタイプのメッセージを送信できます。
public FooBar()
{
var messageHandlers = new Dictionary<Type, Action<EventArgs>>();
messageHandlers.Add(typeof(YouHaveBeenLoggedOutEventArgs), HandleSignOut);
messageHandlers.Add(typeof(TestConnectionEventArgs), HandleConnectionTest);
// ... etc for 90 other types
}
<!> quot; HandleSignOut <!> quot;および<!> quot; HandleConnectionTest <!> quot;メソッドにはコードがほとんどありません。彼らは通常、作業を別のクラスの関数に渡します。
どのようにしてカップリングを低くしてこのクラスを改善できますか?
解決
作業を行うクラスに、興味のあるイベントを登録してもらう... イベントブローカーパターン。
class EventBroker {
private Dictionary<Type, Action<EventArgs>> messageHandlers;
void Register<T>(Action<EventArgs> subscriber) where T:EventArgs {
// may have to combine delegates if more than 1 listener
messageHandlers[typeof(T)] = subscriber;
}
void Send<T>(T e) where T:EventArgs {
var d = messageHandlers[typeof(T)];
if (d != null) {
d(e);
}
}
}
他のヒント
また、Spring.NETなどのIoCフレームワークを使用して、辞書を挿入することもできます。このように、新しいメッセージタイプを取得した場合、この中央ハブを再コンパイルする必要はありません。設定ファイルを変更するだけです。
待望の例:
Exampleという名前の新しいコンソールアプリを作成し、これを追加します:
using System;
using System.Collections.Generic;
using Spring.Context.Support;
namespace Example
{
internal class Program
{
private static void Main(string[] args)
{
MessageBroker broker = (MessageBroker) ContextRegistry.GetContext()["messageBroker"];
broker.Dispatch(null, new Type1EventArgs());
broker.Dispatch(null, new Type2EventArgs());
broker.Dispatch(null, new EventArgs());
}
}
public class MessageBroker
{
private Dictionary<Type, object> handlers;
public Dictionary<Type, object> Handlers
{
get { return handlers; }
set { handlers = value; }
}
public void Dispatch<T>(object sender, T e) where T : EventArgs
{
object entry;
if (Handlers.TryGetValue(e.GetType(), out entry))
{
MessageHandler<T> handler = entry as MessageHandler<T>;
if (handler != null)
{
handler.HandleMessage(sender, e);
}
else
{
//I'd log an error here
Console.WriteLine("The handler defined for event type '" + e.GetType().Name + "' doesn't implement the correct interface!");
}
}
else
{
//I'd log a warning here
Console.WriteLine("No handler defined for event type: " + e.GetType().Name);
}
}
}
public interface MessageHandler<T> where T : EventArgs
{
void HandleMessage(object sender, T message);
}
public class Type1MessageHandler : MessageHandler<Type1EventArgs>
{
public void HandleMessage(object sender, Type1EventArgs args)
{
Console.WriteLine("Type 1, " + args.ToString());
}
}
public class Type2MessageHandler : MessageHandler<Type2EventArgs>
{
public void HandleMessage(object sender, Type2EventArgs args)
{
Console.WriteLine("Type 2, " + args.ToString());
}
}
public class Type1EventArgs : EventArgs {}
public class Type2EventArgs : EventArgs {}
}
およびapp.configファイル:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects"/>
</context>
<objects xmlns="http://www.springframework.net">
<object id="messageBroker" type="Example.MessageBroker, Example">
<property name="handlers">
<dictionary key-type="System.Type" value-type="object">
<entry key="Example.Type1EventArgs, Example" value-ref="type1Handler"/>
<entry key="Example.Type2EventArgs, Example" value-ref="type2Handler"/>
</dictionary>
</property>
</object>
<object id="type1Handler" type="Example.Type1MessageHandler, Example"/>
<object id="type2Handler" type="Example.Type2MessageHandler, Example"/>
</objects>
</spring>
</configuration>
出力:
Type 1, Example.Type1EventArgs Type 2, Example.Type2EventArgs No handler defined for event type: EventArgs
ご覧のとおり、MessageBroker
はどのハンドラーも知らず、ハンドラーはMessageHandler<>
を知りません。マッピングはすべてapp.configファイルで行われるため、新しいイベントタイプを処理する必要がある場合は、configファイルに追加できます。これは、他のチームがイベントタイプとハンドラーを定義している場合に特に便利です。それらは、dllでそれらのものをコンパイルするだけで、デプロイメントにドロップして、マッピングを追加するだけです。
実際のハンドラーをMessageHandler<EventArgs>
にキャストできないため、ディクショナリには<=>ではなくtypeオブジェクトの値があります。そのため、少しハックしなければなりませんでした。ソリューションはまだきれいで、マッピングエラーをうまく処理できると思います。このプロジェクトでSpring.Core.dllを参照する必要があることに注意してください。ライブラリはこちらで見つけることができます。 、およびドキュメントこちら。 依存性注入の章はこれに関連しています。また、このためにSpring.NETを使用する必要はないことに注意してください-ここでの重要なアイデアは依存性注入です。どういうわけか、ブローカーにタイプaのメッセージをxに送信するように指示する必要があるので、依存関係の注入にIoCコンテナーを使用することはブローカーにxを認識させず、その逆も同様です。
IoCおよびDIに関連するその他のSO質問:
残りのコードは表示されませんが、もっと少ない数のEvent argクラスを作成してみます。代わりに、含まれるデータおよび/または後でそれらを処理する方法に関して互いに類似したいくつかを作成し、発生したイベントの正確なタイプを示すフィールドを追加します(おそらく列挙型を使用する必要があります)。
理想的には、このコンストラクタをはるかに読みやすくするだけでなく、メッセージの処理方法(単一のイベントハンドラで同様の方法で処理されるメッセージをグループ化する)
おそらく、メッセージごとに異なるクラスを持つ代わりに、メッセージを識別するフラグを使用します。
これにより、メッセージの数が大幅に減少し、保守性が向上します。私の推測では、ほとんどのメッセージクラスの差はほぼゼロです。
これを攻撃する別の方法を選ぶのは困難です。アーキテクチャの残りの部分は(私には)不明だからです。
たとえば、Windowsを見ると、スローされる可能性のある各メッセージの処理方法をネイティブに知りません。代わりに、基になるメッセージハンドラーがコールバック関数をメインスレッドに登録します。
同様のアプローチをとることができます。各メッセージクラスは、それ自体を処理する方法を知る必要があり、より大きなアプリケーションに自身を登録できます。これにより、コードが大幅に簡素化され、密結合が解消されます。
明らかにディスパッチのメカニズムが必要です。受け取るイベントに応じて、異なるコードを実行する必要があります。
イベントを識別するために型システムを使用しているようですが、実際にはポリモーフィズムをサポートするためのものです。 Chris Livelyが提案しているように、列挙型を使用してメッセージを識別することもできます(型システムを乱用することなく)。
または、タイプシステムの能力を活用して、すべてのタイプのイベントが登録されるレジストリオブジェクトを作成できます(静的インスタンス、構成ファイルなどによって)。次に、Chain of Responsibilityパターンを使用して、適切なハンドラーを見つけることができます。ハンドラーがそれ自体を処理するか、イベントを処理するオブジェクトを作成するファクトリーのいずれかです。
後者の方法は少し仕様が不十分で過剰に設計されているように見えますが、99のイベントタイプ(既に)の場合、私にとって適切なようです。