GijsTimmers / kotnetcli

An easy automated way to log in on Kotnet.
GNU General Public License v3.0
5 stars 4 forks source link

How can we make Communicators reliable and portable #49

Closed jovanbulck closed 7 years ago

jovanbulck commented 9 years ago

Problems with the Communicator pattern

I think the switch to the communicator pattern has been one of the key moves in the kotnetcli project (i.a. discussed in #13). I strongly belief it is the good way to continue on. It nicely decouples core from visualisation concerns, allowing a pluggable visualisation system for everyones needs. It also keeps the code clean, allowing the programmer to focus on his task: the UNIX philosophy - do one thing and do it good.

Currently the pattern merely encapsulates all the visualisation calls. Initiating it can still be messy somehow. As we are porting kotnetcli on various platforms now however, time has come to enhance the pattern once more.

As discussed in various issues already (#48 #43), the current communicator implementations aren't portable / cross platform. This creates a dependency hell and makes keeping everything stable and up-to-date a hassle... :-/

Tackle the problem, not its symptoms

I belief this problem is not due to the communicator pattern an sich though: we shouldn't solve the problem by tackling its symptoms. That is to say: the real problem here is that specific Communicator instantiations currently aren't portable. The CursesCommunicator or DialogCommunicator for example happen to be to worst: i.e. the 'symptoms'. The problem itself is that the current code somehow expects a client to use all of the communicators (by importing them all) which then are instantiated according to the command-line flags passed.

What we should really fix therefore is the portability of communicators, the core of the problem.

Requirements for a solution

In my opinion, a good portable solution to this problem should have the following characteristics:

I'll quickly sketch a possible outline for a solution here:

Sorry for the looong post, but I think it's good to share some thoughts on this one ;-) I'll try to come up with more specific implementation ideas / class hierarchies and things soon...

Please comment below for impressions / ideas / comments / etc

GijsTimmers commented 9 years ago

Thank you, this was an interesting read.

Some thoughts:

import dependencies on init()

This is an interesting idea, but note that it explicitly doesn't comply with PEP 8.

crash platform-independent

If I understand correctly, you would like to have something like this:

class ColoramaCommunicator():
    def __init__(self):
        try:
            import colorama
        except ImportError:
            self.exitFailure("De colorama-module is niet beschikbaar.")
        ...
        ...
        ...
    def exitFailure(msg):
        print msg
        show_terminal_cursor()

One thing I see here is that ColoramaCommunicator() is no longer solely a 'visualising' class: it also controls itself (see self.exitFailure()). Could you perhaps expand a bit on this?

independance of command line arguments

If I get it right, what you're trying to say is that you should, for example, be able to use kotnetcli --bubble on a system even if that system doesn't support it. kotnetcli --help shouldn't show the option though. Is that right?

jovanbulck commented 9 years ago

Good catches, some replies below:

import dependencies on init()

This is an interesting idea, but note that it explicitly doesn't comply with PEP 8.

You're right indeed, I wasn't sure this is possible in Python. We should look into another way of realizing the crash locally criterion above (i.e. import only when needed). I think this criterion is crucial and should somehow be accomplished.

I originally picked up the idea of importing on init from this link, where we can maybe look a bit further. This link confirms indeed it can be very ugly.

I looked into it a bit further now. The idea/problem is not unknown to pyhton programmers ;-) It is sometimes called lazy import. Check out some interesting links : 1 2 3

[1] is definitely most interesting for us, it also quotes PEP you mentioned. Moreover the top answer states:

The best reasons I've seen to perform lazy imports are:

Optional library support. If your code has multiple paths that use different libraries, don't break if an optional library is not installed.

Long story short: we should look into a clean way of doing this and then not having the imports at the top of the file is no big deal I think...

One thing I see here is that ColoramaCommunicator() is no longer solely a 'visualising' class: it also controls itself (see self.exitFailure()). Could you perhaps expand a bit on this?

Good point indeed, but the rationale here would be that a Communicator instance is allowed to keep state, in order to do its cohesive job, i.e. visualizing. Being 'allowed' to keep internal state is indeed something you really need for advanced communicators. As you explicitly said above, the communicator "also controls itself", which is exactly what we want I think. Once you allow a communicator to hold internal state, it should also be responsible for it itself. That of course implies that the other code then is responsible to call co.exitFailure() when needed. This again abstracts the internal state solely to the communicator instance. You should think of it as a private thing, only to be managed and known by the Communicator, I think.

independance of command line arguments

If I get it right, what you're trying to say is that you should, for example, be able to use kotnetcli --bubble on a system even if that system doesn't support it. kotnetcli --help shouldn't show the option though. Is that right?

No that's not really what I'm trying to say here. It is indeed a bit fuzzy, but in fact what I want to express here is straightforward: Communicators shouldn't 'rely' in any way on external code that should make sure the correct platform-dependent instance is instantiated. Communicators should always fail gracefully. That is to say, never 'trust' that they are correctly instantiated. More specific example: an OSXBubbleCommunicator should in theory 'work', i.e. fail gracefully, on a Linux system.

About the --help then: I like the idea of hiding (or disabling for that matter; shouldn't matter as I explained above) things that any way won't work on a specific platform. The only pitfall here is that - as explained above - Communicator code shouldn't rely on it in any way. Indeed, the parsing of option flags is completely independent and merely a front-end to the communicators.

jovanbulck commented 9 years ago

I see communicator getting stuffed up with if "inloggen" then blabla elif "uitloggen" otherblabla ... checks. See e.g. the last (very useful I think) enhancement eventSamenvattingGeven.

Such thins are commonly called bad smells, indicating the need for refactoring the code.

Apart from the more drastic changes sketched above and here, I propose the following:

Communicators should be thought of as a kind of Strategy pattern or Template method pattern. This would justify option ii: further subclass communicators where needed.

I think option ii is a good one, but we maybe wont want the kotnetcli class to know about which communicators have specific subclasses and which not. We should therefore maybe consider an intermediate communicatorCreator level

jovanbulck commented 9 years ago

I got it! This is a nice example of a Factory pattern The key idea is to encapsulate the login/logout dependent creation of Communicators in another object: the factory. Checkout the wikipedia links to get the overall idea if you're not familiar with it...

kotnetcli would look like:

def aanstuurderObvArgumenten(argumenten):
    ...
    ############## switch on login-type flags ##############
    if argumenten.worker == "login":
        print "ik wil inloggen"
        fac = LoginCommunicatorFactory()

    elif argumenten.worker == "logout":
        print "ik wil uitloggen"
        fac = LogoutCommunicatorFactory()

    ############## switch on communicator-related flags ##############
    if argumenten.communicator == "dialog":
        print "ik wil fancy dialogs"
        co = fac.createDialogCommunicator()

    elif argumenten.communicator == "bubble":
        print "ik wil bellen blazen"
        co = fac.createBubbleCommunicator()

     ...

Creating of communicators is thus effectively encapsulated in the factory. kotnetcli.py remains responsible for the command line interface only. This will definitely be the next grand step forward for the overall design quality :-) :+1:

GijsTimmers commented 9 years ago

Thank you for looking this up. This should be implemented before releasing 2.0.0.

GijsTimmers commented 9 years ago

So that means that communicator.py will look something like this:

class LoginCommunicatorFactory():
    class CreateQuietCommunicator(self):
        def __init__(self):
            pass
        def eventNetloginStart(self):
            pass

        (...)

    class CreatePlaintextCommunicator(self.CreateQuietCommunicator):
        def __init__(self):
            Fore.RED = ""

        (...)

class LogoutCommunicatorFactory():
    class CreateQuietCommunicator(LoginCommunicatorFactory.CreateQuietCommunicator):
        def some_specific_logout_feature(self):

        (...)

Right?

jovanbulck commented 9 years ago

No, not at whole. I see the pseudo code I posted confused you since the method names are wrongly capitalized. Fixed it above

The idea of a factory pattern is to encapsulate the creation of the correct object in a complex hierarchy in another object: the factory.

The factory provides a method for every communicator and returns the correct communicator depending whether its a login/logout factory.

Checkout the wiki description: https://en.wikipedia.org/wiki/Abstract_factory_pattern

jovanbulck commented 9 years ago

The idea is to remove all uit_te_voeren_procedure like code. Which is as explained above a bad smell. Any uit_te_voeren_procedure-specific code should be implemented in separate sub classes.

In order to create the correct (sub)class communicator, we'll then use a factory:


## Abstract super class (not intended to directly create), encapsulating things common
## to a Login- and LogoutSummaryCommunicator
class SuperSummaryCommunicator(QuietCommunicator):
    def eventPingFailure(self):
        print "Niet verbonden met het KU Leuven-netwerk."
    def eventPingAlreadyOnline(self):
        print "U bent al online."

class LoginSummaryCommunicator(SuperSummaryCommunicator):

    def eventTegoedenBekend(self, downloadpercentage, uploadpercentage):
        print "Download: " + str(downloadpercentage) + "%" + ",",
        print "Upload: " + str(uploadpercentage) + "%"

    def beeindig_sessie(self, error_code=0):
        if error_code == 0:
            print "login succesvol."
        else:
            print "login mislukt."

class LogoutSummaryCommunicator(SuperSummaryCommunicator):

    def beeindig_sessie(self, error_code=0):
        if error_code == 0:
            print "logout succesvol."
        else:
            print "logout mislukt."

## The abstract factory specifying the interface and maybe returning 
## some defaults (or just passing)
class SuperCommunicatorFabriek:
   def createSummaryCommunicator():
     pass

class LoginCommunicatorFabriek(SuperCommunicatorFabriek):
   def createSummaryCommunicator():
     LoginSummaryCommunicator()

class LogoutCommunicatorFabriek(SuperCommunicatorFabriek):
     def createSummaryCommunicator():
       LogoutSummaryCommunicator()

### kotnetcli.py

def aanstuurderObvArgumenten(argumenten):
    ...
    ############## switch on login-type flags ##############
    if argumenten.worker == "login":
        print "ik wil inloggen"
        fac = LoginCommunicatorFabriek()

    elif argumenten.worker == "logout":
        print "ik wil uitloggen"
        fac = LogoutCommunicatorFabriek()

    ############## switch on communicator-related flags ##############
    if argumenten.communicator == "summary":
        print "ik wil fancy dialogs"
        ## next line will auto create correct instance :-)
        co = fac.createSummaryCommunicator()

     ...
jovanbulck commented 9 years ago

the nice thing is that we can then encapsulate all action-specific communication nicely

e.g. print "Netlogout openen....... " instead of "Netlogin openen....... " on reply to a eventNetloginStart()

GijsTimmers commented 9 years ago

Thank you for taking the time and effort to provide this piece of code, it's helped me understand what you want to do exactly. I read the Wikipedia page, but I didn't get it.

The code example you provided looks very interesting, my hands are itching to code again and get this factory model done. Maybe I can do some work tonight.

jovanbulck commented 9 years ago

:+1:

GijsTimmers commented 9 years ago

I'm working on the code now. I think I didn't really get it.

class LoginCommunicatorFabriek(SuperCommunicatorFabriek):
   def createSummaryCommunicator():
     LoginSummaryCommunicator()

Why not just put the contents of LoginSummaryCommunicator() in the LoginCommunicatorFabriek()?

jovanbulck commented 9 years ago

Why not just put the contents of LoginSummaryCommunicator() in the LoginCommunicatorFabriek()?

No the factory is solely responsible for creating the object. It is called a creational design pattern, for outsourcing creation of an object in a complex hierarchy.

When putting the implementation there, you (1) lose cohesion (2) it's impossible, since this is a constructor, there are various other methods, you cant just 'paste in' (eg kuleuvenEvent(), ....). A method ain't a class...

jovanbulck commented 9 years ago

lazy importing as discussed above can easily be achieved in the corresponding factory methods: 980af26c717909b4d01bc007cf84e656efcd2731

This allows kotnetcli to run without first imporiting all packages, even those not used (eg dialog, notify2, ...)

To test: change the colorama import to something non-existing in coloramac.py and try to run kotnetcli.py -t --> should work :)

