eXtreme .NET: Introducing eXtreme Programming Techniques to .NET Developers [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

eXtreme .NET: Introducing eXtreme Programming Techniques to .NET Developers [Electronic resources] - نسخه متنی

Neil Roodyn

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید












Let's Start Refactoring


Hopefully by now you can see there are some benefits to giving your code the refactoring workout. So how do you go about doing it? Let's get started with an exercise to introduce some basic ideas about refactoring.


Exercise 5-1: Currency Converter Refactoring


For this exercise to be both achievable and printable within the size constraints of this book, the example is reasonably small. If this were the entire piece of software, it is highly unlikely you would refactor it. It is also unlikely you would get paid anything for building it! The application is a simple currency converter that works with conversion rates that are purely the wishes of an Englishman who spends much of his time in Australia and the United States. We will start with the existing Currency Converter application and take some steps to refactor the code. You can download the source code for this application from http://eXtreme.NET.Roodyn.com/BookExercises.aspx.

After we have done the refactoring, the code will be in better shape for us to add new functionality, such as increasing the number of currencies. The refactoring steps each contain one or two well-defined refactoring techniques. The names of these techniques have come from Martin Fowler's Refactoring book.


Step One: Extract Method




1.

Open the Currency Converter solution in Visual Studio.

2.

Examine the code and the form. Notice that it is a very simple program with a basic form. Things to notice include the following:

    All the functionality occurs in the form code.

    There are no tests.

    There is not much else to it!

3.

First we need some tests. We cannot refactor without validating we have not changed behavior. This presents a dilemma: There are no tests to start with, so how do we know we are not refactoring a bug? Also all the code is tightly tied into the GUI; this makes it hard to test and write tests for. Should we move the code away from the GUI first, or should we attempt to write some clever tests to test the code in the GUI layer? As with many real-world scenarios, we are going to compromise. We will write tests for the code we are going to extract from the GUI layer. Then we will get the tests to pass as we move the code. First we need to add a reference to NUnit in the References section, so right-click References, select Add Reference, and browse for the Nunit.Framework.dll file.

4.

We will create a test fixture class to hold our tests. Right-click the project and select Add / Add Class from the pop-up menu. We will name this class ConvertTests.cs.

5.

Edit the class code so that it uses the Nunit.Framework namespace, and add the TestFixture attribute to the class.


using System;
using NUnit.Framework;
namespace CurrencyConverterCS
{
[TestFixture]
public class ConvertTests
{
}
}

6.

Now we can start writing some tests. The first test will be a breadth test that enables us to start refactoring some functionality with the confidence that we haven't broken anything.


[Test]
public void ConversionBreadth()
{
decimal result;
decimal amount;
CurrencyType fromCur;
CurrencyType toCur;
amount = 100.0M;
fromCur = CurrencyType.US;
toCur = CurrencyType.US;
result = MainForm.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"US to US should be no change");
fromCur = CurrencyType.UK;
toCur = CurrencyType.UK;
result = MainForm.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"UK to UK should be no change");
fromCur = CurrencyType.AUS;
toCur = CurrencyType.AUS;
result = MainForm.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"AUS to AUS should be no change");
decimal expected;
fromCur = CurrencyType.US;
toCur = CurrencyType.AUS;
result = MainForm.CurrencyConvert(amount,
fromCur, toCur);
expected = amount * 2;
Assert.AreEqual(expected, result,
"US to AUS is incorrect");
fromCur = CurrencyType.UK;
toCur = CurrencyType.AUS;
result = MainForm.CurrencyConvert(amount,
fromCur, toCur);
expected = amount / 0.5M * 2;
Assert.AreEqual(expected, result,
"UK to AUS is incorrect");
}

7.

As you have probably worked out, this won't compile. We have added a new enumerated type called CurrencyType and are calling a method on the MainForm that doesn't exist. We need to go back to the MainForm and create these. In the MainForm above the class, we add the CurrencyType enum.


public enum CurrencyType
{
US,
AUS,
UK,
}

