How to Close Your Code
Check out this tutorial on closing your code while making sure that you are conforming with SOLID principles.
Join the DZone community and get the full member experience.
Join For FreeWe all know about SOLID principles. Every developer knows what are SOLID principles because every developer has to go through an interview process to get a job. Some can explain really well while they're interviewing
Recently, I was interviewed by a very reputed financial company. They have 4 rounds of interview and of those, 3 rounds asked a question about SOLID principles. They all asked very good interesting questions and it seems they were looking for some specific expertise. I started working there and their code base is awesome. They are not working anywhere near what they asked in an interview. I also found some controller code files has 2-3k lines of code, which is a presentation layer. Though they have very nice onion architecture for the project, I hope whoever worked there all went through that interview process and knows at least a little about SOLID principles. These principles are not just to perform in the interview; we need to apply it in real code. Maybe they have their own reasons for not following them.
Anyway, here I wanted to show some examples which I saw in their code base which can very easily make it closed for modification.
Check following code:
public void Work(string switchoption)
{
switch (switchoption)
{
case "Fun1":
///
/// So many lines of code
///
break;
case "Fun2":
///
/// So many lines of code
///
break;
case "Fun3":
///
/// So many lines of code
///
break;
}
}
If you come across this kind of code (or you end up writing this kind of code), then what can be done? The above code is not closed because you don't know what kind of case may have to be added in future. What if there is a change in code for a particular switch case? You have to test all cases because you have changed the function. If your unit test covered all switch cases then you are in a better position. QA has to check every case manually. If something is missed in testing then you may end up with production bugs, which increases the overall maintenance cost.
I do refactor it to the following levels.
At least have a private function for long lines of code for each case.
public void Work(string switchoption)
{
switch (switchoption)
{
case "Fun1":
function1();
break;
case "Fun2":
function2();
break;
case "Fun3":
function3();
break;
}
}
private void function3()
{
Console.WriteLine("Function 3");
}
private void function2()
{
Console.WriteLine("Function 2");
}
private void function1()
{
Console.WriteLine("Function 1");
}
If you create private function then the code will at least increase readability. By giving a proper name to function, it describes that what that piece of code is doing. But still, it has an issue of what if we may need to write more cases there. This code is still not a closed code.
Instead of Switch case, you can implement Dictionary
public void Work2(string switchoption)
{
Dictionary<string, Action> switchfunction = new Dictionary<string, Action>();
switchfunction.Add("Fun1", function1);
switchfunction.Add("Fun2", function2);
switchfunction.Add("Fun3", function3);
switchfunction[switchoption].Invoke();
}
You can simply add a dictionary with a string and Action type. Add all functions to dictionary collection. Dictionary can pick a particular case by Key name and execute the function by invoking it. You can also generate this dictionary list from outside of this function or get dictionary values from any data store. In
This implementation has resolved our problem of closing the function Work
for modification. But we still have to add a new function to the class for adding new cases. It is also not following Single responsibility principle.
Implement Strategy Pattern Using Dependency Injection
Basic strategy pattern implementation explained here.
The basic concept of pattern defines an interface with strategy and have the concrete classes with different strategies implementation. Based on your need you can create an object of particular strategy dynamically. Google "strategy design pattern" and you will get plenty of examples.
Here I wanted to show an example using a dependency framework. In above-given URL of dofactory example of strategy pattern, has context class to create an object of different strategies. We can rely on dependency injection framework for that responsibility.
In the following code, IExampleStrategy
is an interface and has functionexample()
. This interface implemented in three different ExampleStrategy
classes. each class has different implementation of functionexample
.
public interface IExampleStrategy
{
void functionexample();
}
public class ExampleStrategy1 : IExampleStrategy
{
public void functionexample()
{
Console.WriteLine("ExampleStrategy1.functionexample() executed.");
}
}
public class ExampleStrategy2 : IExampleStrategy
{
public void functionexample()
{
Console.WriteLine("ExampleStrategy2.functionexample() executed.");
}
}
public class ExampleStrategy3 : IExampleStrategy
{
public void functionexample()
{
Console.WriteLine("ExampleStrategy3.functionexample() executed.");
}
}
Next is the context class where we instantiate the class based on strategy. In the following class, we have injected IUnityContainer
. It resolves right Strategy
class based on a parameter.
The Work
function is the function that we are changing to our main problem of removing switch cases. The first Work
function can pass parameter and decide what strategy need to execute. The container can get a right concrete class based on a parameter passed. Instantiating an object is a responsibility of container based on a parameter.
Another Work
function also does the same thing but in some cases, the developer has to do things in a loop. The container can take care of creating an object and also can take care of the lifetime of your object. So in case you have to create the same object multiple times for our second work function then a container can reuse the same object created previously.
public class CloseCodeExample2
{
private readonly IUnityContainer _container;
public CloseCodeExample2(IUnityContainer container)
{
_container = container;
}
public void Work(string switchcall)
{
var examplefunction= _container.Resolve<IExampleStrategy>(switchcall);
examplefunction.functionexample();
}
public void Work(List<string> switchcalls)
{
foreach (var item in switchcalls)
{
var examplefunction = _container.Resolve<IExampleStrategy>(item);
examplefunction.functionexample();
}
}
}
Here is our main function where we have defined our Unity container registration for an interface and its implementation.
class Program
{
static void Main(string[] args)
{
CloseCodeExample codeExample = new CloseCodeExample();
codeExample.Work("Fun1");
codeExample.Work2("Fun2");
codeExample.Work2("Fun3");
//Unity container registration.
var container = new UnityContainer();
container.RegisterType<IExampleStrategy, ExampleStrategy1>("Fun1");
container.RegisterType<IExampleStrategy, ExampleStrategy2>("Fun2");
container.RegisterType<IExampleStrategy, ExampleStrategy3>("Fun3");
CloseCodeExample2 codeExample2 = new CloseCodeExample2(container);
codeExample2.Work("Fun1");
codeExample2.Work("Fun2");
Console.WriteLine("---------------------------------");
codeExample2.Work(new List<string>() { "Fun2", "Fun3", "Fun1" });
Console.ReadKey();
}
}
Let's validate our two SOLID principles that we wanted to implement. In the above code, if you want to add a new case then you need to add another class which implements the same interface and just needs to register for the container. We don't need to change function Work
; it is basically closed for modification. Also, by creating a separate class for each case, we have implemented the single responsibility principle. If you implement unit tests then it would be a separate unit test for all classes. It's easy to implement the unit test.
Note: In above example, you still have to consider little bit defensive code; for example, what if no any class implementation available to unity config. It will throw an exception of "Unity.Exceptions.ResolutionFailedException". So you still have to take care of this kind of things.
I hope you like it. If you have any questions, add it to comments.
You can find code sample of above code here at GitHub.
Opinions expressed by DZone contributors are their own.
Comments