我在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
}
这个简单的场景为我们的类添加了以下依赖关系和缺点:
为了解决这些问题,我被建议做“事件驱动编程”或松散耦合。据我所知,这基本上意味着必须使类彼此独立,然后将它们绑定在不同的绑定器中。
问题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”中,我只需要使用一次实例化的类,例如。登录屏幕。如果有多个类的实例,例如。国际象棋游戏中的玩家类。
好吧,关于如何,我会指出我的5 commandments给你。 :)
对于这个问题,只有3个非常重要:
从SRP开始,你必须问自己一个问题:“X级的责任是什么?”。
登录屏幕负责向用户呈现界面以填写并提交他的登录数据。从而
首先,您让我们抽象登录服务:
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,例如在多个模块中添加日志,历史甚至是持久层。
通常,尽量不要模块化或拆分应用程序的一小部分。只要你的代码库中有一些意大利面就可以了
尝试将整个应用程序拆分为自治部分,通过简洁的界面进行交互。如果一个部分变得太大,重构它就会以同样的方式。
编辑:
回答汤姆的问题:
格尔茨 back2dos
首先,我根本不熟悉Haxe。但是,我会回答说这里描述的内容听起来与我在.NET中学习的方式非常相似,所以听起来这对我来说是好的做法。
在.NET中,当用户单击按钮执行某些操作(如登录)时会触发“事件”,然后执行“处理”事件的方法。
总会有代码描述当触发另一个类中的事件时在一个类中执行的方法。它并非不必要的复杂,它必然是复杂的。在Visual Studio IDE中,大部分代码都隐藏在“设计器”文件中,所以我没有定期看到它,但是如果你的IDE没有这个功能,你必须自己编写代码。
至于它如何与您的自定义装载程序类一起使用,我希望有人可以为您提供答案。