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

问题描述 投票:2回答: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
  • 没有我们的自定义按钮类,将无法加载登录屏幕
  • 没有我们的自定义Socket Connection类,将不会加载登录屏幕
  • 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");

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

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

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

MainBinder:
feature = new Main();    

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

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

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

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

events oop functional-programming haxe aop
2个回答
9
投票

好吧,关于如何,我会指出我的5 commandments给你。 :)

对于这个问题,只有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();

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

就像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,在这种情况下,这不是正确的方法。将组件彼此连接起来并不是一个聪明的黑客,而是处理cross-cutting concerns,例如在多个模块中添加日志,历史甚至是持久层。

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

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

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

编辑:

回答汤姆的问题:

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

格尔茨 back2dos


1
投票

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

在.NET中,当用户单击按钮执行某些操作(如登录)时会触发“事件”,然后执行“处理”事件的方法。

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

至于它如何与您的自定义装载程序类一起使用,我希望有人可以为您提供答案。

© www.soinside.com 2019 - 2024. All rights reserved.