You sure can do this. This approach is used by RemObjects DataAbstract (http://old.wiki.remobjects.com/wiki/Business_Rules_Scripting_API). The principle here is to define business-rules that will either apply on the client and on the server, or the server only. You will almost never have to check for business-rules ONLY on the client, because you can never "trust" the client to check your business rules.
CQRS and DDD are two architectural principles that could help you here. Domain Driven Design will kind of "clean" or "refine" your code, pushing the infrastructure away from the core "domain" logic. And business rules apply only in the domain, so it's a good idea the keep the domain isolated from the rest.
Command-Query-Responsability-Segretation. I like this one a lot. Basically, you define a set of commands that will be validated before they are applied. There's no more machine-like code that looks like Model.Set('a', 2). Your code, using this principle, will look like MyUnderstandableBusinessObject.UnderstandableCommand(aFriendlyArgument). When it comes to applying business rules, this is very handy that your actual commands reflect the use cases of your domain.
I also always encounter this problem when I work on node.js / javascript projects. The problem is that you do not have a standardized ORM that can be understood by both the client AND the server. This is paradoxal, as node.js and the browser are running on the same language. When I was drawn towards Node.js, I told myself, man both client and server are running the same language, that's going to save sooo much time. But that was kind of false, as there are not that many mature and professional tools out there, even if npm is really active.
I wanted to build an ORM too that could be both understood by the client/server, and add a relational aspect to it (so that it was compatible with SQL) but I kind of abandoned the project. https://github.com/ludydoo/affinity
But, there are a couple of other solutions. Backbone is one, and it's lightweight.
The actual implementation of your business-rule checking here is what you are going to have to work on. You'll want to extract the "validation" part out of your model into another object that will be able to be shared. Something to get you started :
https://jsfiddle.net/ludydoo/y0otcvrf/
BusinessRuleRepository = function() {
this.rules = [];
}
BusinessRuleRepository.prototype.addRule = function(aModelClass, operation, callback) {
this.rules.push({
class: aModelClass,
operation: operation,
callback: callback
})
}
BusinessRuleRepository.prototype.validate = function(object, operation, args) {
_.forIn(this.rules, function(rule) {
if (object.constructor == rule.class && operation == rule.operation) {
rule.callback(object, args)
}
})
}
MyObject = function() {
this.a = 2;
}
MyObject.prototype.setA = function(value) {
aBusinessRuleRepo.validate(this, 'setA', arguments);
this.a = value;
}
// Creating the repository
var aBusinessRuleRepo = new BusinessRuleRepository();
//-------------------------------
// shared.js
var shared = function(aRepository) {
aRepository.addRule(MyObject, 'setA', function(object, args) {
if (args[0] < 0) {
throw 'Invalid value A';
}
})
}
if (aBusinessRuleRepo != undefined) {
shared(aBusinessRuleRepo);
}
//-------------------------------
// Creating the object
var aObject = new MyObject();
try {
aObject.setA(-1); // throws
} catch (err) {
alert('Shared Error : ' + err);
}
aObject.setA(2);
//-------------------------------
// server.js
var server = function(aRepository) {
aRepository.addRule(MyObject, 'setA', function(object, args) {
if (args[0] > 100) {
throw 'Invalid value A';
}
})
}
if (aBusinessRuleRepo != undefined) {
server(aBusinessRuleRepo);
}
//-------------------------------
// on server
try {
aObject.setA(200); // throws
} catch (err) {
alert('Server Error :' + err);
}
The first thing of all is to model and define your domain. You'll have a set of classes that represent your business objects, as well as methods that correspond th business-operations. (I would really go with CQRS for your case)
The model definition would be shared between the client and the server.
You would have to define two files, or two objects. Separated. ServerRules and SharedRules. Those will be a set of Repository.addRule() calls that will register you business rules in the repository. Your client will get the Shared.js business rules, and the server the Shared.js + Server.js business rules. Those business rules will always be applied on your objects this way.
The little example of code I shown you is very simple, and checks business rules only before the command is applied. Maybe you could add a parameter 'beforeCommand' and 'afterCommand' to check business rules before and after changed are made. Then, if you add the possibility of checking business rules after a command is applied, you must be able to rollback the changes (backbone has this functionality I think).
Good luck
You could automate this a little by automatically getting the name of the method you are in (Can I get the name of the currently running function in JavaScript?)
function checkBusinessRules(model, arguments){
businessRuleRepo.validate(model, getCalleeName, arguments);
}
Model.prototype.command = function(arg){
checkBusinessRules(this, arguments);
// perform logic
}
EDIT 2
A small detail i would like to correct on my first answer. Do not implement your business rules on property setters! Use business operation names instead :
You must make sure that you always set your model properties through methods. If you set your model properties directly by assigning a value, you're bypassing the whole business rule processor thing.
The cheap way is to do this through standard setters such as
SetMyProperty(value);
SetAnotherProperty(value);
This is kind of the low-level business rule logic (based on getters and setters). Then, your business rules will also be low-level. Which is kind of bad.
Better, you should do this through business understandable high-level method names such as
RegisterClient(client);
InvalidateMandate(mandate);
Then, your business rules become way more understandable and you'll almost have a good time implementing them.
BusinessRuleRepository.add(ModelClass, "RegisterClient", function(){
if (!Session.can('RegisterClient')) { fail('Unauthorized'); }
})