Skip to content

Instantly share code, notes, and snippets.

@gpittau
Last active August 29, 2015 14:19
Show Gist options
  • Save gpittau/4c149e2bd252e0d55f94 to your computer and use it in GitHub Desktop.
Save gpittau/4c149e2bd252e0d55f94 to your computer and use it in GitHub Desktop.

#Reduciendo la complejidad con un Motor de Reglas de Negocio

##Complejidad Ciclomática

La complejidad ciclomática es una medida de las bifurcaciones de control producidas por un código en cuestión. Cada punto de complejidad, representa un caso de testing, un riesgo de ocurrencia de bugs, un punto extra de costo de desarrollo y mantenimiento.

La reducción de la complejidad es una cualidad de un buen enfoque de programación, sin embargo, las técnicas y patrones para lograrla son escasos y constituyen más un arte que una práctica formal.

##Correspondencia entre Reglas de Negocio y Complejidad

En el contexto de un proyecto, si despejamos el código accidental, cada punto de complejidad se corresponde con una regla de negocio, de modo que la complejidad del negocio debería tener una relación directa con el costo de implementación y testeos, y es además, el escenario más optimista en las estimaciones.

Normalmente, la implementación de las reglas de negocio en javascript puro tendrá una métrica de complejidad mínima en función directa de la complejidad intrínseca del problema a resolver (las reglas del negocio), todo punto extra es imputable a código accidental, deficiencias del enfoque o de la arquitectura.

Sin embargo, con un enfoque declarativo, es posible reducir la complejidad aún por debajo del mínimo impuesto por las reglas de negocio.

##Top-Down, Bottom-Up y Lenguajes de Propósito Específico

Un enfoque declarativo implica un cambio de paradigma de programación, pero también, implica un cambio en la estrategia análisis del problema.

Mayoritariamente, es costumbre analizar los problemas en un enfoque “Top-Down” que consiste en subdividir el problema sucesivamente hasta alcanzar un nivel en el que es posible resolverlo con las primitivas del lenguaje (javascript).

Existe otro enfoque que consiste en construir un lenguaje de propósito específico cuyas primitivas habilitan la implementación (trivial) de la solución , éste enfoque es conocido como “Botton-Up”. El objetivo consiste en minimizar la distancia que existe entre el problema y el lenguaje que se utiliza para implementarlo.

En la práctica, ambos enfoques pueden coexistir, y un balance óptimo permite realizar implementaciones muy elegantes (bottom-up) sin sacrificar el pragmatismo necesario para realizar labores cotidianas (top-down)

##Análisis Top-Down/Botom-Up del problema Reglas de Negocio

Si, tomamos un enunciado de ”acceptance test“ estándar:

  • Given my bank account is in credit, and I made no withdrawals recently,
  • When I attempt to withdraw an amount less than my card's limit,
  • Then the withdrawal should complete without errors or warnings

Un primer análisis permite identificar los elementos participantes:

  • Given myBankAccount.state contains hasCredit and not contains hasWithdrawalsRecently
  • When myAction.action is-equal Attempt and myAction.type is-equal Withdraw and myAction.amount is-less-than myBankAccount.creditCardLimit
  • Then the withdrawal should complete without errors or warnings

Los elementos que componen la regla se agrupan en datos estructurales y operadores lógicos

####datos estructurales

  • myBankAccount
    • state: array of state tokens
    • creditCardLimit: currency/number
  • myAction
    • state: token/string
    • type: token/string
    • amount: currency/number

####operadores lógicos

  • contains
  • not
  • and
  • is-less-than
  • is-equal

###BDD:

