Create Flexible Code Using Interfaces
In object-oriented programming, interfaces define a contract that classes can implement. They specify method signatures and properties that implementing classes must provide. This allows for consistent behavior across different types while enabling flexibility in implementation. In C#, interfaces are defined using the interface keyword, and classes implement them using the : InterfaceName syntax.
In this exercise, you will refactor a tightly coupled console application to use interfaces. By introducing interfaces and dependency injection, you will decouple the application logic from specific implementations, making the code more flexible and easier to maintain.
This exercise takes approximately 20-25 minutes to complete.
Before you start
Before you can start this exercise, you need to:
- Ensure that you have the latest short term support (STS) version of the .NET SDK installed on your computer. You can download the latest versions of the .NET SDK using the following URL: Download .NET.
 - Ensure that you have Visual Studio Code installed on your computer. You can download Visual Studio Code using the following URL: Download Visual Studio Code.
 - Ensure that you have the C# Dev Kit configured in Visual Studio Code.
 
For additional help configuring the Visual Studio Code environment, see Install and configure Visual Studio Code for C# development.
Exercise scenario
Suppose you’re a software developer at a tech company working on a new project. Your team has identified that some parts of the codebase are tightly coupled, making it difficult to test and extend. To address this, you decide to refactor the code to use interfaces and dependency injection. This will decouple the application logic from specific implementations, improving flexibility and maintainability.
This exercise includes the following tasks:
- Create a new C# project.
 - Define interfaces to abstract logging and database access.
 - Update existing classes to implement the interfaces.
 - Refactor the application to use dependency injection.
 - Test the refactored application to ensure it works as expected.
 
Task 1: Review the current version of your logging application.
In this task, you download the existing version of your banking app and review the code.
Use the following steps to complete this section of the exercise:
- 
    
Download the starter code from the following URL: Implement interfaces - exercise code projects

 - 
    
Extract the contents of the LP2SampleApps.zip file to a folder location on your computer.
 - 
    
Expand the LP2SampleApps folder, and then open the
Interface_M3folder.The Interface_M3 folder contains the following code project folders:
- Solution
 - Starter
 
The Starter folder contains the starter project files for this exercise.
 - 
    
Use Visual Studio Code to open the Starter folder.
 - 
    
In the EXPLORER view, collapse the STARTER folder, select SOLUTION EXPLORER, and expand the Interface_M3 project.
You should see the following project files:
- Application.cs
 - ConsoleLogger.cs
 - DatabaseAccess.cs
 - IDataAccess.cs
 - ILogger.cs
 - Program.cs
 
 - 
    
Take a minute to open and review each of the files.
 
- 
    
Application.cs: Defines theApplicationclass, which contains the main application logic and is tightly coupled toConsoleLoggerandDatabaseAccess. - 
    
ConsoleLogger.cs: Defines theConsoleLoggerclass, responsible for logging messages to the console, currently tightly coupled toApplication. - 
    
DatabaseAccess.cs: Defines theDatabaseAccessclass, responsible for connecting to a database and retrieving data, currently tightly coupled toApplication. - 
    
IDataAccess.cs: Defines theIDataAccessinterface, specifying the contract for data access operations likeConnectandGetData. - 
    
ILogger.cs: Defines theILoggerinterface, specifying the contract for logging operations with aLog(string message)method. - 
    
Program.cs: Contains the entry point of the application, creating and running an instance of theApplicationclass.[!TIP] Review of each file in the previous list provides understanding of the current project starting code, which is tightly coupled. Reviewing
TASKcomments in the files provides context for understanding how the project code is refactored into loosely coupled code. 
- 
    
Run the application using the following command (or debug with
F5):dotnet runTo debug, set breakpoints in your code (e.g., in
Application.csorProgram.cs) and pressF5in Visual Studio Code to start debugging. This allows you to step through the code and inspect variables. - 
    
Verify the output to ensure that the application logs messages to the console and retrieves data from the database.
Your app should produce output that’s similar to the following example:
ConsoleLogger: Application started. Database connected. ConsoleLogger: Data retrieved: Sample Data from Database ConsoleLogger: Application finished. 
Task 2: Create IDataAccess interface for data access
In Visual Studio Code open IDataAccess.cs, the file only has comments.
- 
    
Open the
IDataAccess.csfile in Visual Studio Code. - 
    
