Design and Code Review Checklist: by Tim Star
Design and Code Review Checklist: by Tim Star
By Tim Star
Table of Contents
VISUAL CHECKLIST
Intertech provides a easy to read checklist in the form of an infographic to make
sure your design and code are good to go. See the infographic
Page 1
www.Intertech.com
This Intertech checklist provides a comprehensive compilation of design and code review
principles for consideration during projects. There are items on the checklist that are outlined in
detail further on in the document and a few where we’ve provided links from this document to
quality design and review resources.
Page 2
www.Intertech.com
Aggregation. Similar to composition except when the delegating class is destroyed, the
child classes are not.
Consider Polymorphism. Make a group of heterogeneous classes look homogeneous
Consider generics.
Testability considerations?
YAGNI (You ain’t gonna need it) When in doubt, leave it out!
Does object wake up in a known good state (constructor)
Consider Security
Page 3
www.Intertech.com
Page 4
www.Intertech.com
Inheritance:
• Abstract class - cannot be instantiated. May have abstract and non abstract
methods. Derived class must implement all abstract methods.
• Sealed Class - Cannot be inherited
• Virtual Method - may be overridden
• Abstract method - no implementation, must be overridden
• Sealed method - Cannot be overridden
• Sealed Override Method - No longer may be overridden. Public sealed override
MyMeth()
Page 5
www.Intertech.com
Did we run a code review? If you have a team or enterprise developer edition, the code review
may be found in the Analyze menu.
Page 6
www.Intertech.com
1. [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.
Never)]
2. int aComplexInteger;
3. public int AComplexInteger
4. {
5. get { return aComplexInteger; }
6. set
7. {
8. if (value == 0)
9. throw new ArgumentOutOfRangeException("AComplexInteger");
10. if (value != aComplexInteger)
11. {
12. aComplexInteger = value;
13. //Maybe raise a value changed event
14. }
15. }
16. \
1. using System;
2. using System.Collections.Generic;
3. using System.Linq;
4. using System.Text;
5.
6. namespace CicMaster
7. {
8. class BetterConstructorLogic
9. {
10. #region WhyItIsBetter
11. //No duplicate code, this is called constructor chaining
Page 7
www.Intertech.com
The final technique I would like to mention is use a factory to create all but the simplest objects.
The following (admittedly nonsensical) code needs to execute maybe a half dozen lines of code
to construct an Order object.
1. using System;
2. using System.Collections.Generic;
3. using System.Linq;
4. using System.Text;
5. using System.Threading;
6.
7. namespace BusinessLayer
8. {
9. class ObjectContext
10. {
11. public ObjectContext(String username)
12. {
13. //Look up the user permissions
14. }
15. public bool IsInTranaction { get; set; }
Page 8
www.Intertech.com
Duplicating these few lines of code in a couple places is not that difficult. Now say the
application is enhanced and grows for a few years and suddenly we see this code duplicated
dozens or hundreds of times. At some point it is likely that we want to change the construction
logic, finding and changing all the code we use to create the order is difficult, time consuming,
and a QA burden.
A better approach would be to encapsulate the logic required to build a new order. Here is an
implementation using a simple factory. It is much easier to find, change, and test this code:
Page 9
www.Intertech.com
6. if (!ctx.IsInTranaction)
7. {
8. if (ctx.BeginTransaction())
9. {
10. //...
11. }
12. }
13. return new Order(ctx);
14. }
15. }
16. class DryConsumer
17. {
18. public DryConsumer()
19. {
20. Order order = OrderFactory.GetOrder();
21. }
22. \
If we recognize that we are duplicating even a few lines of code over and over we need to take a
serious look at the code and figure out a way to encapsulate the logic.
Page 10
www.Intertech.com
1. using System.Collections.Generic;
2.
3. namespace LSP
4. {
5. public abstract class WidgetBase
6. {
7. public virtual IList<int> SomeList(string title)
8. {
9. //Some useful boilerplate implementation
10. return new List<int>();
11. }
12. }
13. }
It is a simple abstract class with one method (for now) named SomeList that accepts a string
argument and returns a generic IList of type int. Messing with the signature would certainly be
a violation of the principle but the good news is the compiler saves us from that, changing the
parameter type or return type in our derivation will not compile.
1. using System.Collections.Generic;
2.
3. namespace LSP
4. {
5. public class BrokenWidget:WidgetBase
6. {
7. public override IList<int> SomeList(object title)
Page 11
www.Intertech.com
8. {
9. return new List<int>();
10. }
11.
12. public override IList<object> SomeList(string title)
13. {
14. return new List<object>();
15. }
16. }
17. }
If we are only considering code reuse we might be tempted to say something like “My derivation
is almost like a widget but I don’t have the notion of ‘SomeList’ so I just won’t implement it”.
This will compile:
1. using System;
2. using System.Collections.Generic;
3.
4. namespace LSP
5. {
6. public class AlmostAWidget:WidgetBase
7. {
8. public override IList<int> SomeList(string title)
9. {
10. throw new NotImplementedException();
11. }
12. }
13. }
Page 12
www.Intertech.com
Now let’s add two more derivations to our Model, NotLspWidget which makes use of array
covariance:
1. namespace LSP
2. {
3. public class NotLspWidget:WidgetBase
4. {
5. public override WidgetBase[] Relatives()
6. {
7. return new NotLspWidget[1] { this };
8. }
9. }
10. }
1. namespace LSP
2. {
3. public class LspWidget : WidgetBase
4. {
5. public override WidgetBase[] Relatives()
6. {
7. return new WidgetBase[1] { this };
8. }
9. }
10. }
Notice that with NotLspWidget the backing store for the Relatives array is an array of type
NotLspWidget (the derived type) but with LspWidget the store is an Array of type WidgetBase,
Now consider the following unit tests which will act as our consumer code (the code we, as good
designers, are trying not to break by applying the Liskov Substitution Principle rules correctly to
our object hierarchy).
1. using System;
2. using Microsoft.VisualStudio.TestTools.UnitTesting;
3. using LSP;
4.
5. namespace UnitTestProject1
6. {
7. [TestClass]
8. public class UnitTest1
9. {
10. [TestMethod]
11. public void TestLspCovariance()
12. {
13. WidgetBase w = new NotLspWidget();
14. WidgetBase[] relatives = w. Relatives();
Page 13
www.Intertech.com
15.
16. //try to put some other WidgetBase to the array
17. relatives[0] = new LspWidget();
18. Assert.IsTrue(true);
19. }
20. [TestMethod]
21. public void TestLspCovariance2()
22. {
23. WidgetBase w = new LspWidget();
24. WidgetBase[] relatives = w.Relatives();
25.
26. //try to put some other WidgetBase to the array
27. relatives[0] = new NotLspWidget();
28. Assert.IsTrue(true);
29. }
30. }
31. }
The tests are really simple and similar. In test one I get the relatives from the NotLspWidget,
then try to add and LspWdiget to the array but we get a runtime exception:
Test two does the exact reverse and works perfectly well because the designer of LspWidget did
not rely on covariance to “convert” the returned array. This sounds like a contrived example but
if our consumers are using abstractions, as they should be, they should be able to add abstract
types to arrays of what appear to be abstract types without worrying about this bit of .Net trivia.
Page 14
www.Intertech.com
Now let’s handle the more difficult application of the behavior portion of the principle. For this
we must consider the reasonable assumptions consumers of our object hierarchy will make.
Method Hiding is a pretty nasty violation of the Liskov Substitution Principle that can be
particularly difficult to chase down. For this example we will add the following method to
WidgetBase:
Now we hide this method in NotLspWidget by using the new key word and changing the
algorithm. Note, there is nothing wrong with changing the algorithm!
1. [TestMethod]
2. public void TestHiddenMethod()
3. {
4. NotLspWidget w = new NotLspWidget();
5. Assert.AreEqual(w.DoMath(3), ((WidgetBase)w).DoMath(3));
6. }
Page 15
www.Intertech.com
2. {
3. return null;
4. }
Page 16
www.Intertech.com
Page 17
www.Intertech.com
code should be easier to maintain. If we do not have good unit tests in place I would
suggest that Meyer’s approach is the better way to go but if we do have a good suite of
unit tests in place I am going to favor Martin’s approach
We are not going to look at Meyer’s application of the principle as it is straight forward
and intuitive but let’s look at some violations of Martin’s application of the principle.
Consider the following code:
1. namespace Solid.OCP.Violation
2. {
3. public class DbService
4. {
5. public bool Create()
6. {
7. throw new NotImplementedException();
8. }
9.
10. public int Update()
11. {
12. throw new NotImplementedException();
13. }
14.
15. public List<string> Select()
16. {
17. throw new NotImplementedException();
18. }
19.
20. public int Delete()
21. {
22. throw new NotImplementedException();
23. }
24. }
25. }
1. namespace Solid.OCP.Violation
2. {
3. public class AzureTableService
4. {
5. //Assume Azure Table Storage is NOT updatable
6. //which is not true
7.
Page 18
www.Intertech.com
Again, the method implementation is not important. The shape of the class is similar to
DbService but there is no use of abstraction.
If we need to create a client that works with both services it might look like this:
1. namespace Solid.OCP.Violation
2. {
3. public class Client
4. {
5.
6. public void Save(List<object> persitables)
7. {
8.
9. foreach (var item in persitables)
10. {
11. if(item is AzureTableService)
12. {
13. AzureSave((AzureTableService) item);
14. }
15. else if(item is DbService)
16. {
17. DbSave((DbService)item);
18. }
19. LogMessage("Somebody updated something");
20. }
21. }
22.
23. private void DbSave(DbService container)
24. {
25.
26. if(container.Select().Count >0 )
27. {
28. container.Update();
Page 19
www.Intertech.com
29. }
30. else
31. {
32. container.Create();
33. }
34.
35. }
36.
37. private void AzureSave(AzureTableService container)
38. {
39.
40. if (container.Select().Count > 0)
41. {
42. container.Delete();
43. }
44.
45. container.Create();
46.
47. }
48.
49. private void LogMessage(string msg)
50. {
51. //Log an audit record
52. }
53. }
54. }
Consider the Save method. Conceptually we want to save a list of “things”, if the object doesn’t
exist in the storage device we should create it otherwise we want to update it (this is called an
upsert). Since we didn’t use abstraction, the list of things must be of type “object”. Next we
iterate through the list and inspect each object then we have to run code specific to the type of
object to accomplish the save. There are many problems with this. First if an object other than
AzuerTableService or DbService is in the list we are going to blindly do nothing – the alternative
would be to throw an exception but neither behavior is good. Second, the client must
implement a specialized method for each type of server. Finally, if we want to add a new server
to the mix we have to crack open the client, modify the if/then or maybe add a switch as the
number of different Servers we support grows and we also have to add the specialized method
mentioned above for each Server.
Now consider a design where we make use of an abstraction. First we will define an abstraction
called IUpsertable
1. namespace Solid.OCP.Better
2. {
3. //************ Put this in a seperate assembly ************
4.
5.
Page 20
www.Intertech.com
We are using an interface, it could also have been an abstract class if there was some shared
implementation.
The key now is that each of our servers is going to implement this interface:
1. namespace Solid.OCP.Better
2. {
3. //************ Put this in a seperate assembly ************
4.
5. public class DbService:IUpsertable
6. {
7. public bool Create()
8. {
9. throw new NotImplementedException();
10. }
11.
12. public int Update()
13. {
14. throw new NotImplementedException();
15. }
16.
17. public List<string> Select()
18. {
19. throw new NotImplementedException();
20. }
21.
22. public int Delete()
23. {
24. throw new NotImplementedException();
25. }
26.
27.
28. public int Upsert()
29. {
30.
31. if (this.Select().Count() > 0)
32. {
33. return this.Update();
34. }
35. this.Create();
Page 21
www.Intertech.com
36. return 1;
37. }
38. }
39. }
1. namespace Solid.OCP.Better
2. {
3. //************ Put this in a seperate assembly ************
4.
5. public class AzureTableService : IUpsertable
6. {
7. public bool Create()
8. {
9. throw new NotImplementedException();
10. }
11.
12. public int Upsert()
13. {
14. int count = 1;
15. if (this.Select().Count()>0)
16. {
17. count = this.Delete();
18. }
19. this.Create();
20. return count;
21. }
22.
23. public List<string> Select()
24. {
25. throw new NotImplementedException();
26. }
27.
28. public int Delete()
29. {
30. throw new NotImplementedException();
31. }
32.
33. }
1. namespace Solid.OCP.Better
2. {
3. public class Client
4. {
5. public void Save(List<IUpsertable> persitables)
6. {
Page 22
www.Intertech.com
7.
8. foreach (var item in persitables)
9. {
10. item.Upsert();
11. LogMessage("Somebody updated something");
12. }
13.
14.
15. }
16. private void LogMessage(string msg)
17. {
18. //Log an audit record
19. }
20.
21. }
22. }
We have made a couple key design decisions here. First we decided that the DbService and
AzureTableService should be responsible for implementing an Upsert method and thus removed
the burden of adding a server specific method in our client. Second, by having the Save method
use a parameter of type List<IUpsertable> there is no danger of getting any type of object into
the method that does not implement the IUpsertable interface. Finally, the Save method no
longer needs to know anything about the concrete types that are being passed in, it may simply
iterate the list and call Upsert on each object in the list.
By using an abstraction in our Save method we can add any number of servers to the system
and have the client use them without ever having to modify the Client. This discussion has been
limited to class design but we should mentally go through this exercise with each method as
well. In the Client above our Save method requires a List<IUpsertable> parameter to be passed.
In our implementation we are doing nothing to alter the contents of the list so an even better
design would be to define the save method as follows:
By defining the parameter as List<T> we limited the possible consumers of this client to
consumers that had a List of IUpsertable objects. Modifying the parameter to
IEnumerable<IUpsertable> opens up our potential clients to those that have a List, Array or any
other structure that Implements the IEnumerable interface.
Page 23
www.Intertech.com
Client Team A, responsible for creating an application that sends an email reminder as a part of
their process, creates a client that consumes the server and everything works. Now assume the
server team gets a new requirement, from Client Team B, and now they need to support
sending a text message. That’s almost the same as sending an email message so they decide to
be polymorphic and use the IMessage interface. Their new class looks like this: (can you spot
the Liskov Substitution Principle violation?)
Page 24
www.Intertech.com
6. {
7. //Do the real work here
8. return true;
9. }
10. public string Subject
11. {
12. get
13. {
14. throw new NotImplementedException();
15. }
16. set
17. {
18. throw new NotImplementedException();
19. }
20. }
21. }
Client Team B is happy, things work great and Client Team A is happy because now they can
send SMS messages if they want to. Client Team A wants to enhance their system to allow for a
list of addresses to be included as a blind copy so they give the Server team this new
requirement. The server team comes up with the following change to the interface and new
implementations:
Page 25
www.Intertech.com
Client Team A gets the new server and things are great but Client Team B is now broken and
must recompile and go through a round of regression testing because a feature was added for
another team.
The main goal of the Interface Segregation Principle is to loosen the coupling between servers
and their clients. An SMS message supports a list of addresses, a message body, and the notion
of sending but Client B was unnecessarily also coupled to the Subject and eventually the
BccAddressList property. In short, a change to any unused property or method or a change to
an overly broad interface itself will cause Team B problems. Let’s rework things a little bit and
see if we can improve the design.
Page 26
www.Intertech.com
With this design we have a nice crisp IMessage definition that will serve Client B very well in that
the client does not depend on any methods it does not use. We have created a “wider”
interface Called IEmailMessage that inherits from IMessage for Team A to use so Team A can
send Email or an SMS text message.
If we want to remove the requirement that a Client understand it needs to set the address list
and message body before calling the send method we could also do this:
Page 27
www.Intertech.com
Think of an interface as reasons for a client to change. The larger the interface, the more likely
it is that you will need to change clients in the future. An interesting but positive side effect is
that the smaller interface also “lowers the bar” for new servers that wish to implement the
interface. When the next great message medium comes along the new server only needs to
implement a very small IMessage interface to work with clients already using other IMessage
servers. In summary, favor many client-specific interfaces over one larger general purpose
interface.
Page 28
www.Intertech.com
Page 29
www.Intertech.com
Now if we apply the Single Responsibility Principle to our design we recognize that we should
not be mixing the Send method with the IMessage interface. We might come up with
something like this:
Page 30
www.Intertech.com
17.
18. public class SmtpMessage : IEmailMessage
19. {
20. public IList<String> ToAddresses { get; set; }
21. public IList<String> BccAddresses { get; set; }
22. public string MessageBody { get; set; }
23. public string Subject { get; set; }
24. }
25.
26. public class SmsMessage : IMessage
27. {
28. public IList<String> ToAddresses { get; set; }
29. public string MessageBody { get; set; }
30. }
31.
32. public class SmsMessageServer:IMessageServer
33. {
34. public bool Send(IMessage message)
35. {
36. //Do the real work here
37. return true;
38. }
39. }
40.
41. public class SmtpMessageServer : IMessageServer
42. {
43. public bool Send(IMessage message)
44. {
45. //Do the real work here
46. return true;
47. }
48. }
With this design, classes implementing the IMessageServer interface only need to concern
themselves with transporting the message and classes implementing the IMessage interface
only need to concern themselves with building a message. See how we tie these 2 things
together in the next section.
Page 31
www.Intertech.com
When the Dependency Inversion Principle is applied this relationship is reversed. The
presentation layer defines the abstractions it needs to interact with an application layer. The
application layer defines the abstractions it needs from a business layer and the business layer
defines the abstractions it needs from a data access layer. That is a key departure from the more
classic approach, the higher layer defines the abstractions it needs to do its job and the lower
layers implement those abstractions.
A strict application of the principle may even put the abstractions in the layer defining them, for
example the presentation layer contains the presentation logic and application layer
abstractions (abstract classes or Interfaces), and the Application assembly contains application
logic and business layer abstractions and so on. In this application of the principle the Data
access layer depends upon the business layer, the business layer depends upon the application
Page 32
www.Intertech.com
layer and the application layer depends upon the presentation layer. The dependencies
(references) have been inverted thus the name of the principle.
That structure above feels a little bit funky to me so in all of my projects with any complexity I
usually end up with something like the following image. Presentation, Application, Business,
and DataAccess are in different assemblies. The interfaces will be in a different assembly or
assemblies as well.
Page 33
www.Intertech.com
The second part of the principle stating that abstractions should not depend upon details rather
details should depend upon abstractions historically solved a potential problem with C++
whereby header files could contain both the public and private functions and member variables
so in C++ this meant use a pure abstract class. In C# – a “pure” abstract class is an interface or an
abstract class with zero implementation.
What I like about this principle is that if I have followed the principle throughout my code base I
can test each layer in isolation by using Mocks, Fakes, test doubles etc. Here is a super simple
example. My company is in the business of building widgets. As a part of designing the
application I have decided that I need to log information and my first requirement is that I need
to be able to interact with a list of widgets transactionally. I create an Interface assembly and
add the following interfaces:
1. namespace Dip.Interfaces
2. {
3. public interface IWidget
4. {
5. int Length { get; set; }
6. int Width { get; set; }
7. bool DoWork();
8. }
9. }
10.
11.
12. namespace Dip.Interfaces
13. {
14. public interface ILogger
15. {
16.
17. //LoggingConcerns
18. bool LogMessage(string message);
19. bool LogMessage(string message, string callStack);
20.
21. }
22. }
23.
24. using System.Collections.Generic;
25.
26. namespace Dip.Interfaces
27. {
28. public interface ICoordinatingService
29. {
30. void CoordinateTransaction(IList<IWidget> widgets);
31.
32. }
33. }
Page 34
www.Intertech.com
For My data access layer I will only consider logging for brevity. I decide I need to be able to log
to a database or a file so I add a new project and create 2 classes as follows:
1. using Dip.Interfaces;
2.
3. namespace Dip.Storage
4. {
5. public class FileLogger:ILogger
6. {
7. public bool LogMessage(string message)
8. {
9. //write to file
10. return true;
11. }
12.
13. public bool LogMessage(string message, string callStack)
14. {
15. //write to file
16. return true;
17. }
18. }
19. }
20.
21. using Dip.Interfaces;
22.
23. namespace Dip.Storage
24. {
25. public class DbLogger:ILogger
26. {
27.
28. public bool LogMessage(string message)
29. {
30. //write to Database
31. return true;
32. }
33.
34. public bool LogMessage(string message, string callStack)
35. {
36. //write to Database
37. return true;
38. }
39. }
40. }
Page 35
www.Intertech.com
1. using Dip.Interfaces;
2.
3. namespace Dip.Business
4. {
5. public class Widget:IWidget
6. {
7. ILogger log;
8. //xtor injection
9. public Widget(ILogger logger)
10. {
11. log = logger;
12. }
13.
14. public bool DoWork()
15. {
16. //Execute business rules and log an entry
17. log.LogMessage("Hello World");
18. return true;
19. }
20. public int Length { get; set; }
21. public int Width { get; set; }
22. public string OtherStuffNotPersisted { get; set; }
23. }
24. }
Notice that to construct the widget I need to supply an ILogger – this is called constructor
injection. If the entire class doesn’t need to use a logger we could only add the ILogger
parameter to the methods that do need a logger – this is method injection. Again note, the
business does not reference the DataAccess layer directly – it only references the Interfaces
assembly. When I go to test the business layer I can use a mock implementation of ILogger
rather than rely on a database or the file system. This is the power of the dependency inversion
principle.
Continuing on we add our application layer. This layer tends to coordinate interactions between
existing business objects and maybe take care of transactions:
1. using Dip.Interfaces;
2. using System.Collections.Generic;
3.
4. namespace Dip.ApplicationService
5. {
6. public class CoordinatingService : ICoordinatingService
7. {
8. ILogger log;
9. public CoordinatingService(ILogger logger)
10. {
11. log = logger;
12. }
Page 36
www.Intertech.com
13.
14. public void CoordinateTransaction(IList<IWidget> widgets)
15. {
16. //Begin Transaction
17. foreach(var item in widgets)
18. {
19. item.DoWork();
20. }
21. //Commit...
22. }
23.
24. }
25. }
Again, we are using constructor injection to get our ILogger. In fact we are only relying on
IWidget and ILogger so this layer can be tested with mock versions of those objects and thus be
tested in isolation.
Now we move to the UI – I just used an MVC 4 project for this. Add a controller and I am
breaking the dependency inversion principle on purpose here:
1. using Dip.ApplicationService;
2. using Dip.Business;
3. using Dip.Interfaces;
4. using Dip.Storage;
5. using System.Collections.Generic;
6. using System.Web.Mvc;
7.
8. namespace Dip.UI.Controllers
9. {
10. public class WidgetController : Controller
11. {
12. //
13. // GET: /Widget/
14.
15. public ActionResult Index()
16. {
17.
18. //Call the service layer for this
19. var service = new CoordinatingService(new DbLogger());
20. //Normally this would be Extracted from the request
21. service.CoordinateTransaction(new List<IWidget>{
22. new Widget(new DbLogger()){Length=3,Width=4},
23. new Widget(new DbLogger()){Length=5,Width=6}
24. });
25.
26. var model = "success";
27.
28. //Send model to view
29. return View(model);
Page 37
www.Intertech.com
30. }
31.
32. }
33. }
How did I break the rule here? Several ways – I am referencing Application, business, and
the data access assemblies directly and instantiating those items right in the controller.
Why did I do this? Because the dependency Inversion Principle does not tell us how to
resolve dependencies it is used to guide us in a design that allows us to better test our
software in isolation. The above solution will allow us to test the application and business
layers in isolation. If we want to test our data access layer in isolation we would need to
refactor and better use interfaces or we would have to rely on the Microsoft Shim
framework to intercept calls to the database and file system and return some predictable
and repeatable results. With this design we have basically said “the UI layer is untestable in
isolation” and that may be good enough or that may be considered a fatal flaw. How might
we change the design of the UI layer to follow the dependency inversion principle? That is a
topic for another series but here is a hint, use a dependency injection container. With this
solution we will add a constructor to the controller that accepts an ILogger and
ICoordinatingService parameter. Additionally we will want to use a creational pattern which
will (using interfaces) expose a method that returns a Widget via the IWidget interface and
add that “factory” interface as a parameter to the constructor of the controller as well.
Page 38
www.Intertech.com
Person is our base class; Teacher and student derive from person. We can say that a student Is
A person and that a Teacher Is A person.
The up-side of this design is it is simple, intuitive, and it will perform well. The down side of this
approach is that there is a tight coupling between the derived classes and the base class, we are
breaking encapsulation by making the functionality of the derived class dependent on the
functionality in the base class, and we may even break encapsulation on our Person class by
using protected or public access modifiers where we normally wouldn’t do so.
The problem with breaking encapsulation is that making a change to one class (our base class)
can cause unintended changes in other consuming classes (our derived classes). I can’t begin to
tell you how many times I have seen a seemingly small change to a base class break huge pieces
of an application.
Page 39
www.Intertech.com
Now let’s consider using delegation rather than inheritance. We will change our class diagram
to look like the following:
We can say a teacher Has A person and similarly student Has A person.
The down-side of this design is that we have to write more code to create and manage the
person class and thus the code will not perform as well (though in most cases I suspect the
performance hit is negligible). We also cannot use polymorphism - we cannot treat a student as
a person and we cannot treat a teacher as a person. The up-side of this design is that we can
decrease coupling by defining a person interface and using the interface as the return type for
our Person property rather than the concrete Person class, and our design is more robust.
When we use delegation we can have a single instance of a person act as both a student and a
teacher. Try that in C# using the inheritance design!
Page 40
www.Intertech.com
Page 41
www.Intertech.com
If we take a little different approach and use composition instead we might have a class diagram
similar to the following:
The CompositionDatabaseWriter does not need to implement IDatabaseWriter but I did that
because I think it makes the example easier to understand. The application will use the
CompositionDatabaseWriter to do database work and CompositionDatabaseWriter will
determine whether to use SqlServerDatabaseWriter or OracleDatabaseWriter at runtime
perhaps by using a configuration file entry. When one of the CompositionDatabaseWriter
methods is called, CompositionDatabaseWriter simply calls the corresponding method on the
Sql Server or Oracle object.
Both designs allow us to interact with an oracle or Sql Server database which was our goal. Now
here is where the flexibility of composition comes in; suppose we now need to support a MySql
database. If we use inheritance to solve this problem, at a minimum we must create a
MySqlDatabaseWriter subclass, recompile, test, and deploy our data layer. If we use
composition instead we can create a MySqlDatabaseWriter in a new assembly and all we have
to do to use it is deploy this single assembly and change a config file. Very powerful, because
we used composition we can change the behavior at runtime via configuration.
Page 42
www.Intertech.com
The fact of the matter is that our sample is so simple we didn’t even use composition at all so
imagine the considerably more complex scenario where what we are doing is creating a
persistence object that must transactionally work with the file system and a database. We
define the persistence object and within the persistence object we delegate the responsibility of
dealing with the file system to one class, and dealing with the database to another. The
persistence object is composed of a FileSytemWriter and a DatabaseWriter. Our Persistence
object has only one concern – coordinating database and file persistence. Our FileSystemWriter
has only one concern – managing File System interactions, and finally our DatabaseWriter is only
concerned about interacting with a database of one type. We have decomposed a fairly
complex problem into a few smaller more managable problems, we came up with a nice robust
design thanks to composition and we have managed to do it in such a way that there are no SRP
violations either!
Page 43
www.Intertech.com
Page 44
www.Intertech.com
If all of those (mostly financial) arguments aren’t enough to convince you, let’s take a look at
some down sides from an architect or developer’s point of view.
If code is in a release you have to assume it is being used. When it comes time to change the
code you think you needed, you are going to spend time figuring out how to change it without
breaking the (imagined?) user base. This might involve migrating data, configurations, backup
procedures, reports, integration work flows etc. When it comes time to change the code you
really do need you must consider all of the code. This includes the code you don’t realize that
you don’t need – if it is in production, how can you be confident in saying “oh, we really don’t
need that code”. This really stifles our ability to modernize our code. Even if you do find a way
to refactor the bloated code base not only will you have to change the existing code but you will
have to change the tests, documentation, and training materials.
Let the business analysts, user base, and marketing folks decide what features your product
needs and when it needs it. I would rather architect and design a system based on things that
are known. I don’t ever want to tell somebody that the system is the way it is because I thought
we needed some functionality that turns out to be useless. When we do a good job of designing
and reviewing our system we can be confident that we can refactor our code to implement
major functional changes when they are understood, needed, and the highest priority.
Page 45
www.Intertech.com
Consulting
Need help with a project? Intertech provides consulting on all the popular technologies
like .NET and Agile. Speak with one of our consultants today and let us help you.
Intertech’s Agile Consulting Services
Intertech’s .NET Consulting Services
Training
Interested in learning more about Agile or C#? Take a look at Interech’s multitude of
training courses:
Interech’s Agile Training Courses
Intertech’s C# Training Courses
Intertech’s Training Schedule
Contact Information
Intertech, Inc
1575 Thomas Center Dr, Eagan, MN 55122
800.866.9884
651-288-7000
Page 46