代码之家  ›  专栏  ›  技术社区  ›  Tom

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

  •  2
  • Tom  · 技术社区  · 16 年前

    我要做一个大项目,现在正忙着准备。对于这个问题,我将创建一个小场景:一个简单的应用程序,它有一个主类(这个类在应用程序启动时执行)和一个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将不会加载
    • 如果没有我们的自定义SocketConnection类,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:这有什么意义吗?这不是让事情变得不必要的复杂吗?

    另外,在上面的“绑定器”中,我只需要使用实例化一次的类,例如登录屏幕。如果一个类有多个实例,例如象棋中的一个玩家类,该怎么办。

    2 回复  |  直到 9 年前
        1
  •  10
  •   back2dos    16 年前

    好吧,关于 ,我会指出我的 5 commandments 致您。:)

    • 单一责任(SRP)
    • 相关性反转(DIP)

    从…开始 SRP公司 ,你必须问自己一个问题:“X班的责任是什么?”。

    登录屏幕负责向用户提供一个界面,以填写和提交其登录数据。因此

    1. 它依赖于button类是有意义的,因为它需要button。

    首先,让我们抽象一下登录服务:

    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
    

    从主类的角度来看,登录屏幕如下所示:

    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 调用实际服务器提供的,但首先, 网络服务提供商 这意味着 . 出于同样的原因 ILoginInterface 是真的保持在最低限度。

    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."
        }   
    }
    

    尽管这种认证可能很普通,但从主要类的角度来看,

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

    现在,让我们来谈谈我留在主楼的那个“…”。嗯,我很懒,所以我可以告诉你,在我的代码中你很可能会看到

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

    当然,这远不是一个干净的方法。事实是,这是最简单的方法,依赖性仅限于一次,以后可以通过 dependency injection

    作为一个简单的例子 IoC -haXe中的容器:

    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);

    现在说到AOP,在这种情况下它不是正确的方法。把组件连接起来并不是一个聪明的方法,而是处理 cross-cutting concerns

    在你的代码库里放一些意大利面条是可以的,只要

    1. 意大利面部分足够小,可以在合理的时间内理解或重构/重写,而不会破坏应用程序(第一点应该保证)

    编辑:

    针对汤姆的问题:

    1. 那是品味的问题。在某些框架中,人们甚至使用外部配置文件,但这对于haXe来说没有什么意义,因为您需要指示编译器强制编译在运行时注入的依赖项。在您的代码中,在一个中心文件中设置依赖关系,同样需要大量的工作,而且要简单得多。对于更多的结构,您可以将应用程序拆分为“模块”,每个模块都有一个负责注册其提供的实现的加载程序类。在主文件中,加载模块。
    2. 那要看情况了。我倾向于根据它们在类的包中声明它们,然后将它们重构到一个额外的包中,以防其他地方需要它们。通过使用匿名类型,还可以完全解耦,但在flash9平台上会有轻微的性能影响。
    3. 我不会抽象这个按钮,然后通过IoC注入一个实现,但可以这样做。我会明确地创建它,因为最终,它只是一个按钮。它有样式、标题、屏幕位置和大小,并触发单击事件。我认为,正如上面指出的,这是不必要的模块化。
    4. behavioral patterns

    尔兹

        2
  •  1
  •   Gama11 zzapper    7 年前

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

    至于这是如何与您的自定义加载程序类一起工作的,我希望这里有人能给您提供一个答案。