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 将无法加载 Main
  • 没有自定义加载器类,LoginScreen 将无法加载
  • 如果没有我们的自定义按钮类,LoginScreen 将无法加载
  • 如果没有我们的自定义 SocketConnection 类,LoginScreen 将无法加载
  • 现在LoginScreen里面已经设置了SocketConnection(以后会被很多不同的类访问),除了LoginScreen第一次需要socket连接之外,其实和它没什么关系

为了解决这些问题,有人建议我做“事件驱动编程”,或者说松耦合。据我了解,这基本上意味着必须使类彼此独立,然后将它们绑定在单独的绑定器中。

那么问题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:这有意义吗?这不会使事情变得不必要的复杂吗?

此外,在上面的“Binders”中,我只需要使用实例化一次的类,例如。登录屏幕。如果一个类有多个实例怎么办,例如。国际象棋游戏中的玩家类。

4

2 回答 2

10

好吧,关于如何,我会向你指出我的5 条诫命。:)

对于这个问题,只有 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看起来有点痒。但老实说,它完成了它需要做的所有事情。现在它可以由一个类有效地实现Server,它将所有网络封装在一个类中,为您的实际服务器提供的N个调用中的每一个都有一个方法,但首先,ISP建议,许多客户端特定的接口都比一个更好通用接口。出于同样的原因ILoginInterface,它确实保持在最低限度。

不管这两个是如何实际实现的,你都不需要改变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 服务器)上,该服务器进行身份验证,并在成功的情况下在实际应用服务器上创建会话。在设计方面,这可以反映在两个单独的类中。

现在,让我们谈谈我在 Main 中留下的“……”。好吧,我很懒,所以我可以告诉你,在我的代码中你可能会看到

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

当然,这远非干净的方式。事实是,这是最简单的方法,并且依赖项仅限于一次出现,以后可以通过依赖注入将其删除。

就像haXe中的IoC -Container 的一个简单示例:

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 类的作用是初始化应用程序。完成后,它应该将控制权传递给登录控制器,当该控制器获取用户对象时,它可以将其传递给实际应用程序的主控制器等等。我建议您阅读一些有关行为模式的内容以获取一些想法。

问候
back2dos

于 2010-07-12T23:59:16.340 回答
1

首先,我对 Haxe 一点也不熟悉。但是,我会回答,这里描述的内容听起来与我在 .NET 中学习做事的方式非常相似,所以在我看来这是一种很好的做法。

在 .NET 中,您有一个“事件”,当用户单击按钮执行某项操作(如登录)时触发,然后执行一个方法来“处理”该事件。

当另一个类中的事件被触发时,总会有描述在一个类中执行什么方法的代码。它不是不必要的复杂,它必然是复杂的。在 Visual Studio IDE 中,大部分代码都隐藏在“设计器”文件中,所以我不会经常看到它,但如果您的 IDE 没有此功能,则必须自己编写代码.

至于这如何与您的自定义加载器类一起使用,我希望这里有人可以为您提供答案。

于 2010-07-12T13:45:33.683 回答