8.

Next we add the CurrencyConvert method to the MainForm code. We need to make it static or shared so that it can be accessed from the test code. This isn't ideal, but we can work on refactoring away from that later.


public static decimal CurrencyConvert(decimal amount,
CurrencyType fromCur, CurrencyType toCur)
{
return 0;
}

9.

Now you can compile and run the test. It will fail because the method has no body. We need to move the body from the button click.


public static decimal CurrencyConvert(decimal amount,
CurrencyType fromCur, CurrencyType toCur)
{
decimal converted = 0.0M;
decimal initial = 0.0M;
initial = amount;
converted = initial;
if (fromCur == CurrencyType.UK)
{
converted = initial / UKInUS;
}
else if(fromCur == CurrencyType.AUS)
{
converted = initial / AusInUS;
}
if (toCur == CurrencyType.UK)
{
converted = converted * UKInUS;
}
else if(toCur == CurrencyType.AUS)
{
converted = converted * AusInUS;
}
return converted;
}

10.

This will not compile because the UKInUS and AusInUS variables are not static, so we need to change the declaration of these, too.


private static decimal AusInUS = 2;
private static decimal UKInUS = 0.5M;

11.

Now compile and run the test in the NUnit GUI (or console if you prefer); the test should pass.

12.

We can now call this new method from the button click event.


private void ConvertButton_Click
(object sender, System.EventArgs e)
{
decimal converted = 0.0M;
decimal initial = 0.0M;
CurrencyType fromCur = CurrencyType.US;
CurrencyType toCur = CurrencyType.US;
initial = Convert.ToDecimal(Amount.Text);
if (FromUK.Checked)
{
fromCur = CurrencyType.UK;
}
else if (FromAUS.Checked)
{
fromCur = CurrencyType.AUS;
}
if (ToUK.Checked)
{
toCur = CurrencyType.UK;
}
else if (ToAus.Checked)
{
toCur = CurrencyType.AUS;
}
converted = CurrencyConvert(initial, fromCur, toCur);
Result.Text = converted.ToString();
}


You can see that we now have more code than before, and in fact it appears less obvious. This often happens in the early stages of a refactoring session. Don't be afraid of this. If you have an idea of where you want to go, you think at first that you are going in the wrong direction. Have the courage that you are going to get somewhere that is much better in the end, however, and you will find you can get there most of the time. Occasionally, you will be wrong, and then you need to have the courage to admit defeat and throw your changes away. Just do it.


Step Two: Extract Class



At this point, I am looking at the code and thinking that the CurrencyType should be encapsulated in a class that has the conversion routines as methods. To move in this direction, we must create a new class called ConvertibleCurrency. Do this from the right-click pop-up menu in Solution Explorer, as we did before.


1.

Before we get started on the code in the new class, we will define some behaviors of the class in a test. This documents what we are expecting the class to do. Back in the ConvertTests class, we add a new method:


[Test]
public void ConvertTo()
{
ConvertibleCurrency currency;
decimal result;
decimal expected;
currency = new ConvertibleCurrency(
CurrencyType.US, 100.0M);
result = currency.ConvertTo(CurrencyType.US);
Assert.AreEqual(100.0M, result,
"US to US should be no change");
currency = new ConvertibleCurrency(
CurrencyType.AUS, 100.0M);
result = currency.ConvertTo(CurrencyType.UK);
expected = 100.0M / 2 * 0.5M;
Assert.AreEqual(expected, result,
"AUS to UK incorrect result");
}

2.

We have now defined the constructor of the new class and one method, so let's add those to the class.


public ConvertibleCurrency(CurrencyType type, decimal val)
{
}
public decimal ConvertTo(CurrencyType type)
{
decimal converted = 0.0M;
return converted;
}

3.

Compile and run the tests; the latest test will (as expected) fail. We need to implement the constructor and move the code from the MainForm static method into the class.


