[Tutor] communication between class instances

Magnus Lycka magnus@thinkware.se
Fri Dec 6 06:27:02 2002


At 17:16 2002-12-05 -0700, Poor Yorick wrote:
>Magnus Lycka wrote:
> > I'm not sure what you mean that "app" should be but I try to
> > make sure that my program logic is unaware of the GUI.
>
>The classes were just sort of a pseudocode for the structure I've
>created.  The class that I called GUI just contains all the code for the
>user interface.

What GUI tool kit did you intend to use? There's hardly any
point to try to write GUI classes until you have a GUI tool kit.
I'm not sure your pseudocode resembles any real code enough to
learn anything from.

>   I am struggling with just the thing you mentioned.  How
>do I keep the program logic unaware of the GUI when the program depends
>on information gathered by the GUI widgets.

The GUI must obviously be aware of the public interfaces of the
application logic.

The GUI will call the appropriate public method in the application
logic. Typically, things won't happen the other way around. Although
due to dynamic nature of python, you can have the application calling
the GUI without being aware of what the GUI is like... See below.

>Generally, the concept that
>I seem to be butting up against is how should instances of objects
>communicate with each other?

As little as possible! :)

Keep related data or behavoiur in one place.
Minimize the number of classes with which another class collaborates.
Minimize the amount of collaboration between a class and its collaborator.

>  Of course I can have them exchange
>references to each other, but if I do that, is there really a point to
>keeping them separate as classes?

There is nothing technical that prevents you from mixing GUI and
application logic in a class. I usually don't though. It gets very
difficult to do automatic unit tests and debugging of the logic part,
In case you realize that you want to use the logic layer with a
different GUI or some other interface such as CGI, you need a clear
separation.

>And what happens to flow control when
>the program instance is doing its own thing and at the same time widgets
>are generating calls to methods of the program instance?

Aha. You don't know how event based programs work, do you? I
can understand that this is confusing at first. In a normal
single threaded application, only one thing is happening at any
given time.

In GUI programs, you typically leave the flow control to the
event loop of the GUI tool kit. You provide methods in your GUI
code that will be called when various user initiated events occur
(key presses, mouse clicks etc). These methods in your GUI code
will call your application logic as appropriate. It will send from
the GUI to the application logic, and it will get new data back to
present in the GUI controls and windows.

This means that the routines in your application code should
typically be able to return in a short time, or the user interface
will seem dead. For longer running tasks, you need to employ some
special method, such as running that task in a separate thread, or
to divide it into several smaller tasks.

Here's a trivial GUI code example that you can try out:
---------------------------
import Tkinter

class Gui:
     def __init__(self, master):
         self.button = Tkinter.Button(master, text='1', command=self.do)
         self.button.pack()
         self.logic = Logic()

     def do(self):
         value = long(self.button['text'])
         answer = self.logic.call(value)
         self.button['text'] = str(answer)

class Logic:
     def call(self, value):
         return value * 2


root = Tkinter.Tk()

Gui(root)

root.mainloop()
---------------------------
It doesn't do much, but it shows you the general structure of a
GUI program. The "business logic" (multiply a given value with
2) is kept away from the GUI. The GUI only knows the public
interface of the logic: What method to call, what kind of
parameters to provide, and what kind of return values to expect.
Also the business logic is totally unaware of the fact that it's
being used by a button in a GUI.

In the following example, I've tried to code a simple calculator in
such a way that the GUI code and the application logic should be
independent of each other. It should be possible to write a GUI front
end for the current backend in any GUI tool kit. The current one is
wxPython, but Tkinter or Swing should certainly be possible. It
should also be possible to change backend, adding more mathematical
functions and buttons without changing the GUI code. This makes this
a bit more complex than usual, since the GUI has a limited knowledge
of what it will look like, and what it might do.

So, here I actaully let the logic layer get references to three methods
in the GUI layer. This is because the GUI isn't intended to know when to
expect various things to happen, such as display changes or application
exit. As I said, the GUI should be generic and able to handle backend
changes without being rewritten. You might call this reverse callbacks.
When an event (clicking on a button) occurs, the GUI will call a callback
function in the application layer. The conventional solution would be that
these callback methods returned different values to the GUI that could be
presented. But in this particular application the GUI doesn't know what
to expect from the various callback methods, so instead it provides the
three "reverse callback methods" to the logic layer and lets that signal
things back to the user.

