Creating reusable code with Lasso 9 Traits
Published on
When Kyle Jessup first introduced to me the concepts of Traits in Lasso 9, I thought "cool, that will be really useful", then promptly forgot about the feature amongst all the other awesome new things in Lasso 9. Many months later I was getting tired of adding a delete method to every object on a sizable project, and I remembered traits!
Ever since, I've been writing less and less code to do the same things - face it, every data object has a common set of things it needs to do, and while this is not going to apply to everything some are easy to abstract.
What's a trait?
A trait is an object that provides one or more methods that can be imported into a Lasso 9 type in order to build up functionality in a modular way. Several of these traits can be imported into a type, and a single trait can be imported into more than one type. Change the trait and you change the functionality of every type that includes it.
Building our demonstration type
We'll start with a simple type to handle a company object. Any object that is inherently tied to a database table really should have the following basic functionality:
- Add
- Update
- Delete
- Search
Of these, I usually combine "Add" and "Update" into the same method: "Save" but that's another topic. Suffice to say that Save and Search methods are generally a little too specific to the object to easily abstract to a trait - too many exceptions arise for comfort and the compromises made to deal with these exceptions eventually outweigh the time saving associated with a trait.
Our demonstration type "company" would look something like this:
define company => type {
data
public id::integer = 0,
public companyName::string = string,
public address1::string = string,
public address2::string = string,
public city::string = string,
public state::string = 'ON',
public country::string = 'CA',
public postal::string = string,
public status::boolean = false
// aliasing companyName... name is the shortcut :)
public name() => { return .companyName }
/* ===============================================================
base onCreate method, enables faster one-step instantiate and load
=============================================================== */
public onCreate(id::integer=0) => { #id > 0 ? .load(#id) }
/* ===============================================================
save method
=============================================================== */
public save() => {
// save method here
} // end save method
/* ===============================================================
delete method
=============================================================== */
public delete() => {
.id == 0 ? return
protect => {
handle_error => {
// error handling here
}
inline(
$gv_sql,
-SQL='DELETE FROM company WHERE id = '+.id+' LIMIT 1'
)
/inline
}
} // end delete method
/* ===============================================================
load method
=============================================================== */
public load(id::integer=.id) => {
// load method here
} // end load method
/* ===============================================================
search method
=============================================================== */
public search(
term::string=string,
-skip::integer=0,
-max::integer=$gv_max) => {
// search method here
} // end search method
}
So looking at the type you'll notice that I've left all the methods empty apart from delete. This is intentional. If you want the entire type with all the code, look at the end of the article.
If you need help understanding Lasso 9 types and methods, the documentation links are at the end as well.
You'll also notice that I'm referring to a few site environmental variables, and the type assumes they are set. This is just one way to handle this, there are other ways and not in the scope of this article.
- $gv_sql is the connection string
- $gv_max is the default maximum # rows returned, and can be overrriden when search is invoked.
So lets focus for the moment on "Delete".
Assuming your SQL table structure relies on an integer primary key column called "id" then creating a reusable trait for delete is really easy.
In the type above, there's already a delete method. However, there's really not a lot that differentiates it from what any other delete method might look like. The only thing different is in fact the table name.
The delete trait would look something like this:
define std_delete => trait {
require tablename, id
provide delete(id::integer=.id) => {
#id <= 0 ? return
protect => {
handle_error => {
return error_currenterror
}
.id == 0 ? .id = #id
inline(
$gv_sql,
-SQL='DELETE FROM '+.tablename+' WHERE id = '+#id+' LIMIT 1'
)
/inline
return
}
}
}
This trait requires the type it's imported into to have defined "tablename" and "id" data members. They can be public or protected, but not private.
The trait as defined above "provides" the delete method to any type it is imported into.
You will notice that the signature of the delete method requires a single optional parameter, the integer id of the row to delete. If it's not provided it will use the current object's id - and return without action if that value is less than or equal to zero.
The "tablename" value is the single thing that makes this portable between types - it can also be imported into a type called "address" with tablename = 'address_table' and it will use that value instead.
Using the trait in a type
The following is what the type would look like, omitting the other methods.
define company => type {
trait {
import std_delete
}
data
protected tablename = 'company',
public id::integer = 0,
public companyName::string = string,
public address1::string = string,
public address2::string = string,
public city::string = string,
public state::string = 'ON',
public country::string = 'CA',
public postal::string = string,
public status::boolean = false
// aliasing groupName... name is the shortcut :)
public name() => { return .companyName }
/* ===============================================================
base onCreate method, enables faster one-step instantiate and load
=============================================================== */
public onCreate(id::integer=0) => { #id > 0 ? .load(#id) }
// the save, load and search methods would go here... but no delete!
}
Usage
The usage in code is exactly the same from one approach to the other.
They both can be used like so:
// example 1
local(x = company)
#x->delete(1)
// example 2
local(x = company(1))
#x->delete
The trait and type, complete with methods
The complete trait and type are available for download here.
Some things to note
While traits are awesome, there are a few things you need to be aware of:
Once you define a trait, you cannot re-define it without restarting the instance. While on the surface it seems a PITA, you have to remember traits are meant to be small and modular. Build them initially as methods within a type to get them fine tuned to the way you want to use it, then move it to a trait.
Planning is an essential part of designing useful traits. Think about what incoming data might be specific to the type you are importing the trait into, and then require it as a property to use.
Documentation links
The Lasso 9 docs are undergoing a bit of a metamorphasis right now, so if the links below are out of date, please contact me and I will correct them.
Traits: http://www.lassosoft.com/Language-Guide-Defining-Traits
Types: http://www.lassosoft.com/Language-Guide-Defining-Types
Methods: http://www.lassosoft.com/Language-Guide-Defining-Methods