Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
I want to apply best practices and use a service layer approach for my application and model design. I will then use these service objects in my handlers in order to handle the business logic for me. Repeat after me:
I WILL NOT PUT BUSINESS LOGIC IN EVENT HANDLERS!
The whole point of the model layer is that it is separate from the other 2 layers (controller and views). Remember, the model is supposed to live on its own and not be dependent on external layers (Decoupled). Decoupling facilitates reuse. From these simple requirements I will create the following classes:
BookService.cfc
- A service layer for book operations
Book.cfc
- Represents a book in my system
A Service Layer defines an application's boundary [Cockburn PloP] and its set of available operations from the perspective of interfacing client layers. It encapsulates the application's business logic, controlling transactions and coordinating responses in the implementation of its operations. Martin Fowler
A service layer approach is a way to architect enterprise applications in which there is a layer that acts as a service or mediator to your domain models, data layers and so forth. This layer is the one that event handlers or remote ColdBox proxies can talk to in order to interact with the domain model. There are basically two approaches when building a service layer:
Designing the services around functionality that might encompass several domain model objects. This is our preferred approach as it creates a more rich service layer and you will not have class explosion.
Designing a service for each business object, which in turn matches a table or set of tables in the permanent storage. This approach is easy to follow, but the consequences are the responsibilities that could be grouped are not and you will end up with class explosion.
The best way to determine the most effective solution is to actually try both approaches. Once you design them and put them in practice, you will find your preference. I want to concentrate and challenge you to try these approaches out and learn from your experiences. I believe there is NO SILVER BULLET on OO design, just stick to best practices and practice "code smell", which is the art of knowing when your code is not right and therefore smells nasty!
The BookService
object will be my API to do operations as mentioned in my requirements and this is the object that will be used by my handlers. My Book
object will model a Book's data and behavior. It will be produced, saved and updated by the BookService
object and will be used by event handlers in order to populate and validate them with data from the user.
The view layer will also use the Book
object in order to present the data. As you can see, the event handlers are in charge of talking to the Domain Model for operations/business logic, controlling the user's input requests, populating the correct data into the Book
model object and making sure that it is sent to the book service for persistence.
All your model objects will be located under your models
folder of your application root by convention. For a more secure installation, place your models outside of the web root and use a ColdFusion Mapping or WireBox Mapping to access them.
ColdBox allows you to integrate with the model layer easily by leveraging WireBox as your default dependency injection framework. However, you can integrate your model layer with any third-party DI framework via our cbioc
module (https://github.com/ColdBox/cbox-ioc) as well.
Remember this:
Did you get some spine shivers like I just did. WOW! That is the traditional spaghetti coding style. With ColdBox we are now moving into MVC land and focusing on the model layer.
However, the model layer can even be subdivided into many layers as well as we will investigate in this section.
WireBox, our dependency injection and AOP framework, will do all the magic of building, wiring objects with dependencies and helping you persist objects in some state (singletons, transients, request, etc). The main purpose for model integration is to make the developer's development workflow easier! And we all like that Easy button!
Easily create and retrieve model objects by using one method: getInstance()
Easily handle model or handler dependencies by using cfproperty
and constructor argument conventions.
An Injection DSL (Domain Specific Language) has been created to facilitate dependencies
Easily determine what scope model objects should be persisted in: Transients, Singletons, Cache, Request, Session, etc.
Easily create a configuration binder to create aliases or complex object relationships (Java, WebServices, RSS, etc.)
Easily do FORM data binding or advanced ORM data binding
Easily compose ORM relationships from request data
Wow! You can do all that? Yes and much more. So let's begin.
You can have an optional WireBox configuration binder that can fine-tune the WireBox engine and also where you can create object mappings, and even more model locations by convention. Usually you will find this binder by convention in your config/WireBox.cfc
location and it looks like this:
Please refer to the full Binder documentation: (http://wirebox.ortusbooks.com/configuration/configuring-wirebox/) for further inspection.
By default, all objects that you place in the models
folder are available to your application by their name. So if you create a new model object: models/MyService.cfc
, you can refer to it as MyService
in your application. However, if you create a model object: models/security/SecurityService.cfc
it will be available as security.SecurityService
. This is great and dandy, but when refactoring comes to play you will have to refactor all references to the dot-notation paths. This is where mappings come into play.
WireBox has several methods for mappings, the easiest of them are the following:
map( alias ).to( cfc.path )
mapPath( path )
mapDirectory( packagePath, include, exclude, influence, filter, namespace )
Before we start building our objects, we need to understand how WireBox injects dependencies for us. You can define injections using the configuration inside the binder (like any other DI framework), but the easiest approach is to use our injection annotation and conventions (called the injection DSL). The injection DSL can be applied to cfproperty
, cfargument
, cffunction
or called via getInstance()
as we saw previously.
You can utilize the injection DSL by adding an attribute called inject
to properties, arguments and functions. This attribute is like your code shouting, "Hey, I need something here. Hello! I need something!" That something might be another object, setting, cache, etc. The value of the inject
attribute is a string that represents the concept of retrieving an object mapped in WireBox. It can be an object path, an object ID, a setting, a datasource, custom data, etc.
Info: If you don't like annotations or find them obtrusive, you don't have to use them :). Just leverage the WireBox binder to define dependencies instead.
Here are a few examples showing how to apply the injection DSL.
Property Annotation
Constructor Argument Annotation
Setter Method Annotation
You can learn more about the supported injection DSLs in the WireBox Injection DSL documentation.
This DSL namespace is only active if using CacheBox or a ColdBox application context.
So what can Book.cfc
do. It can have the following private properties:
name
id
createdata
ISBN
author
publishDate
It can then have getters/setters for each property that I want to expose to the outside world, remember that objects should be shy. Only expose what needs to be exposed. Then I can add extra functionality or behavior as needed. You can do things like:
have a method that checks if the publish date is within a certain amount of years
have a method that can output the number in certain formats
have a method that can output the publish date in different formats and locales
make the object save itself or persist itself (Active Record)
and so much more
Now, all you OO gurus might be saying, why did he leave the author as a string and not represented by another object. Well, because of simplicity. The best practice, or that code smell you just had, is correct. The author should be encapsulated by its own model object Author that can be aggregated or used by the Book object. I will not get into details about object aggregation and composition, but just understand that if you thought about it, then you are correct. Moving along...
Important Your objects are not always supposed to be dumb, or just have getters and setters (Anemic Model). Enrich them please!
Back to the book service object. This service will need a datasource name (which could be encapsulated in a datasource object) in order to connect to the database and persist stuff. It might also need a table prefix to use (because I want to) and it comes from a setting in my application's configuration file. Ok, so now we know the following dependencies or external forces:
A datasource (as an object or string)
A setting (as a string)
I can also now think of a few methods that I can have on my book service:
getBook([id:string])
:Book This method will create or retrieve a book by id.
searchBook(criteria:string)
:query This method can return a query or array of Books if needed
saveBook(book:Book)
Save or Update a book
deleteBook(book:Book)
Delete a book
Now once you build them and UNIT TEST THEM, yes UNIT TEST THEM. Then you can use them in your handlers in order to interact with them. As you can see, most of the business rules and logic are encapsulated by these domain objects and not written in your event handlers. This creates a very good design for portability, sustainability and maintainability. So let's start actually seeing how to write all of this instead of imagining it. Below you can see a more complete class diagram of this simple example.
Hint Note that sometimes the design in UML will not reflect the end product. UML is our guide but not the final product.
Whenever your models need anything from the ColdBox application then you can leverage the coldbox:
namespace for injections.
If I know that my database operations will get very complex or I want to further add separation of concerns, I could add a third class to the mix: BookGateway.cfc
or BookDAO.cfc
that could act as my data gateway object.
Now, there are so many design considerations, architectural requirements and even types of service layer approaches that I cannot go over them and present them. My preference is to create service layers according to my application's functionality (Encompasses 1 or more persistence tables) and create gateways or data layers when needed, especially when not using an ORM. The important aspect here, is that I am thinking about my project's OO design and not how I will persist the data in a database. This, to me, is key!
Understanding that I am more concerned with my object's behavior than how will I persist their data will make you concentrate your efforts on the business rules, model design and less about how the data will persist. Don't get me wrong, persistence takes part in the design, but it should not drive it.
This DSL namespace interacts with the loaded LogBox instance.
The Model layer represents your data structures and business logic. It is the domain-specific representation of the information that the application operates on. Many applications also use a persistent storage mechanism (such as a database) to store and retrieve data. MVC does not specifically mention the data access layer because it is understood to be underneath or encapsulated by the Model Layer. This is the most important part of your application and it is usually modeled by ColdFusion components (CFCs). You can even create the entire model layer in another language or physical location (Java, web services).
All you need to understand is that this layer is the layer that runs the logic show! For the following example, I highly encourage you to also do , so you can visualize class relationships and design.
There are tons of great UML resources and tools. Here are some great tools for you:
Sparx Systems Enterprise Architect -
ColdFusion generation templates for Enterprise Architect -
ArgoUML -
Poseidon UML -
Learning UML -
UML in a nutshell -
Since some of the examples in this section require the usage of ColdFusion ORM and the ColdBox ORM Extensions module (), let's use CommandBox to install them in our application in two easy steps:
1. Install Module
2. Add Mapping
Open your Application.cfc
and add a mapping to this module:
Unfortunately, we cannot use the ColdBox Module CFMappings to do this because the ColdFusion ORM loads before anything in our system.
ColdBox has a core validation module you can install to provide you with robust server-side validation of model data. You can install it via CommandBox:
Let's say that I want to build a simple book catalog and I want to be able to do the following:
List how many books I have
Search for a book by name
Add Books
Remove Books
Update Books
The default namespace is not specifying one. This namespace is used to retreive either named mappings or full component paths.
I recommend you model these class relationships in to get a better feeling of the design. Anyways, that's it, we are doing domain modeling. We have defined a domain object called Book
and a companion BookService
object that will handle book operations.
ColdBox can populate or bind model objects from data in the request collection by matching the name of the form element to the name of a property on the object. You can also populate model objects from JSON, XML, Queries and other structures a-la-carte by talking directly to 's object populator.
You can find the documentation and source in its repository:
There are several ways I can go about this and your design will depend on the tools you use. If you use an ORM like ColdFusion ORM you most likely will use either an approach or build or build a service layer based on our . If you are not using an ORM, then most likely you will have to build all object, service and data layers manually. We will concentrate first on the last approach in order to do our modeling and then build them out in different ways.
DSL | Description |
| Get the coldbox controller reference |
| Get a reference to the application's flash scope object |
| Get the coldbox application {setting} setting and inject it |
| Get a ColdBox setting {setting} and inject it |
| Get the coldbox application {setting} from the {module} and inject it |
| Get a reference to the loader service |
| Get a reference to the request service |
| Get a reference to the handler service |
| Get a reference to the interceptor service |
| Get a reference to the ColdBox Module Service |
| Get the ColdBox rendering engine reference |
| Get the ColdBox data marshalling reference |
| Get a reference of a named interceptor {name} |
| Get the application's configuration structure |
| Get the framework's configuration structure |
| Get a setting from the ColdBox settings instead of the Application settings |
| Inject the entire {module} settings structure |
| Inject the entire {module} configurations structure |
DSL | Description |
cachebox | Get a reference to the application's CacheBox instance |
cachebox:{name} | Get a reference to a named cache inside of CacheBox |
cachebox:{name}:{objectKey} | Get an object from the named cache inside of CacheBox according to the objectKey |
DSL | Description |
logbox | Get a reference to the application's LogBox instance |
logbox:root | Get a reference to the root logger |
logbox:logger:{category name} | Get a reference to a named logger by its category name |
logbox:logger:{this} | Get a reference to a named logger using the current target object's path as the category name |
DSL | Description |
empty | Same as saying id. Get a mapped instance with the same name as defined in the property, argument or setter method. |
id | Get a mapped instance with the same name as defined in the property, argument or setter method. |
id:{name} | Get a mapped instance by using the second part of the DSL as the mapping name. |
id:{name}:{method} | Get the {name} instance object, call the {method} and inject the results |
model | Get a mapped instance with the same name as defined in the property, argument or setter method. |
model:{name} | Get a mapped instance by using the second part of the DSL as the mapping name. |
model:{name}:{method} | Get the {name} instance object, call the {method} and inject the results |
An object that represents a contact and self-validates using the cbvalidation
module.
Then spice up with some properties and constraints:
You can very easily add persistence to your model+ objects via our annotations or binder configuration. The available scopes are:
transient or no scope
: The default scope. Meaning objects have no scope, they are recreated every single time you request them.
singleton
: Objects are created once and live until your Application expires
cachebox
: You can store your objects in any CacheBox provider and even provide timeouts for them
session
: Store them in the ColdFusion session
scope
server
: Store them in the ColdFusion server
scope
request
: Store them in the ColdFusion request
scope
application
: Store them in the ColdFusion application
scope
CUSTOM
: You can build your own scopes as well.
Hint Pease note that using annotations is optional, you can configure every object in our configuration binder as well.
Object Persistence & Thread Safety: http://wirebox.ortusbooks.com/advanced-topics/object-persistence-and-thread-safety
Make sure you register a datasource in your ColdFusion administrator, we called ours contacts
and then register it in your ColdBox configuration so ColdBox can build datasource structs for us. This is totally optional, but we do it to showcase more injections:
config/ColdBox.cfc
This injection namespace comes from the cborm
module extensions (https://github.com/ColdBox/cbox-cborm). It gives you the ability to easily inject base ORM services or binded virtual entity services for you:
Talk and get objects from the current wirebox injector
Now that we have seen all the theory and stuff, let's get down to business and do some examples. We will start with the full coding approach with no ORM and then spice it up with ORM, so you can see how awesome ORM can be. The examples will not show the entire application being built, but enough to get you started with the process of modeling everything. Here is a layout of what we will build:
I will create a DAO for this small example, so we can showcase how to talk to multiple objects. You can start a new application with CommandBox if you like:
Now let's build the same thing but using ColdFusion ORM and our ActiveEntity
approach from the ColdBox ORM Extensions module.
Our Contact DAO will talk to the datasource object we declared and do a few queries. Notice that this object is a singleton and has some dependency injection.
Then spice it up
You will first make sure your contacts
datsource exists in the Administrator and then we can declare our ORM settings in our Application.cfc
These are the vanilla settings for using the ORM with ColdBox. Make sure that flushAtRequestEnd
and autoManageSession
are set to false as the ORM extensions will manage that for you.
In this example, we also use dbcreate="update"
as we want ColdFusion ORM to build the database for us which allows us to concentrate on the domain problem at hand and not persistence. You also see that we add our own eventHandler which points to the extension's event handler so we can make Active Entity become well, Active!
Now open your ColdBox.cfc
and add the following to activate ORM injections inside of your configure()
method.
Let's put all the layers together and make the handler talk to the model. I create different saving approaches, to showcase different integration techniques:
Spice it up now
Now that you have finished, go execute the contacts and interact with it.
That's right, go to the handler now, no need of data layers or services, we build them for you! This time, we show you the entire CRUD operations as Active Entity makes life easy!
Then spice it up
You will first make sure your contacts
datsource exists in the Administrator and then we can declare our ORM settings in our Application.cfc
These are the vanilla settings for using the ORM with ColdBox. Make sure that flushAtRequestEnd
and autoManageSession
are set to false as the ORM extensions will manage that for you.
In this example, we also use dbcreate="update"
as we want ColdFusion ORM to build the database for us which allows us to concentrate on the domain problem at hand and not persistence. You also see that we add our own eventHandler which points to the extension's event handler so we can make Active Entity become well, Active!
Now open your ColdBox.cfc
and add the following to activate ORM injections inside of your configure()
method.
Now let's build the same thing but using ColdFusion ORM and our approach, in which we will use a service layer but virtually built by ColdBox. This will most likely give you 80% of what you would ever need, but in case you need to create your own and customize, then you would build a service object that extends or virtual or base layer.
Here is our service layer and we have added some logging just for fun :). Notice that this object is a singleton and has some dependency injection.
Then spice it up
Now, some observations of the code:
We use the populator object that is included in WireBox to make our lives easier so we can populate objects from queries and deal with objects.
We also inject a reference to the object factory WireBox so it can create Contact
objects for us. Why? Well what if those objects had dependencies as well.
Let's use CommandBox to build it:
Then spice it up with the validation constraints
Here are the views as well, isn't it awesome that the views stay the same :), that means we did a good job abstracting our model and controllers.
Check out our awesome html
helper object. It can even build the entire forms according to the Active Entity object and bind the values for you!
An object that represents a contact and self-validates using , and is an awesome object. Let's use CommandBox to build it:
Then spice it up with the validation constraints
Now let's use the power of ORM and CommandBox to scaffold everything for you :). The help for this command is here:
It even creates all the integration tests for you.
DSL
Description
entityService
Inject a BaseORMService object for usage as a generic service layer
entityService:{entity}
Inject a VirtualEntityService object for usage as a service layer based off the name of the entity passed in.
DSL
Description
wirebox
Get a reference to the current injector
wirebox:parent
Get a reference to the parent injector (if any)
wirebox:eventManager
Get a reference to injector's event manager
wirebox:binder
Get a reference to the injector's binder
wirebox:populator
Get a reference to a WireBox's Object Populator utility
wirebox:scope:{scope}
Get a direct reference to an internal or custom scope object
wirebox:properties
Get the entire properties structure the injector is initialized with. If running within a ColdBox context then it is the structure of application settings
wirebox:property:{name}
Retrieve one key of the properties structure
That's right, go to the handler now, no need of data layers or services, we build them for you!
Now spice it up
Let's use CommandBox to build it:
Then spice it up with the validation constraints
You will first make sure your contacts
datsource exists in the Administrator and then we can declare our ORM settings in our Application.cfc
These are the vanilla settings for using the ORM with ColdBox. Make sure that flushAtRequestEnd
and autoManageSession
are set to false as the ORM extensions will manage that for you.
In this example, we also use dbcreate="update"
as we want ColdFusion ORM to build the database for us which allows us to concentrate on the domain problem at hand and not persistence. You also see that we add our own eventHandler which points to the extension's event handler so we can make Active Entity become well, Active!
Now open your ColdBox.cfc
and add the following to activate ORM injections inside of your configure()
method.
Now let us relish in the power of the orm-crud
command:
That's it.
The command will create the handler with RESTFul capabilities, the views and all the necessary machinery for you to manage Contacts! Now go get a nice latte in celebration!