This is a bit similar to the event mechanisms in advanced client server
systems like BEA Tuxedo that have features that let the server take
initatives to send messages to the clients. General rules are good,
but sometimes it's better to break them...

In other words, the logic layer here isn't completely unaware of what
context it's used in. It is only intended to be used for an application
that simulates a pocket calculator. It IS completely unaware of HOW the
GUI is implemented though. It could be wxPython, Tkinter, Swing/Jython
as a standalone app or as an applet etc. It might even be possible to
write a CGI application with HTML forms that use this backend. It can
only assume that its user provides the three functions that are needed
on instantiation, and that it understands how to handle the data returned
in getLayout.

Perhaps this isn't the best second example of GUI code to look at. It
violates the general rule of just calling the logic from the GUI and
not vice versa. It still does show a very strict separation of concern
between presentation and logic though.

Here is the logic layer. While it expects three callback methods, it
doesn't expect much of them. You can run it interactively from the
python interpreter.
-----------------------------------
# pycalc.py
from __future__ import division # Don't truncate on 2/3 etc
_copyright = 'Copyright Thinkware AB, Sweden, 2002'
_version = '0.2'

class Calculator:
     '''This is a base version of a calculator. The front end expects
     __init__ and getLayout to look the way they do, and will call any
     callback method it received from getLayout with the text in the
     button as the only parameter.'''

     def __init__(self, displayMethod, helpMethod, closeMethod):
         '''Create a (logic) calculator, and provide three callback
         methods to the GUI. (Reverse callback you might say...)
         displayMethod should be able to take a string as argument,
         and present this string to the user.
         helpMethod will be called when the user presses a button
         associated with the help method. Similarly, closeMethod will
         be called when a button associated with close method is pressed.'''
         self._display = displayMethod
         self._help = helpMethod
         self._close = closeMethod
         self._buffer = []

     def getLayout(self):
         '''Return a list of lists giving the text and callback
         method for each button on the calculator.'''
         a = self.addSymbol
         clr = self.clear
         b = self.back
         h = self.help
         clc = self.calc
         cls = self.close
         return [[('7', a), ('8', a), ('9', a), ('/', a), ('C', clr),],
                 [('4', a), ('5', a), ('6', a), ('*', a), ('CE', b),],
                 [('1', a), ('2', a), ('3', a), ('-', a), ('Off', cls),],
                 [('0', a), ('.', a), ('=', clc), ('+', a), ('?', h),]]

     def close(self, dummy):
         '''Call close method provided by UI'''
         self._close(dummy)

     def help(self, dummy):
         '''Call help method provided by UI'''
         self._help('Backend: %s %s\n%s' % (__name__, _version, _copyright))

     def _update(self):
         '''Update display with internal buffer content'''
         self._display("".join(self._buffer))

     def addSymbol(self, symbol):
         '''Add an entered symbol to the internal buffer'''
         self._buffer.append(symbol)
         self._update()

     def clear(self, dummy):
         '''Clear the internal buffer'''
         self._buffer = []
         self._update()

     def back(self, dummy):
         '''Remove last symbol in internal buffer'''
         if self._buffer:
             del self._buffer[-1]
             self._update()

     def calc(self, dummy):
         '''Replace internal buffer content with result of evaluating it'''
         try:
             expr = "".join(self._buffer)
             result = eval(expr)
             self._buffer = list(str(result))
             self._update()
         except:
             self._buffer = []
             self._display('ERROR')
-----------------------------------

Here is a little interactive experiment without a GUI.

