Hi Tom :)
well, concerning the how, I would point out my 5 commandments to you. :)
For this question only 3 are really important:
- single responsibility (SRP)
- interface segregation (ISP)
- dependency inversion (DIP)
Starting off with SRP, you have to ask yourself the question: "What is the responsibility of class X?".
The login screen is responsible for presenting an interface to the user to fill in and submit his login data. Thus
- it makes sense for it to depend on the button class, because it needs the button.
- it makes no sense it does all the networking etc.
First of all, you let's abstract the login service:
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
From the point of view of the Main class, the login screen looks like this:
interface ILoginInterface {
function show(inputHandler:String->String->Void):Void;
function hide():Void;
function error(reason:String):Void;
}
performing login:
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
Now ILoginService
looks a little titchy. But to be honest, it does all it needs to do. Now it can effectively be implemented by a class Server
, that encapsulates all networking in a single class, having a method for each of the N calls your actual server provides, but first of all, ISP suggests, that many client specific interfaces are better than one general purpose interface. For the same reason ILoginInterface
is really kept to its bare minimum.
No matter, how these two are actually implemented, you will not need to change Main
(unless of course the interface changes). This is DIP being applied. Main
doesn't depend on the concrete implementation, only on a very concise abstraction.
Now let's have some implementations:
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
}
Ok, that was pretty straight forward, I suppose. But just for the fun of it, let's do something really idiotic:
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."
}
}
As pedestrian as this authentication may be, from the point of view of the Main class,
this doesn't change anything and thus will work just as well.
A more likely scenario is actually, that your login service is on another server (possibly an HTTP server), that makes the authentication, and in case of success creates a session on the actual app server. Design-wise, this could be reflected in two separate classes.
Now, let's talk about the "..." I left in Main. Well, I'm lazy, so I can tell you, in my code you are likely to see
var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();
Of course, this is far from being the clean way to do it. The truth is, it's the easiest way to go and the dependency is limited to one occurrence, that can later be removed through dependency injection.
Just as a simple example for an IoC-Container in 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;
}
}
elegant usage (with using
keyword):
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();
This way, you practically have no coupling between the individual classes.
As to the question how to pass events between the button and the login screen:
this is just a matter of taste and implementation.
The point of event driven programming is that both the source and the observer are only coupled in the sense,
that the source must be sending some sort of notification and the target must be able to handle it.
someButton.onClick = handler;
basically does exactly that, but it's just so elegant and concise you don't make a fuzz about it.
someButton.onClick(handler);
probably is a little better, since you can have multiple handlers, although this is rarely required of UI components. But in the end, if you want signalers, go with signalers.
Now when it comes to AOP, it is not the right approach in this situation. It's not a clever hack to wire up components between one another, but about dealing with cross-cutting concerns, such as adding a log, a history or even things as a persistence layer across a multitude of modules.
In general, try not to modularize or split the little parts of your application.
It is ok to have some spaghetti in your codebase, as long as
- the spaghetti segments are well encapsulated
- the spaghetti segments are small enough to be understood or otherwise refactored/rewritten in a reasonable amount of time, without breaking the app (which point no. 1 should guarantee)
Try rather to split the whole application into autonomous parts, which interact through concise interfaces. If a part grows too big, refactor it just the same way.
edit:
In response to Tom's questions:
- that's a matter of taste. in some frameworks people go as far as using external configuration files, but that makes little sense with haXe, since you need to instruct the compiler to force compilation of the dependencies you inject at runtime. Setting up the dependency in your code, in a central file, is just as much work and far simpler. For more structure, you can split the app into "modules", each module having a loader class responsible for registering the implementations it provides. In your main file, you load the modules.
- That depends. I tend to declare them in the package of the class depending on them and later on refactor them to an extra package in case they prove to be needed elsewhere. By using anonymous types, you can also completely decouple things, but you'll have a slight performance hit on platforms as flash9.
- I wouldn't abstract the button and then inject an implementation through IoC, but feel free to do so. I would explicitely create it, because in the end, it's just a button. It has a style, a caption, screen position and size and fires click events. I think, this is unnecessary modularization, as pointed out above.
- Stick to SRP. If you do, no class will grow unneccessarily big. The role of the Main class is to initialize the app. When done, it should pass control to a login controller, and when that controller acquires a user object, it can pass it on to the main controller of the actual app and so forth. I suggest you read a bit about behavioral patterns to get some ideas.
greetz
back2dos