I don't know WinForms (Or EF or really .NET for all that matters), but I'll make a suggestion based on what I do know (or think I might remember).
Lets assume you have a simple form class that calculates risk based on 3 variables, Age, Amount, and Hair Color. When clicking the "Rate" button a label will be updated with the appropriate string: "High", "Medium", and "Low".
public void btnRate_click() {
var RiskScore = 0;
if(txtAge.Value < 25)
RiskScore += 2;
else if(txtAge.Value < 40)
RiskScore += 1;
if(txtAmount.Value < 2.00)
RiskScore += 10;
else if(txtAmount.Value < 10.00)
RiskScore += 3;
else if(txtAmount.Value < 50.00)
RiskScore += 5;
if(txtHairColor.Value == "Red")
RiskScore += 75;
if(RiskScore < 2)
lblResult.Value = "Low";
else if(RiskScore < 8)
lblResult.Value = "Medium";
else
lblResult.Value = "High";
}
Given this code, I would start with some simple "Prefactoring" before laying down some tests. I tend to use standard refactorings that IDEs such as ReSharper can do automatically because they're really safe to do and rarely leave unwanted side effects.
The first thing we'll do is remove the duplication in the code. Specifically by extracting some local variables.
public void btnRate_click() {
var RiskScore = 0;
var Age = txtAge.Value;
if(Age < 25)
RiskScore += 2;
else if(Age < 40)
RiskScore += 1;
var Amount = txtAmount.Value;
if(Amount < 2.00)
RiskScore += 10;
else if(Amount < 10.00)
RiskScore += 3;
else if(Amount < 50.00)
RiskScore += 5;
var HairColor = txtHairColor.Value;
if(HairColor == "Red")
RiskScore += 75;
var Result = ""
if(RiskScore < 2)
Result = "Low";
else if(RiskScore < 8)
Result = "Medium";
else
Result = "High";
lblResult.Value = Result;
}
This refactoring was quick and can be accomplished automatically using Ctrl+Alt+V
We'll now take a moment to organize the code slightly, moving the variable declarations around.
public void btnRate_click() {
var Age = txtAge.Value;
var Amount = txtAmount.Value;
var HairColor = txtHairColor.Value;
var RiskScore = 0;
var Result = ""
if(Age < 25)
RiskScore += 2;
else if(Age < 40)
RiskScore += 1;
if(Amount < 2.00)
RiskScore += 10;
else if(Amount < 10.00)
RiskScore += 3;
else if(Amount < 50.00)
RiskScore += 5;
if(HairColor == "Red")
RiskScore += 75;
if(RiskScore < 2)
Result = "Low";
else if(RiskScore < 8)
Result = "Medium";
else
Result = "High";
lblResult.Value = Result;
}
What we've now managed to accomplish is to isolate the business rules (the risk calculation) from the UI components (the form controls). The next step is to get those business rules out of the UI class and out into somewhere else. Another refactoring is in order now. In looking at this code, there are four variables being operated on in this method. When the code operates on the the same variables, it's often a sign that a class is lurking in there. Let's use the Extract Method Object refactoring here... (I don't remember this keystroke but I'm pretty sure it's in there)
Looking at what this block of code does, we'll call this new class the RiskCalculator
. After the refactoring the code should look something like:
public void btnRate_click() {
var Age = txtAge.Value;
var Amount = txtAmount.Value;
var HairColor = txtHairColor.Value;
var Result = new RiskCalculator(Age, Amount, HairColor).Invoke();
lblResult.Value = Result;
}
// In RiskCalculator.cs
public class RiskCalculator {
private int Age;
private double Amount;
private string HairColor;
public RiskCalculator(int Age, double Amount, string HairColor) {
this.Age = Age;
this.Amount = Amount;
this.HairColor = HairColor;
}
public string Invoke() {
var RiskScore = 0;
var Result = ""
if(Age < 25)
RiskScore += 2;
else if(Age < 40)
RiskScore += 1;
if(Amount < 2.00)
RiskScore += 10;
else if(Amount < 10.00)
RiskScore += 3;
else if(Amount < 50.00)
RiskScore += 5;
if(HairColor == "Red")
RiskScore += 75;
if(RiskScore < 2)
Result = "Low";
else if(RiskScore < 8)
Result = "Medium";
else
Result = "High";
return Result;
}
}
Now we're getting somewhere. Your RiskCalculator
contains only business logic and is now testable. The next step is to write some unit tests around the calculator, validating all of the business rules. This will give you the ability to refactor and clean up the actual calculation code.
Through the activity of writing the tests you may notice that RiskCalculator
isn't really a good name for this class. I mean, if you think about it the data that's being passed into the constructor actually represents your loan! Given that you have a good base of tests for the current RiskCalculator
you can perform a couple of Rename refactorings. We'll start with Rename Method.
The Invoke
method is really uninformative as a name, so we'll rename it. What is it doing? I'd say it's actually performing the risk calculation, so let's call it calculateRisk
. After that, we have to ask ourselves what it's calculating risk for. The answer is the loan, so that will be our second refactoring. We'll use the Rename Class refactoring, renaming the RiskCalculator
to Loan
. This becomes our first domain object in the system.
// RiskCalculator.cs has now become Loan.cs
public class Loan {
// ...
public string CalculateRisk() {
// ...
}
}
After each of these refactorings, the tests should be run and continue to pass. You can continue by cleaning up that awful business logic in the CalculateRisk
method.
Now that our domain object is clean we could turn our attention back to that event handler, which should look something like this:
public void btnRate_click() {
var Age = txtAge.Value;
var Amount = txtAmount.Value;
var HairColor = txtHairColor.Value;
var Result = new Loan(Age, Amount, HairColor).CalculateRisk();
lblResult.Value = Result;
}
I would probably clean this up by doing some Inline Temporary Variable and Extract Variable refactorings:
public void btnRate_click() {
var MicroLoan = new Loan(txtAge.Value, txtAmount.Value, txtHairColor.Value);
lblResult.Value = MicroLoan.CalculateRisk();
}
That's pretty clean now, and there's not really much code in there to mess up, so there's not much of a need for tests at this point.
Hope that helps. I didn't really answer your questions about repositories, but I am hoping that this gives you a path to get started on. When it comes to where to put your logic, keep the Single Responsibility Principle in mind, that will help you decide what goes in your repository and what doesn't. It also might cause you to break the Loan
class into other smaller, more focused classes.
Good Luck!
Brandon