Under the comment
TASK 2:Add the following code to define theIDataAccessinterface:public interface IDataAccess { // Connects to the data source. void Connect(); // Retrieves data from the data source. string GetData(); }The
ILoggerinterface defines a contract for logging functionality, ensuring that any class implementing it provides aLogmethod. - 
    
Save your changes.
 - 
    
Notice that adding the
IDataAccessinterface definition provides the foundation for functionality with a clearly defined contract for data access operations. This ensures that any class implementing the interface will provide concrete implementations for theConnectandGetDatamethods, enabling consistent and flexible data access across the application. 
Task 3: Implement the IDataAccess interface to decouple the DatabaseAccess class
Now, update the existing DatabaseAccess class to implement the newly defined interface.
- 
    
Open the
DatabaseAccess.csfile. - 
    
Under the comment
TASK 3:Add the following code to modify the class to implement theIDataAccessinterface:// This class implements the IDataAccess interface and is responsible for connecting to a database and retrieving data. public class DatabaseAccess : IDataAccess { // Simulates connecting to a database. public void Connect() { Console.WriteLine("Database connected."); } // Simulates retrieving data from the database. public string GetData() { return "Sample Data from Database"; } }The
DatabaseAccessclass now implements theIDataAccessinterface, providing methods to connect to a database and retrieve data. - 
    
Save your changes.
 - 
    
Notice that the only code change is adding the interface reference (
: IDataAccess) to theDatabaseAccessclass. This ensures the class adheres to the contract defined by theIDataAccessinterface, which includes theConnectandGetDatamethods. 
Task 4: Create ILogger interface for logging a message
- 
    
Open the
ILogger.csfile in Visual Studio Code. - 
    
Under the comment
TASK 4:, add the following code to define theILoggerinterface under the Task 4 comment:// Interface for logging messages. public interface ILogger { // Logs a message. void Log(string message); }The
ILoggerinterface abstracts logging operations, requiring implementing classes to provide a method for logging messages. - 
    
Save your changes.
 - 
    
Notice that adding the
ILoggerinterface definition establishes a clear contract for logging functionality. This ensures that any class implementing the interface will provide a concrete implementation of theLogmethod, enabling consistent and reusable logging behavior across the application. 
Task 5: Update ConsoleLogger class to implement the interfaces
Next, you update the existing ConsoleLogger class to implement the newly defined interface.
- 
    
Open the
ConsoleLogger.csfile. - 
    
Under the comment
TASK 5:, add the following code to modify the class to implement theILoggerinterface:public class ConsoleLogger : ILogger { // Logs a message to the console. public void Log(string message) { Console.WriteLine($"ConsoleLogger: {message}"); } } 
[!NOTE] The constructor code in Task 6 is incomplete. It doesn’t show the full
Applicationclass or how_loggerand_dataAccessare declared - those items must remain intact.
The ConsoleLogger class now implements the ILogger interface, providing a concrete implementation of the Log method to log messages to the console.
- 
    
Save your changes.
 - 
    
Notice that adding the
ILoggerinterface definition establishes a clear contract for logging functionality. This ensures that any class implementing the interface will provide a concrete implementation of theLogmethod, enabling consistent and reusable logging behavior across the application. 
Task 6: Refactor the application to use dependency injection
Refactor the Application class to depend on the ILogger and IDataAccess interfaces instead of directly instantiating the ConsoleLogger and DatabaseAccess classes.
- 
    
Open the
Application.csfile. - 
    
Under the comment
TASK 6:, add the following code to replace theApplicationclass:// TASK 6: Implement ILogger and IDataAccess interfaces and // refactor this constructor to accept them as parameters. public Application(ILogger logger, IDataAccess dataAccess) { _logger = logger; _dataAccess = dataAccess; }The
Applicationclass now uses dependency injection to receive its dependencies, making it more flexible and easier to test. 
Task 7: Refactor Program to use dependency injection
- Open the 
Program.csfile. - 
    
Replace all of the existing code with the following:
var logger = new ConsoleLogger(); var dataAccess = new DatabaseAccess(); // Inject the dependencies into the Application class. var app = new Application(logger, dataAccess); app.Run();The
Program.csfile creates instances ofConsoleLoggerandDatabaseAccessand injects them into theApplicationclass, demonstrating how dependency injection works in practice. 
Task 8: Test the refactored application
Finally, test the refactored application to ensure it works as expected.
- 
    
Run the application using the following command (or debug with
F5):dotnet runTo debug, set breakpoints in your code (e.g., in
Application.csorProgram.cs) and pressF5in Visual Studio Code to start debugging. This allows you to step through the code and inspect variables. - 
    
Verify the output to ensure that the application logs messages to the console and retrieves data from the database.
Your app should produce output that’s similar to the following example:
ConsoleLogger: Application started. Database connected. ConsoleLogger: Data retrieved: Sample Data from Database ConsoleLogger: Application finished. 
Check your work: Test the refactored application
Confirm that the application runs without errors and produces the following output:
    ConsoleLogger: Application started.
    Database connected.
    ConsoleLogger: Data retrieved: Sample Data from Database
    ConsoleLogger: Application finished.
This output confirms that the application is functioning correctly, with logging and database operations working as expected.
The output is the same as the starter code, but now the code is decoupled from specific implementations by using interfaces and dependency injection. This exercise demonstrated how to refactor tightly coupled code into loosely coupled components by defining interfaces (ILogger and IDataAccess), implementing them in concrete classes (ConsoleLogger and DatabaseAccess), and injecting these dependencies into the Application class.
Refactoring code using techniques like interfaces and dependency injection helps decouple components, making your application more flexible and maintainable. Interfaces define clear contracts between parts of the system, while dependency injection ensures that dependencies are provided in a modular and testable manner. Together, these practices improve the structure of your code, making it easier to extend, test, and adapt to future requirements.
Now that you’ve finished the exercise, consider archiving your project files for review at a later time. Having your own projects available for review can be a valuable resource when you’re learning to code. Additionally, building a portfolio of projects can be a great way to demonstrate your skills to potential employers.