One of the limitations of those technologies was that the framework would only call the default, parameterless constructor of the page and service classes. To allow unit testing, I used "poor man's" dependency injection (DI), which I learned from Jean-Paul Boodhoo's videos on DnRTV. This is the approach where you have 2 constructors: a default one that calls the other one, which takes instances of all needed dependencies. The default constructor created instances of concrete classes that implemented interfaces. E.g.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface InterfaceA | |
{ | |
string GetSomethingA(); | |
} | |
public class ClassA : InterfaceA | |
{ | |
private readonly InterfaceB b; | |
public ClassA() : this(new ClassB()) | |
{ | |
} | |
public ClassA(InterfaceB b) | |
{ | |
this.b = b; | |
} | |
public string GetSomethingA() | |
{ | |
return this.b.GetSomethingB(); | |
} | |
} | |
public interface InterfaceB | |
{ | |
string GetSomethingB(); | |
} | |
public class ClassB : InterfaceB | |
{ | |
private readonly IRestService service; | |
public ClassB() : this(new RestService()) | |
{ | |
} | |
public ClassB(IRestService service) | |
{ | |
this.service = service; | |
} | |
public string GetSomethingB() | |
{ | |
return this.service.GetDataFromRestService(); | |
} | |
} | |
public interface IRestService | |
{ | |
string GetDataFromRestService(); | |
} | |
public class RestService : IRestService | |
{ | |
public string GetDataFromRestService() | |
{ | |
return new WebClient().DownloadString("http://example.com"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public partial class _Default : Page | |
{ | |
private readonly InterfaceA a; | |
private Label somethingLabel = new Label(); | |
public Label SomethingLabel { get { return this.somethingLabel; } } | |
public _Default() : this(new ClassA()) | |
{ | |
} | |
public _Default(InterfaceA a) | |
{ | |
this.a = a; | |
} | |
public void Page_Load(object sender, EventArgs e) | |
{ | |
this.somethingLabel.Text = a.GetSomethingA(); | |
} | |
} | |
// Test class | |
[TestFixture] | |
public class DefaultTests | |
{ | |
[Test] | |
public void SetSomethingLabel() | |
{ | |
const string response = "something"; | |
var mockA = new MockA { Response = response }; | |
var defaultPage = new _Default(mockA); | |
defaultPage.Page_Load(this, new EventArgs()); | |
Assert.That(defaultPage.SomethingLabel.Text, Is.EqualTo(response)); | |
} | |
} | |
public class MockA : InterfaceA | |
{ | |
public string Response { get; set; } | |
public string GetSomethingA() | |
{ | |
return Response; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[TestFixture] | |
public class DefaultIntegrationTests | |
{ | |
[Test] | |
public void SetSomethingLabel() | |
{ | |
const string response = "something"; | |
var classA = new ClassA(new ClassB(new MockRestService { Response = response })); | |
var defaultPage = new _Default(classA); | |
defaultPage.Page_Load(this, new EventArgs()); | |
Assert.That(defaultPage.SomethingLabel.Text, Is.EqualTo(response)); | |
} | |
} | |
public class MockRestService : IRestService | |
{ | |
public string Response { get; set; } | |
public string GetDataFromRestService() | |
{ | |
return this.Response; | |
} | |
} |
If I'd have known more about the dependency inversion principle then, I would have been very skeptical about putting up with this limitation. I would have immediately gone searching for ways to insert something into the web request pipeline to control page and service creation. If I need to replace an implementation at the bottom of a dependency graph, it should be as easy as replacing one interface registration in an IoC container.
Searching the web now it looks like it was possible, but not in an a way that made you feel good. They look like hacks, or the domain of .NET experts. If you wanted to stick to the SOLID principles, though, it's what you should have done.
I do find it surprising that Microsoft gave us frameworks based primarily on object oriented languages (C#, VB.NET) that didn't let you observe object-oriented (OO) practices. Every example from that time involved creating instances of concrete classes in the page or service classes themselves. I even remember one example that told developers to drag and drop a new SqlConnection object onto every page. Not very maintainable. Perhaps Microsoft didn't think the existing MS web developers of the day could embrace these concepts, so many being ASP/VB6 devs.
Whatever the reason, these limitations lead to some particularly gnarly code, completely lacking in abstractions and injection points. For example, the WCF service I'm currently tearing apart to allow mocking out the bottom-most layer. It goes something like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The WCF service class | |
public class Service1 : IService1 | |
{ | |
public string GetData(int value) | |
{ | |
return ServiceLogic.GetData(value); | |
} | |
} | |
// Class with static methods and a static instance of DB/service instances | |
public class ServiceLogic | |
{ | |
private static Context context; | |
internal static Context Context | |
{ | |
get | |
{ | |
if (context == null) | |
{ | |
initializeContext(); | |
} | |
return context; | |
} | |
} | |
// Used for unit testing. Must be set back to null or previous value after test or before next test. | |
internal static void InjectContext(Context context) | |
{ | |
ServiceLogic.context = context; | |
} | |
static void initializeContext() | |
{ | |
context = new Context(); | |
} | |
public static string GetData(int value) | |
{ | |
return Context.GetData(value); | |
} | |
} | |
public class Context | |
{ | |
private DataLayer dataLayer; | |
public Context() | |
{ | |
this.dataLayer = new DataLayer(); | |
} | |
public string GetData(int value) | |
{ | |
return this.dataLayer.GetData(value); | |
} | |
} | |
public class DataLayer | |
{ | |
public string GetData(int value) | |
{ | |
return string.Format("You entered: {0}", value); | |
} | |
} |
It wasn't until ASP.NET MVC 3 that Microsoft built IoC into ASP.NET MVC from the start, allowing controllers to take dependencies in constructors. Until then, people used IoC containers that implemented the complex looking code that allowed using DI in ASP.NET. I'm still surprised that it took so long for them to bake this in.
The next time I look at a technology that is based primarily on an OO language, I'll be looking for the injection points, no matter how complex they are to use. If they don't exist, the technology will need a very compelling reason for me to use it.
No comments:
Post a Comment