Software design principles
DRY
SOLID
RESTful API
TDD
Testing
DI-Dependency Injection
DRY principle
Dont Repeat Yourself
SOLID principles
Single Responsibility - Not too much functionality, there should be only one reason for change.
Open close principle - Open for extension closed for modification, in practice this shoud be implemented by designing the interface to the class such that extensions can be implemented via polymorphism and abstraction, think of the Shape class if you make the Area() method abstract then every new Shape can define its own Area() function rather than having to modify a single Area() function for each new Shape that you require.
Liskov substitution principle - Subtype objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. We can use the same Area() function of the Shape class above, If Area is defined as being Lenght squared then when we introduce a Rectangle class the Area function is wrong, instead we should refactor the design to use interfaces so that we Define an IShape interface that has an Area() function, then simply create a new Shape that implements the IShape interface and thus the new shape implements its own definition of Area().
Interface segragation principle - Clients or Superclasses shouldn't be forced to depend on Methods they dont use or dont make any sense. In these cases move the methods out into seperate interfaces, so that only the required methods are included. Think of the Bird Class that has a Fly() Method, but a new bird, Penguin can not fly, so the Fly() method makes no sense. Seperate out the fly method into a Fly() interface so that Each bird can implement the interface as required
Dependency inversion principle - High level modules should not depend onlow level modules, both should depend upon abstraction. So for example a class should not be instantiating other classes, rather instead use Dependency Injection to inject an intance of the required class, typically but not limited to, via the constructor.
RESTful or REST services REpresentational State Transfer
REST is an architectural style used to design networked applications. It leverages HTTP methods to interact with resources (like data) using standardized operations. RESTful APIs are commonly used to build web services.
REST is about using simple, stateless HTTP requests to perform operations on resources, making your system easier to scale, maintain, and evolve. RESTful APIs have become the standard for building web services due to their simplicity and effectiveness
Core Principles of REST
-
Stateless: Each request from a client to a server must contain all the information the server needs to understand and process the request. No client context is stored on the server between requests (the server doesn't keep session data).
-
Client-Server: The client and server are separate entities. Clients request resources, and servers process these requests and return the appropriate resources or responses.
-
Uniform Interface: A consistent way to interact with resources. REST APIs should have predictable, uniform operations (e.g., GET
, POST
, PUT
, DELETE
).
-
Resource-Based: Everything is treated as a resource, such as a user, product, or comment. Each resource is uniquely identified by a URI (Uniform Resource Identifier).
-
Representations: Resources can be represented in different formats (e.g., JSON, XML), depending on what the client requests.
-
Cacheable: Responses should define whether the data can be cached to improve performance.
-
Layered System: The client should not be aware of the complexity of the system; intermediaries like load balancers or caches may be introduced between the client and the server.
Common HTTP Methods in REST
- GET: Retrieve a resource.
- POST: Create a new resource.
- PUT: Update an existing resource.
- DELETE: Delete a resource.
- PATCH: Partially update a resource.
TDD - (Test-Driven-Development)
TDD is a software development approach where tests are written before the actual code. It follows a cycle of writing a failing test, writing just enough code to make the test pass, and then refactoring the code to improve its structure while keeping all tests green (passing).
The TDD Cycle
The TDD cycle follows three main steps, often referred to as Red-Green-Refactor:
- Red: Write a test for a specific feature or function that fails (because the functionality doesn’t exist yet).
- Green: Write the minimal amount of code needed to pass the test.
- Refactor: Refactor the code to improve its design or performance without changing the behavior, ensuring all tests still pass.
Testing
If we have the following class, how should we test it, should we be concerned about whether its a unit test or an integration test, and should we fake it?
public class NumberConverter
{
public string ConvertNumberToString(int? number)
{
var numberAsString = number.ToString();
if (String.IsNullOrWhiteSpace(numberAsString))
throw new Exception("oops, number is null");
return numberAsString;
}
}
Heres some test we could create
using System;
using Xunit;
public class NumberConverterTests
{
[Fact]
public void ConvertNumberToString_WithValidNumber_ReturnsNumberAsString()
{
// Arrange
var converter = new NumberConverter();
int? number = 42;
// Act
var result = converter.ConvertNumberToString(number);
// Assert
Assert.Equal("42", result);
}
[Fact]
public void ConvertNumberToString_WithNullNumber_ThrowsException()
{
// Arrange
var converter = new NumberConverter();
int? number = null;
// Act & Assert
var exception = Assert.Throws<Exception>(() => converter.ConvertNumberToString(number));
Assert.Equal("oops, number is null", exception.Message);
}
}
Explanation:
-
Class Definition (NumberConverter
):
- The
ConvertNumberToString
method is encapsulated inside the NumberConverter
class.
-
Tests (NumberConverterTests
):
- The test class is named
NumberConverterTests
and contains two test methods, one for a valid number and another for a null
value.
Now everything is properly defined. The NumberConverter
class is where the actual method resides, and you can use this class in your unit tests.
The difference between Unit and Integration testing:
Unit Testing
- Focus: Tests individual units of code (like methods or functions) in isolation.
- Purpose: Verify that a single method or function behaves as expected given certain inputs.
- No external dependencies: It should not depend on external systems (e.g., databases, web services, etc.). Any required dependencies are mocked or faked.
- Small scope: The test is focused on the smallest testable part of an application.
Integration Testing
- Focus: Tests how different units or components of a system work together.
- Purpose: Verify that the interactions between different modules or external systems (e.g., databases, APIs) work as expected.
- Larger scope: Involves more than just one method, often covering scenarios that span multiple layers or components.
Why these Tests are a Unit Tests:
The ConvertNumberToString
function does not depend on any external components or modules. It just takes an int?
and returns a string based on its value. So, testing this function falls strictly under unit testing because:
- You are testing only this method.
- There are no external integrations involved.
In contrast, if your method were part of a larger system that, say, retrieved the number from a database, passed it through multiple services, or called other methods, that would be integration testing.
Dependency Injection (DI)
Dependency Injection (DI) in .NET 8 is a design pattern that allows the separation of object creation and its dependencies from the actual business logic. DI is built into .NET Core and later versions, including .NET 8, and helps manage the lifecycle and dependencies of objects in your application automatically.
Key Concepts:
-
Inversion of Control (IoC): Instead of creating instances of classes directly (e.g., new SomeClass()
), you ask the framework to provide the necessary dependencies, which ensures flexibility and easier testing.
-
Service Registration: You register dependencies (services) with the IoC container (usually in the Program.cs
file in .NET 8).
-
Service Injection: Once services are registered, the framework injects them into classes that need them, usually via constructor injection.
How to Set Up DI in .NET 8
- Register Services in the IoC container.
- Inject Services into the classes that depend on them.
Example: Setting up DI in .NET 8
Let's walk through a basic example where we have a service that provides some logic (e.g., a logging service) and a controller that uses it.
Step 1: Define an Interface and Implementation
public interface ILoggerService
{
void Log(string message);
}
public class ConsoleLoggerService : ILoggerService
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
Step 2: Register the Service in the IoC Container
In .NET 8, this is done in the Program.cs
file. You specify how the service should be created, for example, using Scoped, Singleton, or Transient lifetimes.
var builder = WebApplication.CreateBuilder(args);
// Register the service
builder.Services.AddScoped<ILoggerService, ConsoleLoggerService>();
var app = builder.Build();
- Scoped: A new instance is created per HTTP request.
- Singleton: A single instance is created and shared throughout the app's lifetime.
- Transient: A new instance is created each time the service is requested.
Step 3: Inject the Service into a Consumer Class
Let’s say you have a controller that needs this logger service:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILoggerService _logger;
// Constructor injection
public WeatherForecastController(ILoggerService logger)
{
_logger = logger;
}
[HttpGet]
public IActionResult Get()
{
_logger.Log("Getting weather forecast.");
return Ok("Weather forecast data");
}
}
Here, ILoggerService
is injected via the constructor of the WeatherForecastController
.
Step 4: Run the Application
When you run the app and make a request to the WeatherForecastController
, the ConsoleLoggerService
is automatically injected into the controller and used to log a message.
Summary:
- Register your services in
Program.cs
.
- Inject those services into your classes using constructor injection.
This setup promotes flexibility, testability, and a clean separation of concerns.