Software Design Principles

Software design principles

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

  1. 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).

  2. Client-Server: The client and server are separate entities. Clients request resources, and servers process these requests and return the appropriate resources or responses.

  3. Uniform Interface: A consistent way to interact with resources. REST APIs should have predictable, uniform operations (e.g., GET, POST, PUT, DELETE).

  4. 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).

  5. Representations: Resources can be represented in different formats (e.g., JSON, XML), depending on what the client requests.

  6. Cacheable: Responses should define whether the data can be cached to improve performance.

  7. 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:

  1. Red: Write a test for a specific feature or function that fails (because the functionality doesn’t exist yet).
  2. Green: Write the minimal amount of code needed to pass the test.
  3. Refactor: Refactor the code to improve its design or performance without changing the behavior, ensuring all tests still pass.

Explanation:

  1. Class Definition (NumberConverter):

    • The ConvertNumberToString method is encapsulated inside the NumberConverter class.
  2. 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) 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:

  1. 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.

  2. Service Registration: You register dependencies (services) with the IoC container (usually in the Program.cs file in .NET 8).

  3. 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

  1. Register Services in the IoC container.
  2. 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:

  1. Register your services in Program.cs.
  2. Inject those services into your classes using constructor injection.

This setup promotes flexibility, testability, and a clean separation of concerns.