A SOLI[D] refactoring exercise – Part 1

I initially planned to write about the SOLID principles in isolation, taking them one by one, defining them, and then coming up with a simple fit for purpose scenario complemented by some nice diagrams. But I soon I realised that almost all my knowledge on the subject, either coming from books or the internet, is based on the same type of articles.

Therefore, in an attempt to make things a bit more interesting, my approach is to have a single piece of code based on a scenario (as close as possible to real life) and work through refactoring this code using the SOLID principles. By doing this I’m hoping that this article will provide a bit more insight on how to spot and handle the violations of these principles, and also serve as good refactoring practice. And I promise, this is a guaranteed rectangle/square/shape -free post about SOLID.

The scenario I had in mind is about an on-line e-book provider that universities use as a SaaS to allow their students to borrow e-books on-line. The users of this application (the students) can log-in with the credentials provided by their university and download password protected e-books to read offline. Each e-book can be read for a limited time period after which the password expires and there is also a monthly limit of e-books one student can download. This monthly allowance is based on the package purchased by the university the student belongs to.

The refactoring exercise will be based around the StudentService and Student classes:

public class StudentService
{
	public bool Add(string emailAddress, Guid universityId)
	{		
		Console.WriteLine(string.Format("Log: Start add student with email '{0}'", emailAddress));

		if (string.IsNullOrWhiteSpace(emailAddress))
		{
			return false;
		}

		var studentRepository = new StudentRepository();

		if (studentRepository.Exists(emailAddress))
		{
			return false;
		}

		var universityRepository = new UniversityRepository();

		var university = universityRepository.GetById(universityId);

		var student = new Student(emailAddress, universityId);
		
		if (university.Package == Package.Standard)
		{
			student.MonthlyEbookAllowance = 10;
		}
		else if (university.Package == Package.Premium)
		{
			student.MonthlyEbookAllowance = 10 * 2;
		}							
		
		studentRepository.Add(student);

		Console.WriteLine(string.Format("Log: End add student with email '{0}'", emailAddress));

		return true;
	}
	
	public IEnumerable<Student> GetStudentsByUniversity()
	{
		//...		
	}

	public IEnumerable<Student> GetStudentsByCurrentlyBorrowedEbooks()
	{
		//...		
	}
}
public class Student
{
	public string EmailAddress { get; private set; }
	public Guid UniversityId { get; private set; }
	public int MonthlyEbookAllowance { get; set; }
	public int CurrentlyBorrowedEbooks { get; private set; }

	public Student(string emailAddress, Guid universityId)
	{
		this.EmailAddress = emailAddress;
		this.UniversityId = universityId;
	}		
}

And here’s the initial project structure.

RefactoringExercise
 
 
 
 
 
 
 
 
 
 

As you probably noticed, the Add method from the StudentService class is responsible with adding a new Student to the database. We will start refactoring with this method.

Pass #1 – Dependency Inversion Principle (DIP)

Although there might be a temptation to apply these principles in the order suggested by the acronym, don’t forget that it’s just an acronym and the order of the letters is not a natural order of the how the principles should be applied.

When refactoring I always prefer to start with the most obvious things which can have the immediate effect of clearing-up the code. In this context for me that’s the D. This principle is defined by two rules:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend upon details. Details should depend upon abstractions.

The first rule is easier to understand. The high level modules are the ones that contain the core (use-cases and the the business logic) of the application. Low level modules are modules that perform very specific operations (often infrastructure related) which are orchestrated by the higher level modules.

Looking at our example, it’s obvious that the StudentService is part of a high level module and it depends on the StudentRepository which persists students to the database and belongs to a low level module. It’s also clear that neither of them depend on abstractions.

In order to satisfy the first rule it’s enough to do something a lot of .NET developers are already familiar with. We put both classes behind interfaces (abstractions) and inject the lower level abstraction, the new IStudentRepository, into the StudentService constructor. This is Dependency Injection but it’s enough to satisfy the DIP.

Here’s how the code looks after applying the first rule:

public class StudentService : IStudentService
{
	private readonly IStudentRepository _studentRepository;
	private readonly IUniversityRepository _universityRepository;

	public StudentService(IStudentRepository studentRepository, IUniversityRepository universityRepository)
	{
		_studentRepository = studentRepository;
		_universityRepository = universityRepository;
	}

	public bool Add(string emailAddress, Guid universityId)
	{		
		Console.WriteLine(string.Format("Log: Start add student with email '{0}'", emailAddress));

		if (string.IsNullOrWhiteSpace(emailAddress))
		{
			return false;
		}			

		if (_studentRepository.Exists(emailAddress))
		{
			return false;
		}			

		var university = _universityRepository.GetById(universityId);

		var student = new Student(emailAddress, universityId);
				
		if (university.Package == Package.Standard)
		{
			student.MonthlyEbookAllowance = 10;
		}
		else if (university.Package == Package.Premium)
		{
			student.MonthlyEbookAllowance = 10 * 2;
		}							
		
		_studentRepository.Add(student);
		
		Console.WriteLine(string.Format("Log: End add student with email '{0}'", emailAddress));

		return true;
	}
	
	public IEnumerable<Student> GetStudentsByUniversity()
	{		
		//...
	}

	public IEnumerable<Student> GetStudentsByCurrentlyBorrowedEbooks()
	{		
		//...
	}
}

But what about the second rule?

The second rule is not as easy to understand because it doesn’t have a direct reflection in code. It’s more about perspective and who triggers the change in the abstraction-details relationship. In our example, the IStudentRepository is the abstraction of the StudentRepository detail. In order to satisfy this rule any change in the StudentRepository should be triggered by a change in the IStudentRepository.

To achieve this it helps to think that the IStudentRepository abstraction is in the same higher level module as StudentService. Practically, this would translate to placing IStudentRepository in the same project (higher level module) as the UserService, while StudentRepository will be placed in a separate project (lower level module):

RefactoringExercise_DIP

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

No Comments

Post a Comment