Python 2.2.1 (#34, Sep 27 2002, 18:37:42) [MSC 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
 >>> import pycalc
 >>> def close(x):
...     print "Close app"
...
 >>> def display(x):
...     print x
...
 >>> help=display
 >>> calc = pycalc.Calculator(display, help, close)
 >>> keyFunc = calc.getLayout()
 >>> print keyFunc
[[('7', <bound method Calculator.addSymbol of <pycalc.Calculator instance 
at 0x0
07B5AE8>>), ('8', <bound method Calculator.addSymbol of <pycalc.Calculator 
insta
[and so on...]
 >>> seven = keyFunc[0][0]
 >>> seven[1](seven[0])
7
 >>> seven[1](seven[0])
77
 >>> plus = keyFunc[3][3]
 >>> print plus
('+', <bound method Calculator.addSymbol of <pycalc.Calculator instance at 
0x007
B5AE8>>)
 >>> plus[1](plus[0])
77+
 >>> seven[1](seven[0])
77+7
 >>> equals = keyFunc[3][2]
 >>> print equals
('=', <bound method Calculator.calc of <pycalc.Calculator instance at 
0x007B5AE8
 >>)
 >>> equals[1](equals[0])
84
 >>> hlp = keyFunc[3][4]
 >>> hlp[1](hlp[0])
Backend: pycalc 0.2
Copyright Thinkware AB, Sweden, 2002
 >>>

Certainly not the most convenient calculator interface, but it's
possible to write test scripts, and debug things etc. It seems to
work: 77+7 = 84. If the GUI will make function calls as above, it
will be possible to display entered data and show calculation results.

Here is a presentation layer written for wxPython:
-----------------------------------
# wxpycalc.py
_copyright = 'Copyright Thinkware AB, Sweden, 2002'
_name = 'wxPyCalc'
_version = '0.2'

from wxPython.wx import *
from wxPython.lib.rcsizer import RowColSizer
from pycalc import Calculator

class MyFrame(wxFrame):
     '''This is a generic GUI for calculators. It provides the actual
     calculator with (reverse) callback methods for display, showing
     help messages and closing the application.
     On the top of the GUI it will present a display field. The content
     of this display field is determined by the display callback method
     mentioned above.
     After instanciating the calculator, it will fetch information about
     a grid of buttons to show below the display.'''
     def __init__(self, parent, title, pos=wxDefaultPosition,
                  size=wxDefaultSize, style=wxDEFAULT_FRAME_STYLE ):
         wxFrame.__init__(self, parent, -1, title, pos, size, style)
         panel = wxPanel(self, -1)
         sizer = RowColSizer()

         calculator = Calculator(self.display, self.help, self.OnCloseMe)
         numberOfColumns = len(calculator.getLayout()[0])

         self.displayBox = wxTextCtrl(panel, -1, "", style=wxTE_READONLY)
         sizer.Add(self.displayBox, row=0, col=0, colspan=numberOfColumns,
                   flag=wxEXPAND)

         x = 1
         for row in calculator.getLayout():
             y = 0
             for text, method in row:
                 button = wxButton(panel, -1, text, size=wxSize(30,30))
                 button.myMethod = method
                 sizer.Add(button, flag=wxEXPAND, row = x, col=y)
                 EVT_BUTTON(self, button.GetId(), self.callback)
                 y += 1
             x += 1

         panel.SetSizer(sizer)
         panel.SetAutoLayout(true)

         EVT_CLOSE(self, self.OnCloseWindow)

     def help(self, msg):
         dlg = wxMessageDialog(self,
                 ('A simple Python calculator\n%s %s\n%s\n\n%s' %
                  (_name, _version, _copyright, msg)),
                 'About %s' % _name, wxOK | wxICON_INFORMATION)
         dlg.ShowModal()
         dlg.Destroy()

     def callback(self, evt):
         button = evt.GetEventObject()
         button.myMethod(button.GetLabel())

     def display(self, data):
         self.displayBox.SetValue(data)

     def OnCloseMe(self, evt):
         self.Close(true)

     def OnCloseWindow(self, evt):
         self.Destroy()

class MyApp(wxApp):
     def OnInit(self):
         self.frame = MyFrame(NULL, _name, size = wxSize(160, 170))
         self.frame.Show(true)
         self.SetTopWindow(self.frame)
         return true

def main():
     app = MyApp(0)
     app.MainLoop()

if __name__ == '__main__':
     main()
-----------------------------------

It's left as an exercise to the reader to make new frontends
(Tkinter, Swing etc) or backends (hex-calculator, trigonometry
etc). It might be a good thing to be able to swap backend on
the fly as well. I guess this means the "import pycalc" needs
to be replaced with something more sofisticated. Another neat
extension would be to make it aware of key presses, at least for
the buttons that only have one character on the key top. This
can be done without changing the backend.


-- 
Magnus Lycka, Thinkware AB
Alvans vag 99, SE-907 50 UMEA, SWEDEN
phone: int+46 70 582 80 65, fax: int+46 70 612 80 65
http://www.thinkware.se/  mailto:magnus@thinkware.se