Copyright © 2017 Ashok P. Nadkarni. All rights reserved.
In addition to supporting object-oriented programming. Tcl also provides the foundation for building extended OO systems on top. This chapter provides an introduction to these facilities.
This is a draft chapter from the book The Tcl Programming Language, now available in print and electronic form from Amazon, Gumroad and others. |
1. Introduction
This chapter describes Tcl features that support object oriented programming. It does not go into detail about what constitutes object oriented programming (depends on who you ask), what its benefits are (depends on who you ask) or how your classes should be designed (depends on … you get the point). The number of words written on the topic in all likelihood exceeds the total number of objects in the universe and we don’t want to add to that number. You can always refer there. Also, the author is hardly an expert in object oriented design and it would really be a case of the one-eyed leading the blind.
Nevertheless, as we go along we will briefly describe some basic concepts for the benefit of the reader who really is completely unexposed to OO programming.
Most of the example code in this chapter is based on a framework for modeling banks. Our bank has accounts of different types, such as savings and checking, and which allow operations such as deposits and withdrawals. Some of these are common to all account types while others are unique. We have certain privileged customers who get special treatment and we have to also follow certain directives from Big Brother.
No, Citibank cannot run its operations based on our framework but it suffices for our illustrative purposes.
2. Classes
2.1. Objects and classes
The core of OO programming is, no surprise, an object. An object, often a representation of some real world entity, captures state (data) and behaviour which is the object’s response to messages sent to it. In most languages, implementation of these messages involves calling methods which are just function calls with a context associated with the object. For example, the state contained in an object representing a bank account would include items such as the current balance and account number. The object would respond to messages to deposit or withdraw funds.
A class is (loosely speaking) a template that defines the data items and methods (collectively called members) encapsulated by objects of a specific type. More often than not, creating a object of the class, often known as instantiating an object, is one of the duties of the class.
Not every OO system has, or needs, the notion of a class. Prototype based systems instead create objects by “cloning” an existing object - the prototype - and defining or modifying members.
TclOO provides facilities to support both the classy and the classless[1] models.
2.2. Creating a class
Classes are created in TclOO using the oo::class create
command.
Let us create a class, Account
, that models
a banking account.
% oo::class create Account
→ ::Account
This creates a new class Account
that can be used to create
objects representing bank accounts.
The class
would create a new class |
There is however no class definition associated with our Account
class and therefore, there is as
yet no state or behaviour defined for objects of the class.
That is done through one or more class definition scripts.
We will look at the contents of these definition scripts
throughout this chapter, but for now, simply note that class
definitions can be built up in incremental fashion. A definition
script can be passed as an additional argument to the
oo::class create
command, in the form
oo::class create CLASSNAME DEFINITIONSCRIPT
and also through one or more oo::define
commands which take
the form
oo::define CLASSNAME DEFINITIONSCRIPT
Thus the statements
oo::class create CLASSNAME DEFINITIONSCRIPT
and
oo::class create CLASSNAME
oo::define CLASSNAME DEFINITIONSCRIPT
are equivalent.
We will see both forms of class definition as we go along. As is generally the case, Tcl has the flexibility to fit your programming style.
2.3. Destroying classes
A class, as we shall see later, is also an object and like all objects can be destroyed.
% Account destroy
This will erase
Not commonly used in operational code, this ability to destroy classes is sometimes useful during interactive development and debugging to reset to a known clean state.
We will be using the Account
class throughout the chapter so
let us recreate it before we move on.
% oo::class create Account
→ ::Account
2.4. Defining data members
In our simple example, the state for an account object includes an account number that uniquely identifies it and the current balance.
We will need data members to hold this information and we define
them through a class definition script passed to oo::define
.
% oo::define Account {
variable AccountNumber Balance
}
The author uses mixed case for data members to avoid conflicts with names of method arguments and local variables. |
This defines the data members for the class as per-object variables.
AccountNumber
and Balance
are then visible within all methods of
the class and can be referenced there without any qualifiers or
declarations.
There can be multiple variable
statements, each defining
one or more data members.
Data members do not have to be declared using variable
in
a class definition script. They can also be declared within a
method using the my variable
command which we show later.
Note the difference between the
variable statement in the context of a class definition
and the variable command used
to define namespace variables. They both have very similar
function but the former only defines data member names, not
their values whereas the latter defines names in namespace as well
as their initial values.
|
2.5. Defining methods
Having defined the data members, let us move on to defining the
methods that comprise the behaviour of an Account
object. An
Account
object responds to requests to get the current balance
and to requests for depositing and withdrawing funds.
Methods are defined through the method
command which, like
variable
, is executed as part of a class definition script.
oo::define Account {
method UpdateBalance {change} {
set Balance [+ $Balance $change]
return $Balance
}
method balance {} { return $Balance }
method withdraw {amount} {
return [my UpdateBalance -$amount]
}
method deposit {amount} {
return [my UpdateBalance $amount]
}
}
As you see, a method is defined in exactly the same manner as
proc
defines a Tcl procedure. Just like in that case a method
takes an arbitrary number of arguments including a variable
number of trailing arguments collected as the args
variable.
The difference from a procedure lies in how it is invoked
and the context in which the method executes.
A method runs in the context of its
object’s namespace. This means the
object data members such as Balance
, defined through variable
,
are in the scope of the method and can directly be referenced
without any qualifiers as seen in the method definition above.
The method context also makes available several commands -
such as self
, next
and my
- which can only be called from
within a method. We will see these as we proceed with our
example but for now note the use of my
to refer to a method
of the object in whose context the current method is running.
Another point about method definitions concerns method
visibility. An exported method is a method that can be invoked
from outside the object’s context. A private method on the other
hand, can only be invoked from within another method in the object
context. Methods that begin with a lower case letter are
exported by default. Thus in our example, deposit
and
withdraw
are exported methods while UpdateBalance
is
not. Method visibility can be changed by using the export
and
unexport
commands inside a oo::define
class definition
script. Thus
oo::define Account {export UpdateBalance}
would result in UpdateBalance
being exported as well.
Method definitions can be deleted at any time with the
deletemethod
command inside a class definition script.
The following code snippet will crash Tcl versions prior to 8.6.2 due to a bug in the Tcl core. |
oo::class create C {method print args {puts $args}}
C create c
c print some nonsense
oo::define C {deletemethod print}
c print more of the same
Deletion of methods from classes is rarely used. However, deletion of methods from objects is sometimes useful in specialization of objects.
2.6. Constructors and destructors
There is one final thing we need to do before we can start banking
operations and that is to provide some means to initialize an
Account
object when it is created and perform any required clean
up when it is destroyed.
These tasks are performed through the special methods named
constructor
and destructor
. These differ from normal methods
in only two respects:
-
They are not explicitly invoked by name. Rather, the
constructor
method is automatically run when an object is created. Conversely, thedestructor
method is run when the object is destroyed. -
The
destructor
method definition differs from other methods in that it only has a single parameter - the script to be run. It does not have a parameter corresponding to arguments.
For our simple example, these methods are straightforward.
oo::define Account {
constructor {account_no} {
puts "Reading account data for $account_no from database"
set AccountNumber $account_no
set Balance 1000000
}
destructor {
puts "[self] saving account data to database"
}
}
Note the syntax of the destructor definition |
Both constructors and destructors are optional. They do not have to be defined in which case TclOO will simply generate empty methods for them.
2.7. The unknown method
Every object has a method named unknown
which is run when no
method of that name is defined for that object (actually in the
method chain for that object as we see later).
The definition of the unknown
method takes the form
oo::define CLASSNAME {
method unknown {target_method args} {…implementation…}
}
The unknown
method is passed the name of the invoked method as
its first argument followed by the arguments from the invocation
call.
The default implementation of this method, which is inherited
by all objects from the root oo::object
object, raises an
error. Classes and objects can override the default implementation
method to take some other action instead.
An example of its use is seen in
the COM client implementation in TWAPI.
The properties and methods
exported from a COM component are not always known beforehand and in
fact can be dynamically modified. The TclOO-based wrapper for COM
objects defines an unknown
method that looks up method
names supported by a COM component the first time a method is
invoked. If found, the lookup returns an index into a function
table that can then be invoked through the ComCall
method.
The implementation of unknown
looks like
oo::define COMWrapper {
method unknown {method_name args} {
set method_index [COMlookup $method_name]
if {$method_index < 0} {
error "Method $method_name not found."
}
return [my ComCall $method_index {*}$args]
}
}
(This is a greatly simplified, not entirely accurate or correct, description for illustrative purposes.)
2.8. Modifying an existing class
As we have seen in previous sections, you can incrementally
modify a class using oo::define
. Practically nothing about a
class is sacred - you can add or delete methods, data members, change
superclasses or mixins, and so on.
The question then arises as to what happens to objects that have already been created if a class is modified. The answer is that existing objects automatically “see” the modified class definition so for example any new methods can be invoked on them. Or if you add a mixin or a superclass, the method lookup sequence for the object will be appropriately modified.
However some care should be taken when modifying a class since existing objects may not hold all state expected by the new class. For example, the new constructors are (obviously) not run for the existing objects and thus some data members may be uninitialized. The modified class code has to account for such cases.
3. Working with objects
Having defined our model, we can now begin operation of our bank to illustrate how objects are used.
3.1. Creating an object
An object of a class is created by invoking one of two built-in
methods on the class itself. The create
method creates an
object with a specific name. The new
method generates a name for
the created object.
% set acct [Account new 3-14159265]
→ Reading account data for 3-14159265 from database
::oo::Obj70
% Account create smith_account 2-71828182
→ Reading account data for 2-71828182 from database
::smith_account
Creating an object also initializes the object by invoking a its constructor.
The created objects are Tcl commands and as such can be created in any namespace.
% namespace eval my_ns {Account create my_account 1-11111111}
→ Reading account data for 1-11111111 from database
::my_ns::my_account
% Account create my_ns::another_account 2-22222222
→ Reading account data for 2-22222222 from database
::my_ns::another_account
Note that my_account
and my_ns::my_account
are two distinct
objects.
3.2. Destroying objects
Objects in Tcl are not garbage collected as in some other
languages and have to be explicitly destroyed by calling their
built-in destroy
method. This also runs the object’s
destructor method.
% my_ns::my_account destroy
→ ::my_ns::my_account saving account data to database
Any operation on a destroyed object will naturally result in an error.
% my_ns::my_account balance
Ø invalid command name "my_ns::my_account"
Objects are also destroyed when its class or containing namespace is destroyed. Thus
% namespace delete my_ns
→ ::my_ns::another_account saving account data to database
% my_ns::another_account balance
Ø invalid command name "my_ns::another_account"
generates an error as expected.
3.3. Invoking methods
An object in Tcl behaves like an ensemble command of the form
OBJECT METHODNAME args….
This is the form used to invoke a method on the object from code “outside” the object.
% $acct balance
→ 1000000
% $acct deposit 1000
→ 1001000
As discussed earlier, when calling a method
from another method in the same object context, the alias my
is used to refer to the current object. So the deposit
method
we saw earlier calls the UpdateBalance
method as:
my UpdateBalance $amount
3.4. Accessing data members
Data members are not directly accessible from outside the object.
Methods, such as balance
in our example, have to be defined to
allow callers to read and modify their values. Many OO-purists,
and even non-purists like the author, believe this to be desirable.
However, Tcl being Tcl, it is always possible to add a variable access capability using the fact that each object has a private namespace that can be retrieved through introspection. Thus,
% set [info object namespace $acct]::Balance 5000
→ 5000
% $acct balance
→ 5000
This practice breaks encapsulation and is not recommended. However, some OO systems layered on top of TclOO do offer this feature in a structured manner that does not explicitly expose internal object namespaces. These are however not discussed here.
A good alternative is to automatically define accessor methods for public variables without the programmer having to explicitly do so. One such implementation is described in the Lifecycle Object Generators paper.
4. Inheritance
The defining characteristic of OO systems is support for inheritance. Inheritance refers to the ability of a derived class (also refered to as a subclass) to specialize a class - called its base class or superclass - by extending or modifying its behaviour.
Thus in our banking example, we may define separate classes representing savings accounts and checking accounts, each inheriting from the base account and therefore having a balance and methods for deposits and withdrawal. Each may have additional functionality, for example check writing facilities for the checking account and interest payments for the savings account.
The intention behind inheritance is to model is-a relationships. Thus a checking account is a bank account and can be used at any place in the banking model where the behaviour associated with a bank account is expected. This is-a relation is key when deciding whether to use inheritance or some other facility such as mix-ins.
Let us define our SavingsAccount
and CheckingAccount
. Instead
of using oo::define
as before, we will provide the full class
definition as part of the oo::class
command itself.
oo::class create SavingsAccount {
superclass Account
variable MaxPerMonthWithdrawals WithdrawalsThisMonth
constructor {account_no {max_withdrawals_per_month 3}} {
next $account_no
set MaxPerMonthWithdrawals $max_withdrawals_per_month
}
method monthly_update {} {
my variable Balance
my deposit [format %.2f [* $Balance 0.005]]
set WithdrawalsThisMonth 0
}
method withdraw {amount} {
if {[incr WithdrawalsThisMonth] > $MaxPerMonthWithdrawals} {
error "You are only allowed $MaxPerMonthWithdrawals withdrawals a \
month"
}
next $amount
}
}
oo::class create CheckingAccount {
superclass Account
method cash_check {payee amount} {
my withdraw $amount
puts "Writing a check to $payee for $amount"
}
}
→ ::CheckingAccount
Monthly interest | |
Pretend we are writing a check |
The superclass
command in the class definition establishes that
SavingsAccount
and CheckingAccount
inherit from
Account
. This statement by itself means they will behave exactly
like the Account
class, with the same methods and variables
defined. Further declarations will extend or modify the class
behaviour.
4.1. Methods in derived classes
Methods available in the base class are available in derived
classes as well. In addition, new methods can be defined such as
cash_check
and the monthly_update
in our example, that
are only present on objects of the derived class.
If the derived class defines a method of the same as a method in
the base class, it overrides the latter and will be called when
the method is invoked on an object of the derived class.
Thus the withdraw
method of the SavingsAccount
class overrides the
withdraw
method of the base Account
class. However,
we are just modifying the original method’s functionality
with an additional condition, not replacing it.
Therefore, after making the check we want to just pass
on the request to the base class method and not duplicate its
code. This is done with the command next
which invokes the
next method in the which in this case is the
superclass method with the same name as the current method.
This method chaining is actually only an example of
broader mechanism we will explore in detail
later.
Constructors and destructors are also chained. If a
derived class does not define a constructor, as is true for the
CheckingAccount
class, the base class
constructor is invoked when the object is created. If the derived
class does define a constructor, that is invoked instead and it
is up to that constructor to call the base class constructor using
next
as appropriate. Destructors behave in a similar fashion.
Note that next
may be called at any point in the method, not
necessarily in the beginning or the end.
4.2. Data members in derived classes
Derived classes can define new data members using either
variable
in the class definition or my variable
within a
method as in withdraw
.
Because data members are always defined in the namespace of the object, you have to careful about conflicts between variables of the same name being defined in a base class and a derived class if they are intended to represent different values. |
Data members defined in a parent (or ancestor) class are also
accessible within a derived class but they have to be brought
within scope of the method through the variable
declaration in
the derived class definition or the my variable
statement within
a method as is done in the implementation of
monthly_update
. Although we use a direct variable
reference there for expository purposes,
in the interest of data hiding and
encapsulation, direct reference to variables defined in ancestors
should be avoided if possible. It would have been better to write
the statement as
my deposit [format %.2f [* [my balance] $rate]]
Let us try out our new accounts.
% SavingsAccount create savings S-12345678 2
→ Reading account data for S-12345678 from database
::savings
% CheckingAccount create checking C-12345678
→ Reading account data for C-12345678 from database
::checking
% savings withdraw 1000
→ 999000
% savings withdraw 1000
→ 998000
% savings withdraw 1000
Ø You are only allowed 2 withdrawals a month
% savings monthly_update
→ 0
% checking cash_check Payee 500
→ Writing a check to Payee for 500
% savings cash_check Payee 500
Ø unknown method "cash_check": must be balance, deposit, destroy, monthly_updat...
Overridden base class method | |
Method defined in derived class | |
Check facility not available for savings |
4.3. Multiple inheritance
Imagine our bank is actually a full financial services firm that also provides stock trading services. Accordingly we have a class corresponding to a brokerage account.
oo::class create BrokerageAccount {
superclass Account
method buy {ticker number_of_shares} {
puts "Buy high"
}
method sell {ticker number_of_shares} {
puts "Sell low"
}
}
→ ::BrokerageAccount
Buy high… | |
…sell low. Historically, the author’s trading strategy |
The company now decides to make it even more convenient for customers to lose money participate in the stock market. So we come up with a new type of account, a Cash Management Account (CMA), which combines the features of the checking and brokerage accounts. We can model this in our system using multiple inheritance, where the corresponding class inherits from more than one parent class.
oo::class create CashManagementAccount {
superclass CheckingAccount BrokerageAccount
}
→ ::CashManagementAccount
Be careful when using multiple superclass statements
as the earlier declarations are overwritten
by default. You need to specify the -append option in that
case. See the Tcl documentation for the oo::define command for
details.
|
Our CMA account can do it all.
% CashManagementAccount create cma CMA-00000001
→ Reading account data for CMA-00000001 from database
::cma
% cma cash_check Payee 500
→ Writing a check to Payee for 500
% cma buy GOOG 100
→ Buy high
Use of multiple inheritance is a somewhat controversial topic in OO circles. Be as it may, TclOO offers the facility, and also an alternative using mixins, and leaves the design choices for programmers to make. As it should be.
5. Specializing objects
The next thing we talk about, object specialization, may be new to readers who are more familiar with class-based OO languages such as C++ where methods associated with objects are exactly those that are defined for the class(es) to which the object belongs.
In TclOO on the other hand, we can further “specialize” an individual object by overriding, hiding, and deleting methods defined in the class or even adding new ones. In fact, the potential specialization includes features such as forwarding, filters and mix-ins but we leave them for now as we have not discussed them as yet. As we will see, we can even change an object’s class.
Specialization is done through the oo::objdefine
command which
is analogous to the oo::define
command for classes except that
it takes an object instead of a class. Most of the special
commands like method
and variable
that we have seen used
inside oo::define
scripts can also be used inside the script
passed to oo::objdefine
.
5.1. Object-specific methods
Let us illustrate with our banking example. Imagine our banking system had the requirement that individual accounts can be frozen based on an order from the tax authorities. We need to define a procedure we can call to freeze an account so all transactions on the account will be denied. Correspondingly, we need a way to unfreeze an frozen account. The following code accomplishes this.
proc freeze {account_obj} {
oo::objdefine $account_obj {
method UpdateBalance {args} {
error "Account is frozen. Don't mess with the IRS, dude!"
}
method unfreeze {} {
oo::objdefine [self] { deletemethod UpdateBalance unfreeze }
}
}
}
When the freeze
procedure is passed an Account
object, it uses
oo::objdefine
to override the UpdateBalance
method that was
part of the object’s class definition with a object specific
UpdateBalance
method that raises an error instead.
It then defines a new method unfreeze
that can be called on
the object at the appropriate time to restore things back to normal.
We could have actually defined an unfreeze
procedure instead of
a unfreeze
method as follows:
proc unfreeze {account_obj} {
oo::objdefine $account_obj {deletemethod UpdateBalance}
}
This would have accomplished the same job in a clearer manner. We
chose to implement an unfreeze
method instead to illustrate that
we can actually change an object’s definition even from within
the object.
There are a couple of points that need to be elaborated:
-
The
self
command is only usable within a method and returns the name of the current object when called without parameters. Thus theoo::objdefine
command is instructed to modify the object itself. We will see other uses of theself
command later. -
Although not required in our example, it should be noted that variables defined in the class are not automatically visible in object-specific methods. They need to be brought into scope with the
my variable
statement. -
When called from within a
oo::objdefine
script, thedeletemethod
erases the specified object-specific methods. It does not affect methods defined in the class so the originalUpdateBalance
will still be in place and will no longer be overridden.
Let us see how all this works. At present Mr. Smith can withdraw money freely from his account.
% smith_account withdraw 100
→ 999900
So far so good. Now we get a court order to freeze Mr. Smith’s account.
% freeze smith_account
Mr. Smith tries to withdraw money and run away to the Bahamas.
% smith_account withdraw [smith_account balance]
Ø Account is frozen. Don't mess with the IRS, dude!
Have we affected other customers?
% $acct withdraw 100
→ 4900
No, only the smith_account
object was impacted.
Cornered Mr. Smith pays up to unfreeze the account.
% smith_account unfreeze
% smith_account withdraw 100
→ 999800
Notice that the class definition of UpdateBalance was not lost
in the process of adding and deleting the object-specific method.
|
This ability to define object-specific methods can be very useful. Imagine writing a computer game where the characters are modeled as objects. Several characteristics of the objects, such as the physics determining movement, are common and can be encapsulated with a class definition. The special “powers” of each character cannot be part of this class and defining a separate class for each character is tedious overkill. The special power of a character can instead be added to the character’s object as a object-specific method. Even modeling scenarios like temporary loss of a power without a whole lot of conditionals and bookkeeping becomes very simple using the object specialization mechanisms.
5.2. Changing an object’s class
Being a true dynamic OO language, TclOO can even change the class
of an object through oo::objdefine
. For example, one might
change a savings account to a checking account.
% set acct [SavingsAccount new C-12345678]
→ Reading account data for C-12345678 from database
::oo::Obj81
% $acct monthly_update
→ 0
So far so good. Let us attempt to cash a check.
% $acct cash_check Payee 100
Ø unknown method "cash_check": must be balance, deposit, destroy, monthly_updat...
Naturally that fails because it is not a checking account. Not a
problem, we can fix that by morphing the object to a CheckingAccount
.
% oo::objdefine $acct class CheckingAccount
We can now cash checks successfully
% $acct cash_check Payee 100
→ Writing a check to Payee for 100
but monthly updates no longer work as the account is no longer
a SavingsAccount
.
% $acct monthly_update
Ø unknown method "monthly_update": must be balance, cash_check, deposit, destro...
% $acct destroy
→ ::oo::Obj81 saving account data to database
Needless to say, you have to be careful when “morphing” objects in this fashion since data members may differ between the two classes.
Note the optional form of the oo::objdefine command that we have
used in the above code fragment. When the script passed to
oo::define or oo::objdefine contains only one command, it can
be directly specified as additional arguments to oo::define or
oo::objdefine .
|
Lifecycle Object Generators describes an example of when such morphing might be used. Consider a state machine where each state is represented by a class that implements the state’s behaviour. When a state change occurs, the state machine object changes its class to the class corresponding to the target state. See the abovementioned reference for implementation details.
6. Using Mixins
Earlier we looked at the use of inheritance to extend a class. We will now look at another mechanism to extend or change the behaviour of classes (and objects) - mixins.
The literature on the subject describes mix-ins in several different ways, often depending on language-specific capabilities. From this author’s perspective, a mix-in is a way to package a bundle of related functionality such that it can be used to extend one or more classes or objects. In some languages, multiple inheritance is used for this purpose but we will postpone that discussion until after we have seen an example of a mix-in.
Let us go back to our banking model. Imagine we have an Electronic Fund Transfer (EFT) facility that provides for transferring funds to other accounts. We will not worry about how this is done but just assume some global procedures are available for the purpose. This facility is available to all savings accounts but only to selected checking accounts. There are several ways we could implement this but our preference in this case is for mix-ins over the alternatives for reasons we discuss later.
In TclOO a mix-in is also defined as a class in exactly the same
manner as we have seen earlier. In fact, in theory any class can
be a mix-in. What sets a mix-in apart is the conceptual model and
how the class is used. In our example, the EFT facility would be
modeled as a class that implements two methods, transfer_in
and
transfer_out
. Conceptually, the class does not represent an
object, but rather a capability or, as is termed in some
literature, a role. It adds functionality to a “real” object.
oo::class create EFT {
method transfer_in {from_account amount} {
puts "Pretending $amount received from $from_account"
my deposit $amount
}
method transfer_out {to_account amount} {
my withdraw $amount
puts "Pretending $amount sent to $to_account"
}
}
→ ::EFT
Since we want all checking accounts to have this facility, we will
add EFT
to the CheckingAccount
class as a mixin.
% oo::define CheckingAccount {mixin EFT}
% checking transfer_out 0-12345678 100
→ Pretending 100 sent to 0-12345678
% checking balance
→ 999400
We are now able to do electronic transfers on all checking accounts.
Note that modifying the class definition in any manner,
in this case adding a
mixin, also impacts existing objects of that class. Thus the
checking object automatically supports the new functionality.
|
In the case of savings accounts, we only want select accounts
to have this facility. Assuming our savings
object represents
one of these privileged accounts, we can add the mixin to just
that object through oo::objdefine
.
% oo::objdefine savings {mixin EFT}
% savings transfer_in 0-12345678 100
→ Pretending 100 received from 0-12345678
1003090.0
% savings balance
→ 1003090.0
Notice that the EFT
class does not really know anything about
accounts. It encapsulates features that can be
added to any class or object that defines the methods deposit
and withdraw
required to support the mixin’s functionality.
So if we had a BrokerageAccount
class or object, we could mix it
in there as well.
6.1. Using multiple mixins
A class or object may have multiple classes mixed in. So for
example if we had a facility for electronic bill presentment
implemented as a mixin class BillPay
, we could have added it
along with EFT
as a mixin in a single statement
oo::define CheckingAccount {mixin BillPay EFT}
or as multiple statements
oo::define CheckingAccount {
mixin EFT
mixin -append BillPay
}
Note the use of -append above. By default, the mixin command
overwrites existing mixin configuration so without the -append
option when using multiple mixin statments, only class
BillPay would be mixed into CheckingAccount .
|
6.2. Mixins versus inheritance
Because one of its goal is to provide the required infrastructure for additional OO system to be built on top, TclOO offers a wide variety of capabilities that sometimes overlap in their effect. The question then arises as to how to choose the appropriate feature for a particular design requirement. One of these design choices involves mixins and inheritance.
We offer the author’s thoughts on the matter. Luckily, these tend to be few and far between so a couple of paragraphs is sufficient for this purpose.
Instead of mixing our EFT
class
into CheckingAccount
, we could have made it a
superclass and used multiple inheritance instead. Or even modified
or derived from the CheckingAccount
class to add transfer
methods. Why did we choose to go the mixin route?
Not directly inheriting or modifying the CheckingAccount
class was a
no-brainer for obvious reasons. The functionality is something
that could be used for other account types as well and it does not
make sense to duplicate code and add it to every class that needs
those features. That leaves the question of multiple inheritance.
There were several considerations:
-
Inheritance implies an is-a relationship between classes. Saying a checking account is-a “account that has transfer features” sounds somewhat contrived.
-
The above stems from the fact that EFT does not really reflect a real object. It is more like a set of features or capabilities that accounts have. In the real world, it would be a checkbox on a account opening form for a checking account. The general thinking is that such classes are better modeled as mixins.
-
Perhaps most important, when implemented as a mixin, we can provide the feature sets to individual accounts, for example to specific savings accounts. You cannot use multiple inheritance to specialize individual objects in this manner.
For these reasons, mixins seemed a better choice in our design (aside from the fact that we needed some example to illustrate mixins).
There is one practical aspect of TclOO design that may drive your decision. Methods implemented via mix-ins appear in the method chain before methods defined on the object whereas inherited methods appear after. This was not relevant to our example because the mix-in only added new methods. It did not override exising ones.
7. Filter methods
Imagine Mr. Smith is suspected of being up to his old
tricks again and we need to monitor his accounts and log all
activity. How would we do this ? We could specialize every method
for his accounts via oo::objdefine
and log the activity before
invoking the original method. We would have to do this for every
method available to the object - those defined in the object, its
class (and superclasses), object mixins and class
mixins. This would be tedious and error prone. Moreover, since Tcl
is a dynamic language, we would have to make sure we do that any
time new methods were defined for the object or any ancestor
and mixin.
Filter methods offer a easier solution. A filter method is defined
in the same manner as any method in the class or object. It is
marked as a filter method using the filter
command. Any method
invocation on the object will then result in the filter method
being invoked first.
We can add a filter method to the account object whose activity we want to track.
oo::objdefine smith_account {
method Log args {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
return [next {*}$args]
}
filter Log
}
Now all actions on the account will be logged.
% smith_account deposit 100
→ Log(1): 2-71828182 ::Account deposit: 100
Log(2): 2-71828182 ::Account UpdateBalance: 100
999900
Notice from the output that all method invocations, even those
called internally from deposit
are recursively logged. The
filter method must be aware that it may be recursively entered.
We use the info level
command to show the stack level. When
methods are chained, the filter is called for every method in
the chain.
Some additional notes on filter methods:
-
Many times, the filter method needs to know what method the caller is actually trying to invoke. The
self target
command is useful for this purpose. -
Multiple filters may be present and are chained like any other method.
-
Because filter methods are called for all method invocations, they are generally defined with a variable number of arguments.
-
Filter methods may be defined on an object, as in our example, or on a class, in which case they will affect all objects belonging to the class.
-
Our filter method is not exported because it starts with an upper-case letter. This means it will not be called accidentally by clients of the object. However, there is no requirement that filter methods must be private.
-
The filter method normally passes on the call to the target method via
next
which can be called at any point in the filter method. Moreover, the filter is not required to call the target method at all. -
The filter method may choose to pass on the target method result, some transformed version of it, or something else entirely.
-
Filter methods are bypassed when invoking constructors, destructors or the
unknown
method of a class.
7.1. Defining a filter class
The filter
declaration need not occur in the same class that defines
the filter method. This means you can define a generic class for
a filter which can be mixed into a “client” class or object
which can install or remove the filter at appropriate times as desired.
Let us rework our previous example. To start with a clean slate,
let us get rid of the Log
method we defined on the object earlier.
% oo::objdefine smith_account {
filter -clear
deletemethod Log
}
Note -clear option to clear any currently defined filters |
Then we define a class that does the logging.
% oo::class create Logger {
method Log args {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
return [next {*}$args]
}
}
→ ::Logger
Since we only want transactions for that account to be logged, we then mix it into the object and add the filter declaration.
% oo::objdefine smith_account {
mixin Logger
filter Log
}
% smith_account withdraw 500
→ Log(1): 2-71828182 ::Account withdraw: 500
Log(2): 2-71828182 ::Account UpdateBalance: -500
999400
As you can see, we have the same behaviour as before. The advantage of course is that defining a class allows a collection of additional behaviours to be abstracted and easily added to any class or object without repeating the code.
7.2. When to use filters
Filters could be replaced by other techniques such as overriding
and then chaining methods. Conversely, method overrides, such as
in our account freeze
example, could be replaced by filters.
Usually though it is clear which one makes
the most sense. Some general rules are
-
If we need to hook into multiple methods, it is easiest to use a filter method rather than override individual methods. If necessary,
self target
can be used within the filter to selectively hook specific methods as shown in Introspecting filter contexts. -
When a method behaves more as an “observer” on an object as opposed to being a core part of the object’s function, a filter method is a better fit.
-
Filter methods are always placed at the front of the method chain so that can be a factor as well in deciding to use a filter.
8. Method chains
Throughout this chapter we have seen that when a method is invoked
on an object, the code implementing the method for that object
may come from several different
places - the object, its class or an ancestor, a mixin, forwarded methods,
filters or even unknown
method handlers. TclOO locates the code
to be run by searching the potential implementations in a specific order.
It then runs the first
implementation in this list. That implementation may choose to
chain to the next implementation in the list using the next
command and so on through the list.
8.1. Method chain order
For the exact search order and construction of this method chain,
see the reference documentation of the next
command. Here we
will simply illustrate with an example where we define a class
hierarchy with multiple inheritance, mixins, filters and
object-specific methods. Note our method
definitions are empty because we are not actually going to call them.
oo::class create ClassMixin { method m {} {} }
oo::class create ObjectMixin { method m {} {} }
oo::class create Base {
mixin ClassMixin
method m {} {}
method classfilter {} {}
filter classfilter
method unknown args {}
}
oo::class create SecondBase { method m {} {} }
oo::class create Derived {
superclass Base SecondBase
method m {} {}
}
Derived create o
oo::objdefine o {
mixin ObjectMixin
method m {} {}
method objectfilter {} {}
filter objectfilter
}
We have created an object of class Derived
that inherits from
two parent classes, all of which define a method m
. Further we
have mixins for both a class and directly into the object. To
confuse matters further, we have filters defined at both the class
and object levels.
What will the method chain for method m
look like?
Luckily, we do not have to work it out while reading the
manpage. We can do it through introspection via the
info object call
command.
% print_list [info object call o m]
→ filter objectfilter object method
filter classfilter ::Base method
method m ::ObjectMixin method
method m ::ClassMixin method
method m object method
method m ::Derived method
method m ::Base method
method m ::SecondBase method
The output shows the method chain so we can see for example that the filter methods are first in line.
The info object call
command returns a list that contains the
method chain for a particular method invocation for a particular
object. Each element of the list is a sublist with four items:
-
the type which may be
method
for normal methods,filter
for filter methods orunknown
if the method was invoked through the unknown facility -
the name of the method which, as noted from the output, may not be the same as the name used in the invocation
-
the source of the method, for example, a class name where the method is defined
-
the implementation type of the method which may be
method
orforward
We reiterate that not every method in the chain is automatically
invoked. Whether a method occuring in the list is actually called or not
will depend on preceding methods passing on the invocation via
the next
command.
8.2. Method chain for unknown methods
What does the method chain look like for a method that is not defined for the object? We can find out the same way.
% print_list [info object call o nosuchmethod]
→ filter objectfilter object method
filter classfilter ::Base method
unknown unknown ::Base method
unknown unknown ::oo::object {core method: "unknown"}
As expected, the unknown
methods, where
defined, are called. Note the
root oo::object
object which is the ancestor of all TclOO
objects, has a predefined unknown
method.
8.3. Retrieving the method chain for a class
The above example showed the method chain for an object.
There is also a info class call
command that works with
classes instead of objects.
% print_list [info class call Derived m]
→ filter classfilter ::Base method
method m ::ClassMixin method
method m ::Derived method
method m ::Base method
method m ::SecondBase method
8.4. Inspecting method chains within method contexts
Within a method context, the command self call
returns more or
less the same information for the current object as info object call
.
In addition, you can use self call
from within a method context
to locate the current method in the method chain. This command returns a
pair, the first element of which is the same as the method chain
list as returned by info class call
command.
The second element is the index of the current method in that list.
An example will make this clearer.
% catch {Base destroy}
→ 0
% oo::class create Base {
constructor {} {puts [self call]}
method m {} {puts [self call]}
}
→ ::Base
% oo::class create Derived {
superclass Base
constructor {} {puts [self call]; next}
method m {} {
puts [self call]; next
}
}
→ ::Derived
% Derived create o
→ {{method <constructor> ::Derived method} {method <constructor> ::Base method}} 0
{{method <constructor> ::Derived method} {method <constructor> ::Base method}} 1
::o
% o m
→ {{method m ::Derived method} {method m ::Base method}} 0
{{method m ::Derived method} {method m ::Base method}} 1
Clean up any previous definitions |
Note the special form <constructor>
for
constructors. Destructors similarly have the form <destructor>
.
Constructor and destructor method chains are only available
through self call , not through info class call .
|
8.5. Looking up the next method in a chain
At times a method implementation may wish to know if it is the
last method in a method chain and if not, what method
implementation will be invoked next. This information can be
obtained with the self next
command from within a method
context.
We illustrate by modifying the m
method of the Derived
class
that we just defined.
% oo::define Derived {
method m {} { puts "Next method in chain is [self next]" }
}
% o m
→ Next method in chain is ::Base m
As seen, self next
returns a pair containing the class or object
implementing the next method in the method chain and the name of
the method (which may be <constructor>
and <destructor>
). In
the case the current method is the last in the chain, an empty
list is returned.
Notice that although the next method in the method chain is
printed out, it does not actually get invoked because the m
method in Derived
no longer calls next
.
Do not confuse self next with next . The latter invokes the
next method in the method chain while the former only tell you
what the next method is.
|
There is one important issue solved by self next
that we will
illustrate with an example. Imagine we want to package some
functionality as a mixin class. The actual functionality is
immaterial but it is intended to be fairly general purpose
(for example, logging or tracing) and mixable into any class.
% oo::class create GeneralPurposeMixin {
constructor args {
puts "Initializing GeneralPurposeMixin";
next {*}$args
}
}
→ ::GeneralPurposeMixin
% oo::class create MixerA {
mixin GeneralPurposeMixin
constructor {} {puts "Initializing MixerA"}
}
→ ::MixerA
% MixerA create mixa
→ Initializing GeneralPurposeMixin
Initializing MixerA
::mixa
So far so good. Now let us define another class that also uses the mixin.
% oo::class create MixerB {mixin GeneralPurposeMixin}
→ ::MixerB
% MixerB create mixb
Ø Initializing GeneralPurposeMixin
no next constructor implementation
Oops. What happened? If it is not clear from the error message,
the issue is that the GeneralPurposeMixin class naturally calls next
so that class that mixes it in can get initialized through its
constructor. The error is raised because class MixerB
does not
have constructor so there is no “next” method (constructor) to call.
This is where self next
can help. Let us redefine the
constructor for GeneralPurposeMixin.
% oo::define GeneralPurposeMixin {
constructor args {
puts "Initialize GeneralPurposeMixin";
if {[llength [self next]]} {
next {*}$args
}
}
}
% MixerB create mixb
→ Initialize GeneralPurposeMixin
::mixb
Voila! It all works now because we only call next
if there is
in fact a next method to call.
8.6. Controlling invocation order of methods
As we have seen in our examples, a method can use the next
command to invoke its successor in the method chain. With multiple
inheritance, mixins, filters involved, it may sometimes be
necessary to control the order in which inherited methods are
called. The next
command, which goes strictly by the order in the
method chain, is not suitable in this case.
The nextto
command allows this control. It is similar to the
next
except that it takes as its first parameter the name of the
class that implements the next method to be called.
nextto CLASSNAME ?args?
Here CLASSNAME
must be the name of a class that implements
a method appearing later in the method chain.
When might you use this ? Well, imagine you define a class
that inherits from two classes whose constructors take different
arguments. How do you call the base constructors from the derived
class? Using next
would not work because the parent class
constructors do not take the same arguments.
That’s where nextto
rides to the rescue as illustrated below.
oo::class create ClassWithOneArg {
constructor {onearg} {puts "Constructing [self class] with $onearg"}
}
oo::class create ClassWithNoArgs {
constructor {} {puts "Constructing [self class]"}
}
oo::class create DemoNextto {
superclass ClassWithNoArgs ClassWithOneArg
constructor {onearg} {
nextto ClassWithOneArg $onearg
nextto ClassWithNoArgs
puts "[self class] successfully constructed"
}
}
→ ::DemoNextto
We can now call it without conflicts.
% [DemoNextto new "a single argument"] destroy
→ Constructing ::ClassWithOneArg with a single argument
Constructing ::ClassWithNoArgs
::DemoNextto successfully constructed
9. Introspection
There are three things extremely hard: steel, a diamond, and to know one’s self.
Luckily for us, the last part does not hold for TclOO. Just like the rest of Tcl, TclOO offers deep and comprehensive introspection capabilities. These are useful in programming dynamic object systems, runtime debugging and tracing, and building layered OO systems.
Introspection of classes and objects from any context
can be done through the info class
and
info object
ensemble commands. These have subcommands
that return different pieces of information about a class or an
object. In addition, the self
command can be used for
introspection of an object from inside a method context for that
object.
9.1. Enumerating objects
The info class instances
command returns a list of objects
belonging to the specified class.
% info class instances Account
→ ::oo::Obj70 ::smith_account
% info class instances SavingsAccount
→ ::savings
As seen above, this command will only return objects that directly belong to the specified class, not if the class membership is inherited.
You can optionally specify a pattern argument in which case only
objects whose names match the pattern using the rules of the
string match
command are returned. This can be useful for
example when namespaces are used to segregate objects.
% info class instances Account ::oo::*
→ ::oo::Obj70
9.2. Enumerating classes
Classes are also objects in TclOO and therefore the same command used to enumerate objects can be used to enumerate classes.
% info class instances oo::class
→ ::oo::object ::oo::class ::oo::Slot ::Account ::SavingsAccount ::CheckingAcco...
We pass oo::class
to the command because that is the class that
all classes (or class objects, if you prefer) belong to. The
returned list contains two interesting elements:
-
oo::class
is returned because as we said it is a class itself (the class that all class objects belong to) and is therefore an instance of itself. -
If that were not confusing enough,
oo::object
is also returned. This is the root class of the object hierarchy and hence is an ancestor ofoo::class
. At the same time it is a class and hence must be an instance ofoo::class
as well.
As before, we can also restrict the classes returned by specifying a pattern that the name must match.
% info class instances oo::class *Mixin
→ ::ClassMixin ::ObjectMixin ::GeneralPurposeMixin
9.3. Introspecting class relationships
The info class superclasses
command returns the direct
superclasses of a class.
% info class superclasses CashManagementAccount
→ ::CheckingAccount ::BrokerageAccount
% info class superclasses ::oo::class
→ ::oo::object
Notice that oo::object
is a superclass of oo::class
.
Conversely, the info class subclasses
will return the classes
directly inheriting from the specified class.
% info class subclasses Account
→ ::SavingsAccount ::CheckingAccount ::BrokerageAccount
As one might expect, there is also a command,
info class mixins
for listing mixins.
% info class mixin CheckingAccount
→ ::EFT
9.4. Checking class membership
You can get the class an object belongs to with info object class
.
% info object class savings
→ ::SavingsAccount
The same command will also let you check whether the object belongs to a class, taking inheritance into account.
% info object class savings SavingsAccount
→ 1
% info object class savings Account
→ 1
% info object class savings CheckingAccount
→ 0
For enumerating the classes mixed-in with an object, use
info object mixins
, analogous to info class mixins
.
% info object mixins savings
→ ::EFT
From within the method context of an object, the command
self class
command returns the class defining the currently
executing method. Note this is not the same as the class the object
belongs to as the example below shows.
% catch {Base destroy}
→ 0
% oo::class create Base {
method m {} {
puts "Object class: [info object class [self object]]"
puts "Method class: [self class]"
}
}
→ ::Base
% oo::class create Derived { superclass Base }
→ ::Derived
% Derived create o
→ ::o
% o m
→ Object class: ::Derived
Method class: ::Base
Clean up any previous definitions |
The self class command will fail when called from a method
defined directly on an object since there is no class associated
with the method in that case.
|
9.5. Enumerating methods
The list of methods implemented by a class or object can be retrieved
through info class methods
and info object methods
respectively.
Options can be specified to control
whether the list includes inherited and private methods.
% info class methods CheckingAccount
→ cash_check
% info class methods CheckingAccount -private
→ cash_check
% info class methods CheckingAccount -all
→ balance cash_check deposit destroy transfer_in transfer_out withdraw
% info class methods CheckingAccount -all -private
→ <cloned> UpdateBalance balance cash_check deposit destroy eval transfer_in tr...
% info object methods smith_account -private
Lists all methods defined and exported by CheckingAccount itself |
|
Lists both exported and private methods defined by CheckingAccount |
|
Lists all methods defined and exported by CheckingAccount , its ancestors, or mixins |
|
Lists both exported and private methods defined by CheckingAccount , its ancestors, or mixins |
|
Lists exported and non-exported methods defined in the object itself |
The list of methods that are set as filters can similarly be
obtained with info class filters
or info object filters
.
% info object filters smith_account
→ Log
9.6. Retrieving method definitions
To retrieve the definition of a specific method, use
info class definition
. This returns a pair consisting of the
method’s arguments and its body.
% info class definition Account UpdateBalance
→ change {
set Balance [+ $Balance $change]
return $Balance
}
The method whose definition is being retrieved has to be defined in the specified class, not in an ancestor or a class that is mixed into the specified class.
Constructors and destructors are retrieved differently via
info class constructor
and info class destructor
respectively.
% info class constructor Account
→ account_no {
puts "Reading account data for $account_no from database"
set AccountNumber $account_no
set Balance 1000000
}
9.7. Retrieving the method chain
The info class call
command retrieves the method chain for a
method. From a method context, the self call
command returns
similar information while self next
identifies the next method
implementation in the chain.
We have already discussed all these in detail in an earlier section.
9.8. Introspecting filter contexts
When a method is run as a filter, it is often useful for it to
know the real target method being invoked. This information
is returned by self target
which can only be used from within a
filter context. Its return value is a pair containing the
declarer of the method and the target method name.
For example, suppose
instead of logging every transaction as in our
earlier example, we only wanted to log
withdrawals. In that case we could have defined the Log
command
as follows:
oo::define Logger {
method Log args {
if {[lindex [self target] 1] eq "withdraw"} {
my variable AccountNumber
puts "Log([info level]): $AccountNumber [self target]: $args"
}
return [next {*}$args]
}
}
We would now expect only withdrawals to be logged.
% smith_account deposit 100
→ 999500
% smith_account withdraw 100
→ Log(1): 2-71828182 ::Account withdraw: 100
999400
We are 100% right. Again!
The other piece of information that is provided inside a filter
method is about the filter itself and is available through
the self filter
command.
Let us redefine our Log
filter yet again to see this.
% oo::define Logger {
method Log args {
puts [self filter]
return [next {*}$args]
}
}
% smith_account withdraw 1000
→ ::smith_account object Log
::smith_account object Log
998400
As seen above, the self filter
command returns a list of three
items:
-
the name of the class or object where the filter is declared. Note this is not necessarily the same as the class in which the filter method is defined. Thus above, the filter was defined in the
Logger
class but declared in thesmith_account
object. -
either
object
orclass
depending on whether the filter was declared inside an object or a class. -
the name of the filter.
You will see two output lines in the above example. Remember the
filter is called at every method invocation. Thus the Log method
is invoked twice, once before the withdraw method, and then again
when that method in turn calls UpdateBalance .
|
9.9. Object identity
Who in the world am I? Ah, that’s the great puzzle.
Alice in Wonderland
Under some circumstances, an object needs to discover its own identity from within its own method context.
An object can be identified in two ways:
-
the command name used to invoke the object methods
-
the unique namespace in which the object state is stored
The former is returned by the
self object
command, which can also be called as simply self
.
A method may need to know command name of the object
-
when an object method has to be passed to a callback
-
when an object is redefined “on the fly” from within an method, its name must be passed to
oo::objdefine
.
See Object-specific methods for an example.
The unique namespace associated with the object is obtained
through the self namespace
command within a method context or
with the info object namespace
command elsewhere.
% oo::define Account {method get_ns {} {return [self namespace]}}
% savings get_ns
→ ::oo::Obj76
% set acct [Account new 0-0000000]
→ Reading account data for 0-0000000 from database
::oo::Obj106
% $acct get_ns
→ ::oo::Obj106
% info object namespace $acct
→ ::oo::Obj106
Notice when we create an object using new
the namespace matches
the object command name. This is an artifact of the implementation
and this should not be relied on. In fact, like any other Tcl
command, the object command can be renamed.
% rename $acct temp_account
% temp_account get_ns
→ ::oo::Obj106
As you can see, the object command and its namespace name no longer match. Also note the namespace does not change when the command is renamed.
9.10. Enumerating data members
The command info class variables
returns the list of variables
that have been declared with the variable
statement inside a
class definition and are therefore automatically brought within the
scope of the class’s methods.
% info class variables SavingsAccount
→ MaxPerMonthWithdrawals WithdrawalsThisMonth
The listed variables are only those defined
through the variable
statement for that specified class. Thus
the above command will not show the variable Balance
as that was
defined in the base class Account
, not in SavingsAccount
.
For enumerating variables for an object as opposed to a class, there are two commands:
-
info object variables
behaves likeinfo class variables
but returns variables declared withvariable
inside object definitions created withoo::objdefine
. These may not even exist yet if they have not been initialized. -
info object vars
returns variables currently existing in the object’s namespace and without any consideration as to how they were defined.
Hence the difference between the output of the following two commands.
% info object variables smith_account
% info object vars smith_account
→ Balance AccountNumber
The first command returns an empty list because no variables were
declared through variable
for that object. The second command
returns the variables in the object’s namespace. The fact that
they were defined through a class-level declaration is irrelevant.
10. References
- DKF2005
-
TIP #257: Object Orientation for Tcl, Fellows et al, http://tip.tcl.tk/257. The Tcl Implementation Proposal describing TclOO.
- WOODS2012
-
Lifecycle Object Generators, Woods, 19th Annual Tcl Developer’s Conference, Nov 12-14, 2012. Describes a number of useful OO patterns built on top of TclOO.