Code reusability is one of the most important topics in software architecture. This chapter aims to discuss ways to enable code reuse, as well as to help you understand how .NET 6 solves the problem of managing and maintaining a reusable library.
The following topics will be covered in this chapter:
Although code reuse is an exceptional practice, as a software architect you must be aware when this is important for the scenario you are dealing with. Many good software architects agree that there is a lot of overengineering due to trying to make things reusable even though they are often single-use or not understood well enough.
This chapter requires the following things:
There is a single reason that you can always use to justify code reuse – you cannot spend your valuable time reinventing the wheel if it is already running well in other scenarios. That is why most engineering domains are based on reusability principles. Think about the light switches you have in your house.
Can you imagine the number of applications that can be made with the same interface components? The fundamentals of code reuse are the same. Again, it is a matter of planning a good solution so part of it can be reused later.
In software engineering, code reuse is one of the techniques that can bring a software project a bunch of advantages, such as the following:
These aspects indicate that code reuse should be done whenever it is possible. It is your responsibility, as a software architect, to ensure the preceding advantages are utilized and, more than that, that you incentivize your team to enable reuse in the software they are creating.
The first thing you must understand is that code reuse does not mean copying and pasting code from one class to another. Even if this code was written by another team or project, this does not indicate that you are properly working with reusability principles. Let us imagine a scenario that we will find in this book’s use case, the WWTravelClub evaluation.
In this project scenario, you may want to evaluate different kinds of subjects, such as the Package, DestinationExpert, City, Comments, and so on. The process for getting the evaluation average is the same, no matter which subject you are referring to. Due to this, you may want to enable reuse by copying and pasting the code for each evaluation. The (bad) result will be something like this:
Figure 12.1: Bad implementation – there is no code reuse here
In the preceding diagram, the process of calculating the evaluation average is decentralized, which means that the same code will be duplicated in different classes. This will cause a lot of trouble, especially if the same approach is used in other applications. For instance, if there is a new specification about how you must calculate the average or if you just get a bug in the calculation formula, you will have to fix it in all instances of code. If you do not remember to update it in all places, you will possibly end up with an inconsistent implementation.
The solution to the problem mentioned in the previous section is quite simple: you must analyze your code and select the parts of it that would be good to decouple from your application.
The greatest reason why you should decouple them is related to how you are sure that this code can be reused in other parts of the application, or even in another application:
Figure 12.2: An implementation focused on code reuse
The centralization of the code brings with it a different responsibility for software architects, such as yourself. You will have to keep in mind that a bug or incompatibility in this code could cause damage to many parts of the application or different applications. On the other hand, once you have this code tested and running, you will be able to propagate its usage with no worries. Besides, if you need to evolve the average calculation process, you will have to change the code in a single class.
It is worth mentioning that the more you use the same code, the cheaper this development will become. Cost needs to be mentioned because, in general, the conception of reusable software costs more in the beginning.
If you understood that reusability will take you to another level of code implementation, you should have been thinking about how to make this technique available in your development life cycle.
As a matter of fact, creating and maintaining a component library is not very easy, due to the responsibility you will have and the lack of good tools to support the search for existing components.
On the other hand, there are some things that you may consider implementing in your software development process every time you initiate a new development:
The use-identify-modify-design-build process is a technique that you may consider implementing every time you need to enable software reuse. As soon as you have the components you need to write for this library, you will need to decide on the technology that will provide these components.
During the history of software development, there have been many approaches to enable code reuse. From dynamic link libraries (DLLs) to microservices – as we discussed in Chapter 5, Applying a Microservice Architecture to Your Enterprise Application, in the Microservices and the evolution of the concept of modules section. The methodology explained in the section can be used by you, as a software architect, to implement this strategy to accelerate software development. Now let us check how .NET 6 can help us with it.
.NET has evolved a lot since its first version. This evolution is not only related to the number of commands and performance issues but the supported platforms too. As we discussed in Chapter 1, Understanding the Importance of Software Architecture, you can run C# .NET on billions of devices, even if they are running Linux, Android, macOS, or iOS. For this reason, .NET Standard was first announced together with .NET Core 1.0, but .NET Standard became particularly important with .NET Standard 2.0, when .NET Framework 4.7.2, .NET Core, and Xamarin were compatible with it.
The key point is that .NET Standard was not only a kind of Visual Studio project. More than that, it was a formal specification available to all .NET implementations. As you can see in the following table, it covers everything from .NET Framework to Unity:
.NET Standard |
1.0 |
1.1 |
1.2 |
1.3 |
1.4 |
1.5 |
1.6 |
2.0 |
2.1 |
.NET Core and .NET 5 |
1.0 |
1.0 |
1.0 |
1.0 |
1.0 |
1.0 |
1.0 |
2.0 |
3.0 |
.NET Framework |
4.5 |
4.5 |
4.5.1 |
4.6 |
4.6.1 |
4.6.1 |
4.6.1 |
4.6.1 |
N/A |
You can find a full .NET Standard overview at https://docs.microsoft.com/en-us/dotnet/standard/net-standard.
The preceding table indicates that if you build a class library that is compatible with this standard, you will be able to reuse it in any of the platforms presented. Think about how fast your development process could become if you plan to do this in all your projects.
Obviously, some components are not included in .NET Standard, but its evolution is continuous. It is worth mentioning that Microsoft’s official documentation indicates that the higher the version, the more APIs are available to you.
The initiative of having a single framework for all platforms brought us to .NET 5. Microsoft indicated that from .NET 5.0, the framework would run everywhere. The next question you, as a software architect, might have is: what is going to happen to .NET Standard?
The answer to this question is well explained by Immo Landwerth at the dotnet blog: https://devblogs.microsoft.com/dotnet/the-future-of-net-standard/. The basic answer is that .NET 5.0 (and future versions) needs to be thought of as the foundation for sharing code moving forward. Considering .NET 6 is an LTS version, now we can understand the framework as the best option to share code for new applications.
It is quite simple to create a class library. Basically, you need to choose the following project when creating the library:
Figure 12.3: Creating a class library
Once you have concluded this part, you will notice that the project file keeps the information about the target framework moniker (TFM). The idea of the TFM is to define the set of APIs that will be available to the library. You can find the list of the available TFMs at https://docs.microsoft.com/en-us/dotnet/standard/frameworks.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
As soon as your project is loaded, you can start coding the classes that you intend to reuse. The advantage of building reusable classes using this approach is that you will be able to reuse the written code in all the project types we checked previously. On the other hand, you will find out that some APIs that are available in .NET Framework do not exist in this type of project.
There are many approaches where C# helps us deal with code reuse. The ability to build libraries, as we did in the previous section, is one of them. One of the most important ones is the fact that the language is object-oriented. Besides, it is worth mentioning the facilities that generics brought to the C# language. This section will discuss the last two we mentioned.
The object-oriented analysis approach gives us the ability to reuse code in different ways, from the facility of inheritance to the changeability of polymorphism. Complete adoption of object-oriented programming will let you implement abstraction and encapsulation too.
It is important to mention that in Chapter 20, Best Practices in Coding C# 10, we discuss how inheritance can cause complexity in your code. Although the example below presents a valid way to reuse code, consider using composition over inheritance in real-life applications.
The following diagram shows how using the object-oriented approach makes reuse easier. As you can see, there are different ways to calculate the grades of an evaluation, considering you can be a basic or a prime user of the system:
Figure 12.4: Object-oriented case analysis
There are two aspects to be analyzed as code reuse in this design. The first is that there is no need to declare the properties in each child class since inheritance is doing it for you.
The second is the opportunity to use polymorphism, enabling different behaviors for the same method:
public class PrimeUsersEvaluation : Evaluation
{
/// <summary>
/// The business rule implemented here indicates that grades that
/// came from prime users have 20% of increase
/// </summary>
/// <returns>the final grade from a prime user</returns>
public override double CalculateGrade()
{
return Grade * 1.2;
}
}
In the preceding code, you can see the usage of the polymorphism principle, where the calculation of evaluation for prime users will increase by 20%. Now, look at how easy it is to call different objects inherited by the same class. Since the collection content implements the same interface, IContentEvaluated
, it can have basic and prime users too:
public class EvaluationService
{
public IContentEvaluated Content { get; set; }
/// <summary>
/// No matter the Evaluation, the calculation will always get
/// values from the method CalculateGrade
/// </summary>
/// <returns>The average of the grade from Evaluations</returns>
public double CalculateEvaluationAverage()
{
return Content.Evaluations
.Select(x => x.CalculateGrade())
.Average();
}
}
Object-oriented adoption can be considered mandatory when using C#. However, more specific usage will need study and practice. You, as a software architect, should always incentivize your team to study object-oriented analysis. The more abstraction abilities they have, the easier code reuse will become.
Generics were introduced in C# in version 2.0, and it is considered an approach that increases code reuse. It also maximizes type safety and performance.
The basic principle of generics is that you can define in an interface, class, method, property, event, or even a delegate, a placeholder that will be replaced with a specific type later when one of the preceding entities is used. The opportunity you have with this feature is incredible since you can use the same code to run different versions of the type, generically.
The following code is a modification of EvaluationService
, which was presented in the previous section. The idea here is to enable the generalization of the service, giving us the opportunity to define the goal of evaluation since its creation:
public class EvaluationService<T> where T: IContentEvaluated, new()
This declaration indicates that any class that implements the IContentEvaluated
interface can be used for this service. The new constraint indicates this class must have a public parameter-less default constructor.
Besides, the service will be responsible for creating the evaluated content.
public EvaluationService()
{
var name = GetTypeOfEvaluation();
content = new T();
}
It is worth mentioning that this code will work because all the classes are in the same assembly. The result of this modification can be checked in the instance creation of the service:
var service = new EvaluationService<CityEvaluation>();
The good news is that, now, you have a generic service that will automatically instantiate the list object with the evaluations of the content you need. It’s worth mentioning that generics will obviously need more time dedicated to the first project’s construction. However, once the design is done, you will have good, fast, and easy-to-maintain code. This is what we call reuse!
In fact, any code can be reusable. The key point here is if the code you intend to reuse is well-written and follows good patterns for reuse. There are several reasons why code should be considered not ready for reuse:
In any of these cases, considering a refactoring strategy can be a great approach. When you are refactoring code, you are writing it in a better way while respecting the input and output data that this code will process. This enables more comprehensive and lower-cost code when it comes to changing it. Martin Fowler indicates some reasons why we should consider refactoring:
The process of refactoring depends on some steps that we shall follow to guarantee good results and minimize errors during the journey:
As a software architect, you will receive many refactoring demands from your team. The incentive for doing so must be continuous. But you must remind your team that refactoring without following the preceding steps might be risky. So, it is your responsibility to make it happen in a way that can both enable fast programming and less impact, thus delivering real business value.
Considering you have made all the necessary effort to guarantee you have good libraries that can be reused in many of your projects, you will find another difficult situation arises when enabling reusability: it is not simple to let programmers know you have libraries ready to reuse.
There are some simple approaches to documenting a library. As we mentioned when we talked about the development life cycle, documenting is a good way to help developers take notice of the libraries they have. There are two examples of documenting reusable code that we would like to mention here.
This tool is a good alternative for documenting a library using comments made in its code. By simply adding the NuGet package docfx.console
, the tool allows you to create a task that will run once your library has been built:
Figure 12.5: docfx.console NuGet library
The output of this compilation is a stylish static website that contains the documentation of your code:
Figure 12.6: DocFx result
This website is useful because you can distribute the documentation to your team so that they can search for the libraries you have. You can check the customizations of the output and find more information about it at https://dotnet.github.io/docfx/.
There is no doubt that a Web API is one of the technologies that facilitates and promotes code reuse. For this reason, having its documentation well done and, more than that, respecting a standard is good practice and indicates that you are up to date on this approach. To do this, we have Swagger, which respects the OpenAPI Specification.
The OpenAPI Specification is known as the standard for describing modern APIs. One of the most widely used tools for documenting it in an ASP.NET Core Web API is Swashbuckle.AspNetCore
.
The good thing about using the Swashbuckle.AspNetCore
library is you can set the Swagger UI viewer for your Web API, which is a good, graphical way to distribute the APIs.
We will learn how to use this library in ASP.NET Core Web APIs in the next chapter. Until then, it is important to understand that this documentation will help not only your team but any developer who might use the APIs you are developing.
The final design of the solution for evaluating content for WWTravelClub can be checked as follows. This approach consists of using many topics that were discussed in this chapter. First, all the code is placed in a .NET 6 class library.
This means that you can add this code to different types of solutions, such as ASP.NET Core web apps and Xamarin apps for the Android and iOS platforms:
Figure 12.7: WWTravelClub reuse approach
This design makes use of object-oriented principles such as inheritance, so you do not need to write properties and methods more than once that can be used in many classes; and polymorphism, so that you can change the behavior of the code without changing the name of the method.
To finish, the design abstracts the idea of the content by introducing generics as a tool that can facilitate the manipulation of similar classes, such as the ones we have in WWTravelClub to evaluate content regarding cities, comments, destination experts, and travel packages.
The big difference between a team that incentivizes code reuse and one that does not is the velocity of delivering good software to end users. Of course, beginning this approach is not easy, but rest assured that you will get good results after some time working with it.
This chapter aimed to help you understand the advantages of code reuse. It also gave you an idea about what is not properly reused code. This chapter also presented approaches for reusing and refactoring code.
Considering that technology without processes does not take you anywhere, a process was presented that helps enable code reuse. This process is related to using already completed components from your library; identifying features in the software requirements specification that are candidates to be designed as library components; modifying the specification considering these features; designing the reusable components; and building the project architecture with the new component library version.
To finish, this chapter presented .NET Standard libraries as an approach to reusing code for different C# platforms, indicating that .NET 6 and new versions shall be used for reusing code in different platforms. This chapter also reinforced the principles of object-oriented programming when reusing code and presented generics as a sophisticated implementation to simplify the treatment of objects with the same characteristics. In the next chapter, we will learn how to apply a service-oriented architecture (SOA) with .NET.
It is worth mentioning that SOA is considered a way to implement code reuse in sophisticated environments.
These are some books and websites where you will find more information about the topics covered in this chapter:
The code bundle for the book is hosted on GitHub at https://github.com/PacktPublishing/Software-Architecture-with-C-10-and-.NET-6-3E.
Join the book’s Discord workspace for a Ask me Anything session with the authors:
https://packt.link/SAcsharp10dotnet6
3.129.211.87