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:
- Review the current version of your logging application.
- Create IDataAccess interface for data access.
- Implement the IDataAccess interface to decouple the DatabaseAccess class.
- Create ILogger interface for logging a message.
- Update ConsoleLogger class to implement the interfaces.
- Refactor the Application class to use dependency injection.
- Refactor the Program.cs file to use dependency injection.
- Test the refactored application.
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: Placeholder for theIDataAccessinterface (will be used to specify the contract for data access operations likeConnectandGetData). -
ILogger.cs: Placeholder for theILoggerinterface (will be used to specify 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.
NOTE: Reviewing the initial project files code reveals the tightly coupled nature of the application components. 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
TASK 2:comment, to define theIDataAccessinterface, add the following code:public interface IDataAccess { // Connects to the data source. void Connect(); // Retrieves data from the data source. string GetData(); }The
IDataAccessinterface defines a contract for data access operations, ensuring that any class implementing it providesConnectandGetDatamethods. -
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
TASK 3:comment, to implement the IDataAccess interface, replace the existing DatabaseAccess class with the following code:// 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
TASK 4:comment, to define theILoggerinterface, add the following code:// 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
TASK 5:comment, to implement the ILogger interface, replace the existing ConsoleLogger class with the following code:// This class implements the ILogger interface and is responsible for // logging messages to the console. public class ConsoleLogger : ILogger { // Logs a message to the console. public void Log(string message) { Console.WriteLine($"ConsoleLogger: {message}"); } }The
ConsoleLoggerclass now implements theILoggerinterface, providing a concrete implementation of theLogmethod to log messages to the console.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. -
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 class 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
TASK 6:comment, replace the field declarations andApplicationclass constructor with the following code:private readonly ILogger _logger; private readonly IDataAccess _dataAccess; 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 the Program.cs file 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.
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.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 (
ILoggerandIDataAccess), implementing them in concrete classes (ConsoleLoggerandDatabaseAccess), and injecting these dependencies into theApplicationclass.
NOTE 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.
Clean up
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.