public class ConvertibleCurrency
{
private decimal amount;
private CurrencyType currency;
public ConvertibleCurrency(CurrencyType type, decimal val)
{
currency = type;
amount = val;
}
public decimal ConvertTo(CurrencyType type)
{
decimal converted = amount;
if (currency == CurrencyType.UK)
{
converted = converted / UKInUS;
}
else if(currency == CurrencyType.AUS)
{
converted = converted / AusInUS;
}
if (type == CurrencyType.UK)
{
converted = converted * UKInUS;
}
else if(type == CurrencyType.AUS)
{
converted = converted * AusInUS;
}
return converted;
}
}

4.

The code will not compile yet because the UKInUS and AusInUS variables cannot be reached from this class; they need to be moved into the class, but they no longer need to be static.


private decimal AusInUS = 2;
private decimal UKInUS = 0.5M;

5.

Now compile and run the tests; they should pass. We are now ready to use this class from the MainForm code.

6.

Change the CurrencyConvert method in the MainForm as follows.


public static decimal CurrencyConvert(decimal amount,
CurrencyType fromCur, CurrencyType toCur)
{
decimal converted = 0.0M;
ConvertibleCurrency currency =
new ConvertibleCurrency(fromCur, amount);
converted = currency.ConvertTo(toCur);
return converted;
}

7.

You can also delete the UKInUS and AusInUS variables from the MainForm code. These variables are now declared and used in the ConvertibleCurrency class.

8.

Now would also be a good time to move the CurrencyType enum declaration into the ConvertibleCurrency class file.

9.

Compile the project and run the tests. The tests should pass, and the program should still function as before. We have encapsulated the behavior of the currency converter into a class and reduced the functionality in the GUI. This helps with testing and with porting the program to another user interface such as the Web.



Step Three: Move Method and Extract Method



Before we finish with the GUI layer, we can move the static CurrencyConvert method from the MainForm to the ConvertibleCurrency class. This is a more sensible place for this method because it deals with the ConvertibleCurrency class.

Cut and paste the method into the class and compile the project. You should get some build errorssix to be precise. Five are in the test class for calls to the MainForm method we just moved, and the sixth one is in MainForm where the call to the method was made.

The method we moved is still static, so we need to prefix it with the class name ConvertibleCurrency in these six places.

I often use the compiler in this way to detect the places I have broken code when I move a method or change a name. The compiler is far better at finding all the places the change has effected than I am!


1.

Now compile and run the tests again; they should pass. The program should still function as before.

2.

We can now turn our attention to the code in the ConvertibleCurrency class. The if-else blocks of code that convert the amount to U.S. dollars and then to the currency requested look like good candidates for extraction. We'll start by taking the block that converts the amount to U.S. dollars and create a method called ConvertToUS.


private decimal ConvertToUS()
{
decimal converted = 0.0M;
converted = amount;
if (currency == CurrencyType.UK)
{
converted = converted / UKInUS;
}
else if (currency == CurrencyType.AUS)
{
converted = converted / AusInUS;
}
return converted;
}

Then replace the code in the ConvertTo function so it looks like this:


public decimal ConvertTo(CurrencyType type)
{
decimal converted = ConvertToUS();
if (type == CurrencyType.UK)
{
converted = converted * UKInUS;
}
else if(type == CurrencyType.AUS)
{
converted = converted * AusInUS;
}
return converted;
}

Compile and run the tests to validate we haven't broken anything.

3.

We can do the same with the second if-else block by creating a function called ConvertFromUS.


private decimal ConvertFromUS(CurrencyType type,
decimal USAmount)
{
decimal converted = 0.0M;
converted = USAmount;
if (type == CurrencyType.UK)
{
converted = converted * UKInUS;
}
else if (type == CurrencyType.AUS)
{
converted = converted * AusInUS;
}
return converted;
}

4.

Again we need to change the code in the ConvertTo method so that it calls this new method.


public decimal ConvertTo(CurrencyType type)
{
decimal converted = ConvertToUS();
converted = ConvertFromUS(type, converted);
return converted;
}