GijsTimmers commented 8 years ago

Jo, how much work is left on this issue? As far as I can tell, we only need to finish the lazy imports, right?

jovanbulck commented 8 years ago

Jo, how much work is left on this issue? As far as I can tell, we only need to finish the lazy imports, right?

Yes, proof-of-concept (including lazy imports) works indeed. We need to decide on the definitive communicator API and finish things.

GijsTimmers commented 8 years ago

Alright. Jo, you're a lot better than I am when it comes to software design. Could you go ahead and design the API?

jovanbulck commented 8 years ago

Ok, I'll try to look at it this weekend...

jovanbulck commented 8 years ago

106 proposes a simplified communicator interface with cleaner error handling separation of concerns between Worker and Communicator classes -- as also discussed in #59

jovanbulck commented 8 years ago

As one of the final things to be done here, I think we should move the credentials querying (i.e. the user input) to the Communicator interface.

Currently, the front-end (kotnetcli.py and friends) is responsible to ensure the Credentials object passed to the Worker does indeed have credentials. If not, the front-end prompts the user for credentials and stores them in the Credentials object.

Improved design outline:

Advantages:

jovanbulck commented 8 years ago

move the credentials querying (i.e. the user input) to the Communicator interface

Done!

jovanbulck commented 7 years ago

I consider this one done! Comminicators have been thoroughly refactored in the dev branch now.