2

私はHaxeでコードを書いています。ただし、これは高級言語であり、Java、ActionScript、JavaScript、C#などと比較できることを念頭に置いている限り、この質問とはまったく関係ありません(ここでは擬似コードを使用しています)。

私は大きなプロジェクトに取り組むつもりで、今準備に忙しいです。ただし、この質問では、小さなシナリオを作成します。Mainクラス(これはアプリケーションの起動時に実行されます)とLoginScreenクラス(これは基本的に、ユーザーがログイン画面をロードできるようにするクラスです)を持つ単純なアプリケーションです。ログインする)。

通常、これは次のようになります。

Main constructor:
loginScreen = new LoginScreen()
loginScreen.load();

LoginScreen load():
niceBackground = loader.loadBitmap("somebg.png");
someButton = new gui.customButton();
someButton.onClick = buttonIsPressed;

LoginScreen buttonIsPressed():
socketConnection = new network.SocketConnection();
socketConnection.connect(host, ip);
socketConnection.write("login#auth#username#password");
socketConnection.onData = gotAuthConfirmation;

LoginScreen gotAuthConfirmation(response):
if response == "success" {
   //login success.. continue
}

この単純なシナリオでは、クラスに次の依存関係と欠点が追加されます。

  • LoginScreenがないとメインはロードされません
  • LoginScreenは、カスタムローダークラスがないとロードされません
  • LoginScreenは、カスタムボタンクラスがないと読み込まれません
  • LoginScreenは、カスタムSocketConnectionクラスがないとロードされません
  • SocketConnection(将来的には多くの異なるクラスからアクセスする必要があります)がLoginScreen内に設定されました。これは、LoginScreenが初めてソケット接続を必要とするという事実を除けば、実際にはまったく関係ありません。

これらの問題を解決するために、「イベント駆動型プログラミング」または緩い結合を行うことをお勧めします。私が理解している限り、これは基本的に、クラスを互いに独立させてから、別々のバインダーでそれらをバインドする必要があることを意味します。

それで質問1:それについての私の見解は正しいですか、それとも間違っていますか?バインダーを使用する必要がありますか?

アスペクト指向プログラミングがここで役立つと聞きました。残念ながら、Haxeはこの構成をサポートしていません。

ただし、基本的にシグナルラーの作成(public var loginPressedSignaller = new Signaller())、シグナルラーの起動(loginPressedSignaller.fire())、シグナルラーのリッスン(someClass.loginPressedSignaller)を可能にするイベントライブラリにアクセスできます。 .bind(doSomethingWhenLoginPressed))。

したがって、これ以上の調査をほとんど行わずに、これにより以前の設定が次のように変更されることがわかりました。

Main:
public var appLaunchedSignaller = new Signaller();

Main constructor:
appLaunchedSignaller.fire();

LoginScreen:
public var loginPressedSignaller = new Signaller();

LoginScreen load():
niceBackground = !!! Question 2: how do we use Event Driven Programming to load our background here, while not being dependent on the custom loader class !!!
someButton = !!! same as for niceBackground, but for the customButton class !!!
someButton.onClick = buttonIsPressed;

LoginScreen buttonIsPressed():
loginPressedSignaller.fire(username, pass);

LoginScreenAuthenticator:
public var loginSuccessSignaller = new Signaller();
public var loginFailSignaller = new Signaller();

LoginScreenAuthenticator auth(username, pass):
socketConnection = !!! how do we use a socket connection here, if we cannot call a custom socket connection class !!!
socketConnection.write("login#auth#username#password");

このコードはまだ完成していません。私はまだサーバーの応答をリッスンする必要がありますが、おそらく私が行き詰まっている場所を理解しているでしょう。

質問2:この新しい構造は意味がありますか?上記の問題をどのように解決すればよいですか!!! 区切り文字?

それから私はバインダーについて聞いた。したがって、すべてを接続するために、クラスごとにバインダーを作成する必要があるかもしれません。このようなもの:

MainBinder:
feature = new Main();    

LoginScreenBinder:
feature = new LoginScreen();
MainBinder.feature.appLaunchedSignaller.bind(feature.load);
niceBackgroundLoader = loader.loadBitmap;
someButtonClass = gui.customButton();

など...うまくいけば、あなたは私が何を意味するのか理解しています。この投稿は少し長くなっているので、少しまとめる必要があります。

質問3:これは意味がありますか?これは物事を不必要に複雑にしませんか?

また、上記の「バインダー」では、一度インスタンス化されたクラスを使用するだけで済みました。ログイン画面。クラスのインスタンスが複数ある場合はどうなりますか。チェスのゲームのプレーヤークラス。

4

2 に答える 2

10

さて、その方法について、私はあなたに私の五戒を指摘します。:)

この質問では、3つだけが本当に重要です。

  • 単一責任(SRP)
  • インターフェイス分離(ISP)
  • 依存性逆転(DIP)