5.

Compile and run the tests. The fact they work shows us we haven't made any changes to the functionality, even though we have changed the structure of the code in a fairly big way.



Step Four: Replace Type Code with State/Strategy and Replace Conditional with Polymorphism



We are now in a [2] from the code. There are two areas of the code that are smelly: the enumerated type and the if-else blocks. Interestingly, the enumerated type and if-else code are related. The fact they are related makes the smell even fouler!


1.

We will start by using Replace Type Code with State/Strategy. The CurrencyType enumerated type can be replaced with a simple object hierarchy. We'll start by coding the hierarchy.


public abstract class BaseCurrency
{
public abstract decimal InUS
{
get;
}
}
public class USCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 1; }
}
}
public class UKCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 0.5M; }
}
}
public class AUSCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 2; }
}
}

2.

Check it compiles and the tests still run. They should because we haven't yet touched any of the code being called. We will do this now by replacing the use of the enumerated type in the CovertibleCurrency class with the class types we have just created.


public class ConvertibleCurrency
{
private decimal amount;
private BaseCurrency currency;
public static decimal CurrencyConvert(decimal amount,
BaseCurrency fromCur, BaseCurrency toCur)
{
decimal converted = 0.0M;
ConvertibleCurrency currency =
new ConvertibleCurrency(fromCur, amount);
converted = currency.ConvertTo(toCur);
return converted;
}
public ConvertibleCurrency(BaseCurrency type, decimal val)
{
currency = type;
amount = val;
}
public decimal ConvertTo(BaseCurrency type)
{
decimal converted = ConvertToUS();
converted = ConvertFromUS(type, converted);
return converted;
}
private decimal ConvertToUS()
{
decimal converted = 0.0M;
converted = amount / currency.InUS;
return converted;
}
private decimal ConvertFromUS(BaseCurrency type,
decimal USAmount)
{
decimal converted = 0.0M;
converted = USAmount * type.InUS;
return converted;
}
}

3.

Pay careful attention to what has changed in the ConvertibleCurrency class. The if-else statements have disappeared through the use of the hierarchy. The replacement of the enumerated type with a hierarchy has enforced a better code base. The two member variables AusInUS and UKInUS are no longer needed; they have also been deleted.

If we now compile this project, we should find all the places that call the class that also need to be changed; this will be in the MainForm and in the tests. The tests are an area of the code where we need to tread especially carefully. Changing the test code could cause us to inadvertently break one of the tests. The methods in the test code should now read as follows.


[Test]
public void ConversionBreadth()
{
decimal result;
decimal amount;
BaseCurrency fromCur;
BaseCurrency toCur;
amount = 100.0M;
fromCur = new USCurrency();
toCur = new USCurrency();
result = ConvertibleCurrency.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"US to US should be no change");
fromCur = new UKCurrency();
toCur = new UKCurrency();
result = ConvertibleCurrency.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"UK to UK should be no change");
fromCur = new AUSCurrency();
toCur = new AUSCurrency();
result = ConvertibleCurrency.CurrencyConvert(amount,
fromCur, toCur);
Assert.AreEqual(100.0M, result,
"AUS to AUS should be no change");
decimal expected;
fromCur = new USCurrency();
toCur = new AUSCurrency();
result = ConvertibleCurrency.CurrencyConvert(amount,
fromCur, toCur);
expected = amount * 2;
Assert.AreEqual(expected, result,
"US to AUS is incorrect");
fromCur = new UKCurrency();
toCur = new AUSCurrency();
result = ConvertibleCurrency.CurrencyConvert(amount,
fromCur, toCur);
expected = amount / 0.5M * 2;
Assert.AreEqual(expected, result,
"UK to AUS is incorrect");
}
[Test]
public void ConvertTo()
{
ConvertibleCurrency currency;
decimal result;
decimal expected;
currency = new ConvertibleCurrency(
new USCurrency(), 100.0M);
result = currency.ConvertTo(new USCurrency());
Assert.AreEqual(100.0M, result,
"US to US should be no change");
currency = new ConvertibleCurrency(
new AUSCurrency(), 100.0M);
result = currency.ConvertTo(new UKCurrency());
expected = 100.0M / 2 * 0.5M;
Assert.AreEqual(expected, result,
"AUS to UK incorrect result");
}