describe('Given my bank account is in credit and I made no withdrawals recently', function() {
        context('When I attempt to withdraw an amount less than my cards limit', function () {
                it('Then the withdrawal should complete without errors or warnings', function () {
                        var
                        myBankAccount = {
                            state: ['hasCredit'],
                            creditCardLimit: 50
                        },
                        myAction = {
                            action: 'Attempt',
                            type: 'Withdraw',
                            amount: myBankAccount.creditCardLimit - 1
                        };
                        isValidAction(myBankAccount, myAction).should.match(/complete/);
                    }
                );
            }
        );
...

###implementación en javascript:

function isValidAction(myBankAccount, myAction) {
    var found = false;
    for (state in myBankAccount.state) {
        if (state === ’hasCredit’) {
            found = true;
            for (state in myBankAccount.state) {
                if (state === ’hasWithdrawalsRecently’) {
                    found = false;
                    break;
                }
            };
            break;
        }
    }
    if (found &&
        (myAccount.action === ’Attempt’) &&
        (myAction.type === ‘Withdraw’) &&
        (myAction.amount < myBankAccount.creditCardLimit)) {
        return 'complete';
    } else {
        return 'errors or warnings';
    }
}

Metricas

  • Cyclomatic complexity: 4
  • Cyclomatic complexity density: 31%
function isValidAction(myBankAccount, myAction) {
    var found = _(myBankAccount.state).contains('hasCredit') &&
    !_(myBankAccount.state).contains('hasWithdrawalRecently');
    if (found &&
        (myAction.action === 'Attempt') &&
        (myAction.type === 'Withdraw') &&
        (myAction.amount < myBankAccount.creditCardLimit)) {
        return 'complete';
    } else {
        return 'errors or warnings';
    }
}

Metricas:

  • Cyclomatic complexity: 2
  • Cyclomatic complexity density: 33%
<iframe width="100%" height="300" src="//jsfiddle.net/gpittau/dcsnp4sy/embedded/" allowfullscreen="allowfullscreen" frameborder="0"></iframe>
function isValidAction(myBankAccount, myAction) {
    var is_less_than = function(rightKey) {
        return function(leftKey) {
            var
                rVal = this[rightKey],
                lVal = this[leftKey];
            return lVal < rVal;
        };
    };
    return matchRules(
        {
            hasCredit: _(myBankAccount.state).contains('hasCredit'),
            hasWithdrawalRecently: _(myBankAccount.state).contains('hasWithdrawalRecently'),
            creditCardLimit: myBankAccount.creditCardLimit,
            action: myAction.action,
            type: myAction.type,
            amount: myAction.amount
        }, [
            {
                'matches': [{
                    hasWithdrawalRecently: true
                }],
                'returns': 'errors or warnings'
            },
            {
                'matches': [{
                    hasWithdrawalRecently: false,
                    hasCredit: true,
                    action: 'Attempt',
                    type: 'Withdraw',
                    amount: is_less_than('creditCardLimit')
                }],
                'returns': 'complete'
            },
            {
                'matches': [{
                    accountState: '*'
                }],
                'returns': 'errors or warnings'
            }
        ]
    );
}

Metricas

  • Cyclomatic complexity: 1
  • Cyclomatic complexity density: 4%
function isValidAction(myBankAccount, myAction) {
var found = false;
for (s1 in myBankAccount.state) {
if (myBankAccount.state[s1] === 'hasCredit') {
found = true;
for (s2 in myBankAccount.state) {
if (myBankAccount.state[s2] === 'hasWithdrawalRecently') {
found = false;
break;
}
};
break;
}
}
if (found &&
(myAction.action === 'Attempt') &&
(myAction.type === 'Withdraw') &&
(myAction.amount < myBankAccount.creditCardLimit)) {
return 'complete';
} else {
return 'errors or warnings';
}
}
function isValidAction(myBankAccount, myAction) {
return matchRules(
{
hasCredit: _(myBankAccount.state).contains('hasCredit'),
hasWithdrawalRecently: _(myBankAccount.state).contains('hasWithdrawalRecently'),
action: myAction.action,
type: myAction.type,
amount_less_than_credit: myAction.amount < myBankAccount.creditCardLimit
}, [
{
'matches': [{
hasWithdrawalRecently: true
}],
'returns': 'errors or warnings'
},
{
'matches': [{
hasWithdrawalRecently: false,
hasCredit: true,
action: 'Attempt',
type: 'Withdraw',
amount_less_than_credit: true
}],
'returns': 'complete'
},
{
'matches': [{
accountState: '*'
}],
'returns': 'errors or warnings'
}
]
);
}
function isValidAction(myBankAccount, myAction) {
var found = _(myBankAccount.state).contains('hasCredit') && !_(myBankAccount.state).contains('hasWithdrawalRecently');
if (found &&
(myAction.action === 'Attempt') &&
(myAction.type === 'Withdraw') &&
(myAction.amount < myBankAccount.creditCardLimit)) {
return 'complete';
} else {
return 'errors or warnings';
}
}
function matchRules (values, rules) {
var
matchTerm = function (termValue, termKey) {
var
starOrEqual = function (value, valueKey) {
return value === '*' || _.isEqual(values[valueKey], value);
},
ensureArray = function (value) {
return _.isArray(value) ? value : [value];
},
ensurePredicate = function (value) {
return _.isFunction(value) ? value : _.partial(starOrEqual, value);
},
predicator = function (value) {
return _.bind(ensurePredicate(value), values)(termKey);
};
return _.any(ensureArray(termValue), predicator);
},
everyClause = _.partial(_.every, _, matchTerm),
anyMatch = _.partial(_.any, _, everyClause),
anyRule = _.compose(anyMatch, _.property('matches')),
result = _.find(rules, anyRule);
return result ? result.returns : !! result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment