2 votes

La programmation libre dans les langages de haut niveau, comment, pourquoi et combien ?

J'écris mon code en Haxe. Mais cela n'a rien à voir avec la question, tant que vous gardez à l'esprit qu'il s'agit d'un langage de haut niveau, comparable à Java, ActionScript, JavaScript, C#, etc.

Je vais travailler sur un grand projet et je suis en train de me préparer. Pour cette question, je vais cependant créer un petit scénario : une application simple qui a une classe Main (celle-ci est exécutée au lancement de l'application) et une classe LoginScreen (il s'agit en fait d'une classe qui charge un écran de connexion pour que l'utilisateur puisse se connecter).

Typiquement, je suppose que cela ressemblerait à ce qui suit :

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
}

Ce simple scénario ajoute les dépendances et les inconvénients suivants à nos classes :

  • L'écran principal ne se charge pas sans l'écran de connexion
  • LoginScreen ne se chargera pas sans la classe de chargeur personnalisée
  • LoginScreen ne se chargera pas sans notre classe de bouton personnalisée
  • L'écran de connexion ne se chargera pas sans notre classe SocketConnection personnalisée.
  • SocketConnection (qui devra être accédé par de nombreuses classes différentes à l'avenir) a été placé à l'intérieur de LoginScreen maintenant, ce qui n'a en fait aucun rapport avec lui, à part le fait que LoginScreen requiert une connexion socket pour la première fois.

Pour résoudre ces problèmes, on m'a suggéré de faire de la "programmation pilotée par les événements", ou du couplage lâche. D'après ce que je comprends, cela signifie essentiellement que l'on doit rendre les classes indépendantes les unes des autres, puis les relier entre elles dans des classeurs séparés.

Alors première question : mon point de vue sur la question est-il vrai ou faux ? Doit-on utiliser des classeurs ?

J'ai entendu dire que la programmation orientée vers les aspects pouvait être utile ici. Malheureusement, Haxe ne supporte pas cette configuration.

Cependant, j'ai accès à une bibliothèque d'événements qui me permet essentiellement de créer un signaleur (public var loginPressedSignaller = new Signaller()), de déclencher un signaleur (loginPressedSignaller.fire()) et d'écouter un signaleur (someClass.loginPressedSignaller.bind(doSomethingWhenLoginPressed)).

Donc, avec un peu plus de recherche, j'ai pensé que cela changerait ma configuration précédente :

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

Ce code n'est pas encore terminé, par exemple, je dois encore écouter la réponse du serveur, mais vous comprenez probablement où je suis coincé.

Question 2 : Cette nouvelle structure a-t-elle un sens ? Comment dois-je résoudre les problèmes mentionnés ci-dessus concernant les délimiteurs ! !!?

Puis j'ai entendu parler des classeurs. Je devrais peut-être créer un classeur pour chaque cours, pour tout relier ensemble. Quelque chose comme ça :

MainBinder:
feature = new Main();    

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

etc... j'espère que vous comprenez ce que je veux dire. Ce billet devient un peu long, je dois donc le conclure.

Question 3 : cela a-t-il un sens ? Cela ne rend-il pas les choses inutilement complexes ?

De plus, dans les "classeurs" ci-dessus, je n'ai eu à utiliser que des classes qui ne sont instanciées qu'une seule fois, par exemple un écran de connexion. Qu'en est-il s'il y a plusieurs instances d'une classe, par exemple une classe de joueur dans un jeu d'échecs.

10voto

back2dos Points 13253

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,

  1. il est logique qu'il dépende de la classe de bouton, car il a besoin du bouton.
  2. ç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

  1. les segments de spaghetti sont bien encapsulés
  2. 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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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

1voto

Tout d'abord, je ne suis pas du tout familier avec Haxe. Cependant, je répondrais que ce qui est décrit ici ressemble remarquablement à la façon dont j'ai appris à faire les choses en .NET, donc il me semble que c'est une bonne pratique.

Dans .NET, un "événement" se déclenche lorsqu'un utilisateur clique sur un bouton pour faire quelque chose (comme se connecter), puis une méthode s'exécute pour "gérer" l'événement.

Il y aura toujours du code décrivant quelle méthode est exécutée dans une classe lorsqu'un événement est déclenché dans une autre classe. Ce n'est pas inutilement complexe, c'est nécessairement complexe. Dans l'EDI Visual Studio, une grande partie de ce code est cachée dans des fichiers "designer", donc je ne le vois pas régulièrement, mais si votre EDI n'a pas cette fonctionnalité, vous devez écrire le code vous-même.

Quant à savoir comment cela fonctionne avec votre classe de chargeur personnalisée, j'espère que quelqu'un ici pourra vous fournir une réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X