4.

In the MainForm code, the button click code should now look like this.


private void ConvertButton_Click
(object sender, System.EventArgs e)
{
decimal converted = 0.0M;
decimal initial = 0.0M;
BaseCurrency fromCur;
BaseCurrency toCur;
initial = Convert.ToDecimal(Amount.Text);
if (FromUK.Checked)
{
fromCur = new UKCurrency();
}
else if (FromAUS.Checked)
{
fromCur = new AUSCurrency();
}
else
{
fromCur = new USCurrency();
}
if (ToUK.Checked)
{
toCur = new UKCurrency();
}
else if (ToAus.Checked)
{
toCur = new AUSCurrency();
}
else
{
toCur = new USCurrency();
}
converted = ConvertibleCurrency.CurrencyConvert(
initial, fromCur, toCur);
Result.Text = converted.ToString();
}

5.

Notice that we have actually added an extra else condition to the if-else block. The fact that we have replaced the enum with a hierarchy means we no longer can have a default value; this is forcing our code to be more type safe. This lack of type safety might have been the cause of a bug in a later stage, but we have prevented this bug from being born.

6.

Compile and run the tests. We should be back to a working state and ready to move forward to the last step.



Step Five: Replace Conditional with Polymorphism




1.

Because we are now confident that we do not need the enumerated type anymore, we can delete it from the code. However, there is still a small whiff in the MainForm code. Although we've eliminated the if-else blocks of code in the ConvertibleCurrency class, they still linger in the button click method. To remove them, we can use the same hierarchy we have already created. We can also take advantage of a feature of the .NET Framework; if you bind a list of objects to a list box or a combo box, the box will display each object using its ToString method. We will therefore override the ToString method in our concrete currency type classes.


public class USCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 1; }
}
public override string ToString()
{
return "US$";
}
}
public class UKCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 0.5M; }
}
public override string ToString()
{
return "UK£";
}
}
public class AUSCurrency : BaseCurrency
{
public override decimal InUS
{
get{ return 2; }
}
public override string ToString()
{
return "AU$";
}
}

2.

Return to the MainForm in the design view and replace the From and To radio button groups with combo boxes (called fromCombo and toCombo), as shown in Figure 5-1.


Figure 5-1. Replace radio buttons with combo boxes.



3.

Double-click the form to generate the MainForm_Load method. In here we will create a list of the concrete currency types and bind them to the combo boxes.


private void MainForm_Load(object sender, System.EventArgs e)
{
ArrayList currencyList = new ArrayList();
currencyList.Add(new UKCurrency());
currencyList.Add(new USCurrency());
currencyList.Add(new AUSCurrency());
fromCombo.DataSource = currencyList;
toCombo.DataSource = currencyList.Clone();
}

4.

In the button click event, we now need to get the selected object from the combo boxes.


private void ConvertButton_Click
(object sender, System.EventArgs e)
{
decimal converted = 0.0M;
decimal initial = 0.0M;
BaseCurrency fromCur;
BaseCurrency toCur;
initial = Convert.ToDecimal(Amount.Text);
fromCur = fromCombo.SelectedItem as BaseCurrency;
toCur = toCombo.SelectedItem as BaseCurrency;
converted = ConvertibleCurrency.CurrencyConvert(initial,
fromCur, toCur);
Result.Text = converted.ToString();
}

5.

Compile and run the tests, and then do some manual testing to verify you have now created a working piece of software.



Step Six: Customize



I leave it to you to now explore what else you can do with this code. Some ideas for you are as follows:

    Create an ASP.NET Web interface for the code.

    Add another set of currencies to the application.

    Collect the currency exchange rates from an XML file.



    / 117