Continuiamo con lo smell: Conditional Complexity
Problema:
Esiste un metodo di controllo con una logica condizionale composta da diverse varianti i quali valori si conosceranno solamente a runtime.
La stessa logica di controllo viene riprodotta in varie classi affinchè si ottenga il valore desiderato.
Un esempio di logica errata:
Di seguito un esempio di logica errata, sul quale lavoreremo:
public double Capital() { if (expiry == null && maturity != null) return commitment * Duration() * RiskFactor(); if (expiry != null && maturity == null) { if (GetUnusedPercentage() != 1.0) return commitment * GetUnusedPercentage() * Duration() * RiskFactor(); else return (OutStandingRiskAmount() * Duration() * RiskFactor()) + (UnUsedRiskAmount() * Duration() * UnUsedRiskFactor()); }
return 0.0; }
|
Il codice di sopra è raffigurabile nella seguente maniera:
Motivazione:
Nel momento in cui iniziamo a scrivere delle logiche di controllo, solitamente, si parte da un paio di controlli che, se li volessimo togliere, porterebbero solamente a complicare la leggibilità del codice stesso.
Il problema nasce quando i controlli da effettuare continuano a crescere, senza preoccuparsi di migliorare la leggibilità del codice tramite l’applicazione di un Pattern ne, tanto meno, tentando di rimpicciolire le condizioni.
Soluzione:
Tramite il pattern Strategy semplificheremo il mantenimento delle varianti che potranno nascere.
In pratica, utilizzandolo, avremo tutte le varianti in un’unico punto del codice.
Un’altra soluzione applicabile è tramite il pattern Decorator.
Le condizioni vengono solitamente scritte per capire quale algoritmo utilizzare; tramite:
- Decompose Conditional
- Compose Method
possiamo semplificarne la leggibilità ma avremo tanti piccoli metodi che verranno utilizzati dall’algoritmo.
Questo è il codice che otteniamo applicando il pattern Strategy:
class Program { static void Main(string[] args) { ILoan loan1 = new StandardLoan(); loan1.Capital(); } }
public class CapitalStrategy { private readonly ILoan _loan; private object RiskFactor; private object UnusedRiskFactor;
public CapitalStrategy(ILoan loan, object riskFactor, object unusedRiskFactor) { _loan = loan; RiskFactor = riskFactor; UnusedRiskFactor = unusedRiskFactor; }
public double Capital() { switch (_loan.LoanType) { case LoanType.Standard: return _loan.Commitment * _loan.Duration() * RiskFactorFor(); case LoanType.NonStandardUnUsedPercentage: return _loan.Commitment * _loan.UnusedPercentage() * _loan.Duration() * RiskFactorFor(); case LoanType.NonStandardUnUsedPercentageDotOne: return (_loan.OutstandingRiskAmount() * _loan.Duration() * RiskFactorFor()) + (_loan.UnusedRiskAmount() * _loan.Duration() * UnusedRiskFactorFor()); default: return 0.0; } }
private double UnusedRiskFactorFor() { return UnusedRiskFactor.GetFactor(_load).GetUnusedRiskFactor(); }
private double RiskFactorFor() { return RiskFactor.GetFactor(_load).GetRiskFactor(); } }
public enum LoanType { Standard, NonStandardUnUsedPercentage, NonStandardUnUsedPercentageDotOne }
public interface ILoan { double Capital(); LoanType LoanType { get; } double Commitment { get; } double Duration(); double UnusedPercentage(); double OutstandingRiskAmount(); double UnusedRiskAmount(); }
public class StandardLoan : ILoan { readonly CapitalStrategy _capitalStrategy; private object myRiskFactor = new object(); private object myUnusedRiskFactor = new object();
public StandardLoan() { _capitalStrategy = new CapitalStrategy(this, myRiskFactor, myUnusedRiskFactor); }
#region ILoan Members
public double Capital() { return _capitalStrategy.Capital(); }
public LoanType LoanType { get { return LoanType.Standard; } }
public double Commitment { get { throw new NotImplementedException(); } }
public double Duration() { throw new NotImplementedException(); }
public double UnusedPercentage() { throw new NotImplementedException(); }
public double OutstandingRiskAmount() { throw new NotImplementedException(); }
public double UnusedRiskAmount() { throw new NotImplementedException(); }
#endregion }
public class NonStandardUnUsedPercentage : ILoan { readonly CapitalStrategy _capitalStrategy; private object myRiskFactor = new object(); private object myUnusedRiskFactor = new object();
public NonStandardUnUsedPercentage() { _capitalStrategy = new CapitalStrategy(this, myRiskFactor, myUnusedRiskFactor); }
#region ILoan Members
public double Capital() { return _capitalStrategy.Capital(); }
public LoanType LoanType { get { return LoanType.NonStandardUnUsedPercentage; } }
public double Commitment { get; private set; } public double Duration() { throw new System.NotImplementedException(); }
public double UnusedPercentage() { throw new System.NotImplementedException(); }
public double OutstandingRiskAmount() { throw new System.NotImplementedException(); }
public double UnusedRiskAmount() { throw new System.NotImplementedException(); }
#endregion }
|
Benefici e non
+ Rende gli algoritmi più chiari diminuendo o eliminando le condizioni logiche.
+ Semplifica le classi muovendo le variazioni dell’algoritmo nella gerarchia.
+ Abilita la possibilità di swappare tra un algoritmo e un’altro a run-time.
- Complica il design.
- Complica la visione di come l’algoritmo ottiene/riceve i dati. |
E non prendete come scusa: “il mio sistema ormai è troppo evoluto per poterne apportare queste migliorie. E’ troppo tardi.”
Se fosse realmente così non esisterebbe il Refactoring :)
Per questo post ho preso, molto spunto da libro Refactoring To Patterns di Joshua Kerievsky.