Jura Home | John Winn, December 2003 |
A design pattern is a "solution to a problem in context"; that is, it represents a high-quality solution to a recurring problem in design. In software engineering, design patterns are commonly used patterns of source code used to achieve particular goals in a particular context. Programmatic Design Patterns allow automatic implementation of existing design patterns in new code in a clean, object-orientated fashion.
A simple example of a design pattern in Java is the commonly used bean property design pattern. This section of Java source code shows an implementation of this design pattern:
protected String name; public String getName() { return name; } public void setName(String name) { this.name = name; }
This code uses the bean property design pattern to declare a
property called 'name' of type String
whose value can be
set or retrieved. This pattern is used so that, unlike with
fields, code can be executed when setting or retrieving property
values. It follows that the property value need not be stored in a
field, can be derived lazily, setting the property can fire events etc.
etc.
There are several problems with using design patterns in this way:
String
".
BeanInfo
or XML file which must be kept
synchronised.More fundamentally, using patterns is not declarative
programming. In the above code, you are thinking "I want an
editable property called 'name' of type String
" but you
are writing "I have a field called 'name' of type String
and
a method called getName
which returns...etc.".
Declarative programming states that you should write exactly what you
mean.
In Jura, the bean design pattern is available as a Programmatic
Design Pattern, called Property
. This is just
an ordinary class which implements the Pattern
interface.
You import the pattern just like importing any other class and use it
directly within your class definition. At compile time, the
pattern will turn into the corresponding set of fields and methods.
For convenience, Programmatic Design Patterns are often be referred to simply
as Patterns.
Property:name{type=String}
The use of a Pattern here has led to a simple, clean, declarative
piece of code. Now suppose we want to run some code each time the
property value is set or retrieved. If necessary, we could add it
like so, by using the getter
and setter
properties of Property
:
These are just properties of type Block
(a block of statements)
which the pattern inserts into the generated methods.
Property:name{type=String getter={ System.err.println("Getting value of 'name' as "+super.get()); Return{super.get()} } setter={ System.err.println("Setting value of 'name' to be "+newName); super.set(newName); } }
This looks less elegant but is okay as a once-off piece of code. However, this is the sort of debug code that is likely to be used all the time and so would be built in to a well-designed Property pattern, so that you could use the following code instead:
Property:name{type=String debug=true}
The Property
design pattern would then add appropriate
debug statements to the generated methods. Alternatively, you could extend Property
to create a new pattern with the required additional functionality.
As I hope this example illustrates, even simple programmatic design
patterns like Property
can become quite powerful and useful
as they are extended to provide additional details about the
property. For example, whether the property is
transient, whether it fires change events, additional details such as
display name or icon could all be added to the Property
design
pattern. Alternatively, the design pattern class itself can be
extended to provide, for example, a DerivedProperty
(whose
value is calculated from other properties) or an IndexedProperty
.
Note: Because Programmatic Design Patterns are expanded into standard Jura code at compile time, they are completely interoperable with existing code that uses ordinary design patterns.
We will now see an example of creating a simple Property
pattern. We start by providing an example of the pattern in
Jura.
Field:name{type=String access=protected} Method:getName{returns=String access=public // Return{name} } Method:setName( Par:name{type=String} // this.name = name }
We now look for common elements (in this case, the name and type) and replace
these with the variables name
, type
, and cname
for where the name is capitalised. These variables are marked with a
dollar to distinguish them from ordinary variables/properties, as will be
explained shortly. The way the names are set has to be changed from the
shorthand colon notation to setting the name as an attribute. This is
necessary because each name is now the result of an expression, rather than a
fixed value.
Field{name=$name type=$type access=protected} Method{name="get"+$cname returns=$type access=public // Return{$name} } Method{name="set"+$cname Par{name=$name type=$type} // this.$name = $name }
To create our pattern, we now create a class which implements the Pattern
interface. This interface has a single method toJura(TransformContext c)
which must be implemented by the pattern and which must use the context to add
the statements which make up the pattern. The pattern also implements Member
(which indicates that it can be added directly to a Class
or Interface
)
and Variable
(which indicates that it can be used as a variable in
an expression). The dollar now indicates the popup operator, which means "execute
what follows as if it were outside the current 'open' expression".
location=jura.pattern Class:Property{ implements={Pattern Member Variable} Field:name{type=String} Field:type{type=Type} Method:toJura{ Par:context{type=TransformContext} // Local:cname{type=String}=Utils.capitalise(name) context={ Field{name=$name type=$type access=protected} Method{name="get"+$cname returns=$type access=public // Return{$name} } Method{name="set"+$cname Par{name=$name type=$type} // this.$name = $name } } } }
Of course, this is a fairly minimal implementation. The implementation in the prototype allows for inserting statements in the methods (as in the example above), setting the access level of the property (e.g. protected rather than public), adding documentation and making the property read-only, final or transient.
For simplicity and clarity, Jura provides a fairly minimal set of language features. However, almost any language feature can then be added by importing Patterns as needed. In fact, a few basic language features are implemented internally as Patterns. Commonly requested features that can be provided using Patterns include:
ForEach
, IfCast
etc.;Delegate
allows automatic delegation of methods (safe
multiple inheritance)
Method
or Class
instead or apply a Transform
during compilation);The ability to add language elements when they are needed means that the Jura language can be easy to understand and learn whilst also allowing code to be concise and declarative.
Note: The patterns ForEach
, IfCast
,
Property
and Delegate
are implemented in the current prototype (Jura 0.8).