SRPから始めて、「クラスXの責任は何ですか?」という質問を自問する必要があります。

ログイン画面は、ユーザーにログインデータを入力して送信するためのインターフェイスを表示する役割を果たします。したがって

  1. ボタンが必要なので、ボタンクラスに依存するのは理にかなっています。
  2. すべてのネットワーキングなどを行うのは意味がありません。

まず、ログインサービスを抽象化しましょう。

interface ILoginService {
     function login(user:String, pwd:String, onDone:LoginResult->Void):Void;
     //Rather than using signalers and what-not, I'll just rely on haXe's support for functional style, 
     //which renders these cumbersome idioms from more classic languages quite obsolete.
}
enum Result<T> {//this is a generic enum to return results from basically any kind of actions, that may fail
     Fail(error:Int, reason:String);
     Success(user:T);
}
typedef LoginResult = Result<IUser>;//IUser basically represent an authenticated user

Mainクラスの観点から、ログイン画面は次のようになります。

interface ILoginInterface {
    function show(inputHandler:String->String->Void):Void;
    function hide():Void;
    function error(reason:String):Void;
}

ログインの実行:

var server:ILoginService = ... //where ever it comes from. I will say a word about that later
var login:ILoginInterface = ... //same thing as with the service
login.show(function (user, pwd):Void {
      server.login(user, pwd, function (result) {
             switch (result) {
                  case Fail(_, reason): 
                        login.error(reason);
                  case Success(user): 
                        login.hide();
                        //proceed with the resulting user
             }
      });
});//for the sake of conciseness I used an anonymous function but usually, you'd put a method here of course

ILoginService少し厄介に見えます。しかし、正直なところ、それはそれがする必要があるすべてをします。これで、すべてのネットワークを1つのクラスにカプセル化し、実際のサーバーが提供するNServer回の呼び出しごとにメソッドを持つクラスによって効果的に実装できますが、まず、ISPは、多くのクライアント固有のインターフェイスが1つよりも優れていることを示唆しています。汎用インターフェース。同じ理由で、実際には最小限に抑えられています。ILoginInterface

これら2つが実際にどのように実装されていても、変更する必要はありませんMain(もちろん、インターフェイスが変更されない限り)。これは適用されているDIPです。Main具体的な実装に依存するのではなく、非常に簡潔な抽象化にのみ依存します。

次に、いくつかの実装を行いましょう。

class LoginScreen implements ILoginInterface {
    public function show(inputHandler:String->String->Void):Void {
        //render the UI on the screen
        //wait for the button to be clicked
        //when done, call inputHandler with the input values from the respective fields
    }
    public function hide():Void {
        //hide UI
    }
    public function error(reason:String):Void {
        //display error message
    }
    public static function getInstance():LoginScreen {
        //classical singleton instantiation
    }
}
class Server implements ILoginService {
    function new(host:String, port:Int) {
        //init connection here for example
    }
    public static function getInstance():Server {
        //classical singleton instantiation
    }   
    public function login(user:String, pwd:String, onDone:LoginResult->Void) {
        //issue login over the connection
        //invoke the handler with the retrieved result
    }
    //... possibly other methods here, that are used by other classes
}

わかりました、それはかなり簡単だったと思います。しかし、それを楽しむために、本当にばかげた何かをしましょう。

class MailLogin implements ILoginInterface {
    public function new(mail:String) {
        //save address
    }
    public function show(inputHandler:String->String->Void):Void {
        //print some sort of "waiting for authentication"-notification on screen
        //send an email to the given address: "please respond with username:password"
        //keep polling you mail server for a response, parse it and invoke the input handler
    }
    public function hide():Void {
        //remove the "waiting for authentication"-notification
        //send an email to the given address: "login successful"
    }
    public function error(reason:String):Void {
        //send an email to the given address: "login failed. reason: [reason] please retry."
    }   
}

この認証は歩行者である可能性がありますが、Mainクラスの観点からは、これは何も変更しないため、同様に機能します。

より可能性の高いシナリオは、実際には、ログインサービスが別のサーバー(おそらくHTTPサーバー)上にあり、認証を行い、成功した場合は実際のアプリサーバー上にセッションを作成することです。設計上、これは2つの別々のクラスに反映される可能性があります。

それでは、メインに残した「...」についてお話しましょう。まあ、私は怠惰なので、あなたに言うことができます、私のコードではあなたが見る可能性があります

var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();

もちろん、これはそれを行うためのクリーンな方法とはほど遠いです。真実は、これが最も簡単な方法であり、依存性は1回の発生に制限されており、後で依存性注入によって削除できます。

haXeのIoCコンテナの簡単な例と同じように:

