Bien, concernant le comment j'aimerais souligner que mon 5 commandements à vous. :)
Pour cette question, seuls 3 sont vraiment importants :
- responsabilité unique (SRP)
- ségrégation d'interface (ISP)
- inversion de dépendance (DIP)
En commençant par SRP vous devez vous poser la question : "Quelle est la responsabilité de la classe X ?".
L'écran de connexion est chargé de présenter une interface à l'utilisateur pour qu'il puisse remplir et soumettre ses données de connexion. Ainsi,
- il est logique qu'il dépende de la classe de bouton, car il a besoin du bouton.
- ça n'a aucun sens, il fait tout le travail en réseau, etc.
Tout d'abord, abstrayons le service de connexion :
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
Du point de vue de la classe Main, l'écran de connexion ressemble à ceci :
interface ILoginInterface {
function show(inputHandler:String->String->Void):Void;
function hide():Void;
function error(reason:String):Void;
}
l'exécution de la connexion :
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
Maintenant ILoginService
a l'air d'être un peu défectueux. Mais pour être honnête, il fait tout ce qu'il doit faire. Maintenant, il peut effectivement être implémenté par une classe Server
qui encapsule tous les réseaux dans une seule classe, avec une méthode pour chacun des éléments suivants N les appels que votre serveur actuel fournit, mais tout d'abord, ISP suggère, que de nombreuses interfaces spécifiques aux clients sont préférables à une interface à usage général . Pour la même raison ILoginInterface
est vraiment réduit à son strict minimum.
Quelle que soit la manière dont ces deux éléments sont mis en œuvre, vous n'aurez pas besoin de changer Main
(à moins bien sûr que l'interface ne change). Ceci est DIP en cours d'application. Main
ne dépend pas de l'implémentation concrète, mais seulement d'une abstraction très concise.
Maintenant, passons à des implémentations :
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, c'était plutôt direct, je suppose. Mais juste pour le plaisir, faisons quelque chose de vraiment stupide :
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."
}
}
Aussi pédestre que cette authentification puisse être, du point de vue de la classe principale, cela ne change rien et fonctionnera donc tout aussi bien.
Un scénario plus probable est en fait que votre service de connexion se trouve sur un autre serveur (éventuellement un serveur HTTP), qui effectue l'authentification et, en cas de succès, crée une session sur le serveur d'applications actuel. Du point de vue de la conception, cela pourrait se traduire par deux classes distinctes.
Maintenant, parlons du "..." que j'ai laissé dans Main. Eh bien, je suis paresseux, donc je peux vous dire que dans mon code, vous êtes susceptible de voir
var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();
Bien sûr, c'est loin d'être la façon la plus propre de procéder. La vérité est que c'est la manière la plus simple de procéder et que la dépendance est limitée à une occurrence, qui peut être supprimée ultérieurement par le biais de injection de dépendances .
À titre d'exemple simple pour un IoC -Conteneur en 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;
}
}
Utilisation élégante (avec using
mot-clé) :
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();
De cette façon, il n'y a pratiquement pas de couplage entre les différentes classes.
Pour ce qui est de la question de savoir comment faire passer les événements entre le bouton et l'écran de connexion :
c'est juste une question de goût et de mise en œuvre. L'intérêt de la programmation événementielle est que la source et l'observateur ne sont couplés que dans le sens, que la source doit envoyer une sorte de notification et que la cible doit être capable de la gérer. someButton.onClick = handler;
fait exactement cela, mais c'est tellement élégant et concis que vous n'en faites pas un plat. someButton.onClick(handler);
est probablement un peu mieux, puisque vous pouvez avoir plusieurs gestionnaires, bien que cela soit rarement nécessaire pour les composants de l'interface utilisateur. Mais au final, si vous voulez des signaleurs, optez pour les signaleurs.
Maintenant, quand il s'agit d'AOP, ce n'est pas la bonne approche dans cette situation. Il ne s'agit pas d'un piratage astucieux pour relier des composants entre eux, mais de la gestion de questions transversales Il s'agit par exemple d'ajouter un journal, un historique ou même des éléments servant de couche de persistance dans une multitude de modules.
En règle générale, essayez de ne pas modulariser ou diviser les petites parties de votre application. Il n'y a pas de problème à avoir un peu de spaghetti dans votre base de code, tant que
- les segments de spaghetti sont bien encapsulés
- les segments spaghetti sont suffisamment petits pour être compris ou autrement refactorés/réécrits dans un délai raisonnable, sans casser l'application (ce que le point n° 1 devrait garantir).
Essayez plutôt de diviser l'ensemble de l'application en parties autonomes, qui interagissent par le biais d'interfaces concises. Si une partie devient trop grande, refactorez-la de la même manière.
éditer :
En réponse aux questions de Tom :
- C'est une question de goût. Dans certains frameworks, les gens vont jusqu'à utiliser des fichiers de configuration externes, mais cela n'a guère de sens avec haXe, puisque vous devez demander au compilateur de forcer la compilation des dépendances que vous injectez au moment de l'exécution. Configurer la dépendance dans votre code, dans un fichier central, représente tout autant de travail et est beaucoup plus simple. Pour plus de structure, vous pouvez diviser l'application en "modules", chaque module ayant une classe de chargement responsable de l'enregistrement des implémentations qu'il fournit. Dans votre fichier principal, vous chargez les modules.
- Cela dépend. J'ai tendance à les déclarer dans le package de la classe qui en dépend et à les refactorer plus tard dans un package supplémentaire au cas où ils s'avéreraient nécessaires ailleurs. En utilisant des types anonymes, vous pouvez aussi découpler complètement les choses, mais vous aurez une légère baisse de performance sur des plateformes comme flash9.
- Je ne ferais pas l'abstraction du bouton pour ensuite injecter une implémentation via IoC, mais vous êtes libre de le faire. Je le créerais explicitement, car au final, ce n'est qu'un bouton. Il a un style, une légende, une position et une taille d'écran et déclenche des événements de clic. Je pense que c'est une modularisation inutile, comme indiqué ci-dessus.
- Tenez-vous-en à SRP. Si vous le faites, aucune classe ne deviendra inutilement grande. Le rôle de la classe Main est d'initialiser l'application. Quand c'est fait, elle doit passer le contrôle à un contrôleur de connexion, et quand ce contrôleur acquiert un objet utilisateur, il peut le passer au contrôleur principal de l'application actuelle et ainsi de suite. Je vous suggère de lire un peu sur schémas comportementaux pour avoir quelques idées.
salutations
back2dos