One of Amiga E's (many) wonderful features is the inclusion of Object Oriented Programming. While it is not compulsory to use this facility (indeed, I am sure many E coders -- largely those who are used to programming in assembler -- may well never even have looked at it), in many respects adapting your programming style to include some object oriented design principles can be a very good idea.
For time and space reasons, I can't really assess the advantages of OO versus traditional programming; I'd be here for days, and no-one would want to read the article when I've finished. However, there are hundreds, nay thousands, of good books debating the differences between object oriented and traditional programming.
In summary, though, the idea behind OOP is to limit the application of a piece of code to the area of the implementation that it should be dealing with. Accidentally overwriting a global variable with a local one or accessing memory that does not belong to your task should become a thing of the past, once you have learnt and applied the core principles of object oriented design.
To some degree the use of OOP is a 'religious' matter -- another holy war on the scale of "My computer is better than your computer". However, the advantages of OO design -- more efficient and robust programs, as well as an easier design process -- should be considered separately from any attachment to a particular language or mode of programming; the principles can actually be applied without using an object-oriented programming language, it is just that these languages have been deliberately designed to facilitate the implementation of an object oriented design.
I don't want to merely repeat the words of the documentation on
E's OOP implementation; instead I'll pick out a few salient features
and analyse them in the context of a worked example. The example
(the full source of which is available as
messages.e) is a module designed
to simplify message passing between programs.
(Also, the source code for two example programs
messages_1.e and
messages_2.e are also available.)
Object oriented programming -- like any other specialised and
obfuscated area -- has its own jargon. There are a few fundamental
terms (easy enough to pick up) which I will describe here, but bear
in mind that this is by no means a complete summary of OOP.
OBJECT example_object
x
y
gadget_ptr:PTR TO gadget
ENDOBJECT
OBJECT this_is_a_subclass OF example_object
a
b
ENDOBJECT
and the object will inherit the variables and
procedures declared in example_object as well as
possessing those that it has declared on its own. For the
subclass, its full composition (if described separately)
would be
OBJECT another_example_object
x
y
gadget_ptr:PTR TO gadget
ENDOBJECT
PROC example_proc(x, y) OF example_object IS self.x+self.ywhere the self predicate refers to the object itself, and the variables to the variables which compose that object.
NEW example_object.init()and the destructor, which for any object must be specifed as, e.g.,
PROC end() OF example_object, is
END example_object
Despite the seeming uselessness of these terms, they serve as a sort
of verbal shorthand for the concepts that the object oriented programmer
finds him- (or her-) self dealing with.
Everyone understands the principle of using procedures to reduce duplication of source code. If you want to do something more than once in your program, why write it more than once? This is one of the principles that informs object oriented design. It enables the programmer (with a little careful thought) to design reusable code which can be used by many separate projects (or indeed, programmers) at once. In the old paradigm, if you needed to reuse code for another project, you copied it from the source of the old program; for larger projects, this rapidly became unwieldy. Now, of course, all this has changed. With the arrival of object oriented design, there is a way to avoid all of the bugbears of traditional programming practice; admittedly, a new set of problems may well arise, but they are on a completely different level to the mundanity and the chances for error that are laid at the door of traditional programming practices.
E supports this "modular" approach through MODULEs. Normally modules are used in E to remove source code from a file, and to make it available to more than one source file at atime (or, indeed, another module). This does not change much for object oriented code; it merely acts as a vehicle for the implementation of data-hiding.
In order to reduce the amount of work that a coder needs to do, it makes sense to make the interface to a procedure or a set of procedures as well defined as possible. Any code that relies on the internal construction of an object to achieve its aims will need to be changed when the object is; for this reason, it is better to hide the code through the MODULE mechanism, using the PRIVATE command. This means that client procedures will not be able to mess around with the internals of an object, and thus break when the specification of the object is changed. Instead they will be forced to access the object through a defined interface, which need not change when the internals of the object (i.e. the variables and methods that comprise that objects) are changed.
First, consider the problem that you have in front of you. In my case, I wanted to abstract away as much base-level code as possible from the message handling in my programs. The more times you write complex code, the more chances for error you create, so increasing abstraction is always a good idea.
Initially (well, you have to start somewhere) I created a default
object called the message_handler. This object will
serve as the "shell" object for all message handling operations
at a single port. Any operations involving that port should be able
to be accessed through this object, thereby hiding the internals
of message transaction from everyday usage.
EXPORT OBJECT message_handler
sig
PRIVATE
port:PTR TO mp
ENDOBJECT
EXPORT is an E directive to make this object accessible from
outside of the module; the PRIVATE command, on the other hand,
means that only methods belonging to this object will be able to access
the port variable. To all other objects and general purpose
code, for all intents and purposes, all that exists of the
message_handler is its public face - the sig
variable and the methods declared by the object.
The first method that must be declared is, of course, the constructor. As defined above, this will initialise the various resources used by the object's methods. In order to be neat (always good programming practice) I always write the destructor for the object at the same time and place it directly beneath the constructor, so it is easy to check that all resources are being allocated and dealloctaed properly.
PROC init(mp=-1:PTR TO mp) OF message_handler
DEF t:PTR TO process
/*
If a port has been provided, it uses it; otherwise it
uses the default message port belonging to its own
process.
*/
IF mp<>-1
self.port:=mp
ELSE
t:=FindTask(NIL)
self.port:=t.msgport
ENDIF
/*
This value will come in handy if the user of the module
wants to watch multiple ports at the same time.
*/
self.sig:=Shl(1, self.port.sigbit)
ENDPROC
PROC end() OF message_handler
/* It is necessary to clear the port of all messages */
DEF m:PTR TO message_node
WHILE m:=self.get() DO self.reply(m)
ENDPROC
And here are some more exciting methods; these depend upon the construction of the message_node as well.
PROC wait() OF message_handler IS WaitPort(self.port)
PROC send(target:PTR TO mp, taglist:PTR TO tagitem) OF message_handler
DEF tm:PTR TO message_node
NEW tm.init(self.port, taglist)
PutMsg(target, tm)
ENDPROC
PROC wait_for_value(tag, id) OF message_handler
DEF m:PTR TO message_node, finish=FALSE
REPEAT
self.wait()
WHILE m:=self.get()
IF (m.get(tag)=id) THEN finish:=TRUE
self.reply(m)
ENDWHILE
UNTIL (finish=TRUE)
ENDPROC
PROC get() OF message_handler IS GetMsg(self.port)
PROC reply(m:PTR TO message_node) OF message_handler
IF (m.replyport<>self.port) THEN ReplyMsg(m) ELSE END m
ENDPROC
Since Amiga E's system modules implement all of the
basic Amiga OS structures as OBJECTs, it is possible
to declare objects as subclasses of the standard system ones.
I have used this technique below. The advantage of this is that
these objects can then be accessed using the same OS routines
(in this case PutMsg(), GetMsg(), etc.)
as the objects that they are based on.
I have used tagitems (which require the 'utility.library' to be
open) because this is a solution which is arbitrarily extendable.
Each message may contain as much or as little information as you prefer.
It means a little more work to begin with; you must rememember to
initialise any tag values as TAG_USER or higher. (See
the example source code included with this tutorial for an illustration
of this technique.)
EXPORT OBJECT message_node OF mn PRIVATE
tags:PTR TO LONG
ENDOBJECT
PROC init(port:PTR TO mp, tags:PTR TO LONG) OF message_node
self.ln.type:=NT_MESSAGE
self.length:=SIZEOF message_node
self.replyport:=port
self.tags:=tags
ENDPROC
PROC get(tagvalue) OF message_node IS GetTagData(tagvalue, NIL, self.tags)
PROC change(taglist:PTR TO tagitem) OF message_node IS ApplyTagChanges(self.tags, taglist)
These modules may now be accessed in an abstracted fashion - see the
example sources (messages_1.e and
messages_2.e)for details.
Unfortunately, time/space reasons prevent me from launching into a detailed explanation of the code (this article is already later than it should be :) but it should be relatively clear what I'm trying to show. Don't be afraid to build, adapt, or change the code in any way; it is actually a simplified version of the one that I am using at the moment, because that would have taken up even more space, and would not have illustrated as clearly the principles I am trying to convey.
These may seem like great lengths to go to for such a small gain in productivity. This is true for the task described here, but OOP scales very well. It makes it much easier to maintain and develop larger projects than you can by using traditional programming models. I tend to use OOP for most of my programs these days - even if you don't want to move completely to the OOP model, you can still use your time more effectively by delegating tasks such as opening and closing libraries to objects. The advantage of this is that you write the code once, do it properly and then never look at it again, instead of having to adapt and interweave it with any new code you write. It reduces the chance of typing mistakes and makes the actual code look much cleaner. Choosing descriptive variable names here helps a lot, too. Which would you prefer?
message.send(screenblankerport, [MSG_ID, START_BLANKING
TAG_DONE])
or
mn.length:=SIZEOF mn mn.replyport:=thisport mn.ln.name:=STARTBLANKING PutMsg(screenblankerport, mn) /* and so on... */
The choice is yours - obfuscated, boring, spaghetti code or clean, elegant, object oriented works of art. Masterpieces!
(At this point, the author had to be forcibly restrained from launching into The Programmer's Artistic Manifesto....
"Programmers of the world, unite! You have nothing to lose but your
GOTOs.")