class Injector {
    static var providers = new Hash < Void->Dynamic > ;
    public static function setProvider<T>(type:Class<T>, provider:Void->T):Void {
        var name = Type.getClassName(type);
        if (providers.exists(name))
            throw "duplicate provider for " + name;
        else
            providers.set(name, provider);
    }
    public static function get<T>(type:Class<T>):T {
        var name = Type.getClassName(type);
        return
            if (providers.exists(name))
                providers.get(name);
            else
                throw "no provider for " + name;
    }
}

エレガントな使用法(usingキーワード付き):

using Injector;

//wherever you would like to wire it up:
ILoginService.setProvider(Server.getInstance);
ILoginInterface.setProvider(LoginScreen.getInstance);

//and in Main:
var server = ILoginService.get();
var login = ILoginInterface.get();

このように、実際には個々のクラス間の結合はありません。

ボタンとログイン画面の間でイベントを渡す方法についての質問については、
これは好みと実装の問題です。イベント駆動型プログラミングのポイントは、ソースとオブザーバーの両方が、ある意味で結合されているだけであり、ソースが何らかの通知を送信し、ターゲットがそれを処理できる必要があるということです。 someButton.onClick = handler;基本的にはまさにそれを行いますが、それはとてもエレガントで簡潔なので、それについて曖昧にすることはありません。 someButton.onClick(handler);UIコンポーネントでこれが必要になることはめったにありませんが、複数のハンドラーを持つことができるため、おそらく少し優れています。しかし、最終的には、信号機が必要な場合は、信号機を使用してください。

現在、AOPに関しては、この状況では適切なアプローチではありません。コンポーネントを相互に接続するのは賢いハックではありませんが、ログ、履歴、さらには多数のモジュールにわたる永続層としてのものを追加するなど、横断的関心事に対処することについてです。

一般に、アプリケーションの小さな部分をモジュール化または分割しないようにしてください。コードベースにスパゲッティを入れてもかまいません。

  1. スパゲッティセグメントはよくカプセル化されています
  2. スパゲッティセグメントは、アプリを壊すことなく、理解できるか、妥当な時間内にリファクタリング/書き換えられるほど小さいです(ポイント1が保証する必要があります)

むしろ、アプリケーション全体を、簡潔なインターフェースを介して相互作用する自律的な部分に分割するようにしてください。パーツが大きくなりすぎる場合は、同じ方法でリファクタリングします。

編集:

トムの質問に答えて:

  1. それは好みの問題です。一部のフレームワークでは、外部構成ファイルを使用するところまで行きますが、実行時に注入する依存関係のコンパイルを強制するようにコンパイラーに指示する必要があるため、haXeではほとんど意味がありません。中央ファイルのコードで依存関係を設定することは、同じくらい多くの作業であり、はるかに簡単です。より詳細な構造については、アプリを「モジュール」に分割できます。各モジュールには、提供する実装の登録を担当するローダークラスがあります。メインファイルで、モジュールをロードします。
  2. 場合によります。私はそれらに応じてクラスのパッケージでそれらを宣言し、後でそれらが他の場所で必要であることが判明した場合に備えて追加のパッケージにリファクタリングする傾向があります。匿名タイプを使用することで、物事を完全に切り離すこともできますが、flash9などのプラットフォームではパフォーマンスがわずかに低下します。
  3. ボタンを抽象化してからIoCを介して実装を挿入することはしませんが、自由に実行してください。結局、それは単なるボタンなので、明示的に作成します。スタイル、キャプション、画面の位置とサイズがあり、クリックイベントを発生させます。上で指摘したように、これは不要なモジュール化だと思います。
  4. SRPに固執します。そうすれば、クラスが不必要に大きくなることはありません。Mainクラスの役割は、アプリを初期化することです。完了すると、ログインコントローラに制御を渡す必要があり、そのコントローラがユーザーオブジェクトを取得すると、実際のアプリのメインコントローラなどに渡すことができます。いくつかのアイデアを得るために、行動パターンについて少し読むことをお勧めします。

greetz
back2dos

于 2010-07-12T23:59:16.340 に答える
1

まず第一に、私はHaxeにまったく精通していません。ただし、ここで説明する内容は、.NETでの作業方法と非常によく似ているため、これは良い習慣のように思えます。

.NETには、ユーザーがボタンをクリックして何か(ログオンなど)を実行し、メソッドが実行されてイベントを「処理」したときに発生する「イベント」があります。

別のクラスのイベントが発生したときに、あるクラスで実行されるメソッドを説明するコードが常に存在します。不必要に複雑ではなく、必然的に複雑です。Visual Studio IDEでは、このコードの多くは「デザイナー」ファイルに隠されているため、定期的には表示されませんが、IDEにこの機能がない場合は、コードを自分で作成する必要があります。 。

これがカスタムローダークラスでどのように機能するかについては、ここの誰かがあなたに答えを提供してくれることを願っています。

于 2010-07-12T13:45:33.683 に答える