高级语言中的松散编程,如何,为什么以及多少?

Tom*_*Tom 2 oop events aop functional-programming haxe

我在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
}
Run Code Online (Sandbox Code Playgroud)

这个简单的场景为我们的类添加了以下依赖关系和缺点:

  • 如果没有LoginScreen,Main将无法加载
  • 如果没有自定义装入程序类,则不会加载LoginScreen
  • 没有我们的自定义按钮类,将无法加载LoginScreen
  • 没有我们的自定义SocketConnection类,将无法加载LoginScreen
  • SocketConnection(将来必须由很多不同的类访问)现在已经在LoginScreen中设置了,除了LoginScreen第一次需要套接字连接这一事实上,它实际上与它无关.

为了解决这些问题,我被建议做"事件驱动编程"或松散耦合.据我所知,这基本上意味着必须使类彼此独立,然后将它们绑定在不同的绑定器中.

问题1:我的观点是真是假?是否必须使用粘合剂?

我听说面向方面编程可以在这里提供帮助.不幸的是,Haxe不支持此配置.

但是,我确实可以访问一个事件库,它基本上允许我创建一个signaller(public var loginPressedSignaller = new Signaller()),触发一个signaller(loginPressedSignaller.fire())并监听一个signalller(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");
Run Code Online (Sandbox Code Playgroud)

此代码尚未完成,例如.我仍然需要听取服务器响应,但你可能知道我遇到了什么问题.

问题2:这种新结构是否有意义?我该如何解决上面提到的问题!!! 分隔符?

然后我听说了粘合剂.所以也许我需要为每个类创建一个binder,将所有东西连接在一起.像这样的东西:

MainBinder:
feature = new Main();    

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

等...希望你理解我的意思.这篇文章有点长,所以我不得不把它包起来.

问题3:这有什么意义吗?这不会使事情变得不必要地复杂吗?

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

bac*_*dos 9

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

对于这个问题,只有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
Run Code Online (Sandbox Code Playgroud)

从Main类的角度来看,登录界面如下所示:

interface ILoginInterface {
    function show(inputHandler:String->String->Void):Void;
    function hide():Void;
    function error(reason:String):Void;
}
Run Code Online (Sandbox Code Playgroud)

执行登录:

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
Run Code Online (Sandbox Code Playgroud)

现在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
}
Run Code Online (Sandbox Code Playgroud)

好吧,我想这很简单.但只是为了它的乐趣,让我们做一些非常愚蠢的事情:

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."
    }   
}
Run Code Online (Sandbox Code Playgroud)

作为此身份验证的行人可能是,从Main类的角度来看,这不会改变任何内容,因此也可以正常工作.

实际上,更可能的情况是,您的登录服务在另一台服务器(可能是HTTP服务器)上进行身份验证,如果成功,则在实际的应用服务器上创建会话.在设计方面,这可以反映在两个单独的类中.

现在,让我们谈谈我在Main中留下的"......".嗯,我很懒,所以我可以告诉你,在我的代码中你可能会看到

var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();
Run Code Online (Sandbox Code Playgroud)

当然,这远不是干净的方式.事实是,这是最简单的方法,依赖性仅限于一次出现,以后可以通过依赖注入来删除.

就像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;
    }
}
Run Code Online (Sandbox Code Playgroud)

优雅用法(带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();
Run Code Online (Sandbox Code Playgroud)

这样,您几乎没有各个类之间的耦合.

至于如何在按钮和登录屏幕之间传递事件的问题:
这只是品味和实现的问题.事件驱动编程的要点是源和观察者只是在某种意义上耦合,源必须发送某种通知,目标必须能够处理它. someButton.onClick = handler;基本上就是这样,但它只是如此优雅和简洁,你不要对它做一个模糊. someButton.onClick(handler);可能会好一点,因为你可以拥有多个处理程序,尽管UI组件很少需要.但最后,如果你想要信号员,请选择信号员.

现在谈到AOP,在这种情况下,这不是正确的方法.将组件彼此连接起来并不是一个聪明的黑客,而是处理跨领域的问题,例如在多个模块中添加日志,历史甚至是持久层.

通常,尽量不要模块化或拆分应用程序的一小部分.只要你的代码库中有一些意大利面就可以了

  1. 意大利面条很好地封装了
  2. 意大利面条段小到足以在合理的时间内理解或重构/重写,而不会破坏应用程序(第1点应该保证)

尝试将整个应用程序拆分为自治部分,通过简洁的界面进行交互.如果一个部分变得太大,重构它就会以同样的方式.

编辑:

回答汤姆的问题:

  1. 这是一个品味问题.在一些框架中,人们甚至使用外部配置文件,但这对于haXe没有多大意义,因为您需要指示编译器强制编译您在运行时注入的依赖项.在中央文件中设置代码中的依赖关系同样可行,而且更简单.对于更多结构,您可以将应用程序拆分为"模块",每个模块都有一个加载器类,负责注册它提供的实现.在主文件中,加载模块.
  2. 那要看.我倾向于根据它们在类的包中声明它们,然后将它们重构为一个额外的包,以防它们被证明在其他地方需要.通过使用匿名类型,您还可以完全解耦事物,但您在平台上的性能会略有提升为flash9.
  3. 我不会抽象按钮,然后通过IoC注入一个实现,但随意这样做.我会明确地创建它,因为最后,它只是一个按钮.它有风格,标题,屏幕位置和大小,并触发点击事件.我认为,正如上文所指出的,这是不必要的模块化.
  4. 坚持SRP.如果你这样做,任何课程都不会不必要地成长.Main类的作用是初始化app.完成后,它应该将控制传递给登录控制器,并且当该控制器获取用户对象时,它可以将其传递给实际应用程序的主控制器,依此类推.我建议你阅读一些关于行为模式的内容以获得一些想法.

greetz
back2dos