1. Relationship between the model and the code¶
The goal of the exercise¶
The goal of the exercise:
- Getting to know the students/trainer
- Clarification of the requirements for exercises
- Getting started with Visual Studio and .NET application development.
- Building a simple Hello World .NET application, C# basics
- Illustrating the relationship between UML and code
- The interface and the abstract primitive class application technique
For teachers
While there will certainly be some students who have used the Visual Studio environment before, in Prog2 (C++) or for other reasons, there will almost certainly be others who have not used it or who remember it less. The goal here is to get familiar with the interface, so as you work through the exercises, you will be introduced to the things you use (e.g. Solution Explorer, F5 running, using breakpoints, etc.) to build your first C# application.
Prerequisites¶
The tools needed to carry out the exercise:
- Visual Studio 2022
The latest version of Visual Studio should be installed. The Community Edition, Professional and Enterprise versions are also suitable. The Community Edition is free and can be downloaded from the Microsoft website. The Professional is paid, but it is also available free of charge to students of the university (on the website, as part of the Azure Dev Tools for Teaching programme).
Visual Studio Class Diagram support
For some of the exercises in this exercise (and also for the first homework) we will use the Visual Studio Class Designer support. Visual Studio does not always add the Class Designer component during installation. If it is not possible to add a Class Diagram to your Visual Studio project (because the Class Diagram is not listed in the list of the window that appears during the Add New Item command - more on this later in this guide), you will need to install the Class Diagram component later:
What you should check out:
- The exercise does not include a lecture on the subject. At the same time, the exercise builds on basic UML knowledge and the basics of mapping UML class diagrams to code.
Course of exercise¶
The trainer will summarise the requirements for the exercises at the beginning of the exercise:
- Most of these can be found in the fact sheet
- Information on homework is available on the subject's website.
Using Visual Studio development tool, we will build .NET applications in C#. C# is similar to Java, we will gradually learn the differences. The tutorial is guided, with instructions from the tutor, and the tasks are done together.
Solution¶
Download the finished solution
It is essential that you follow the lab guide during the lab, it is forbidden (and pointless) to download the ready-made solution. However, during subsequent self-practice, it can be useful to review the ready-made solution, so we make it available.
The solution is available on GitHub. The easiest way to download it is to clone it from the command line to your machine using the git clone
command:
git clone https://github.com/bmeviauab00/lab-modellkod-kiindulo -b solved
You need to have git installed on your machine, more information here.
1. Task - Build a "Hello world" .NET console application¶
The task is to create a C# console application that prints the text "Hello world!" to the console.
The application is written in C#. The compiled application is run by the .NET runtime. The theoretical background of compiling/running and the basics of .NET are covered in the first lecture.
The steps to create a solution and a project within it in Visual Studio 2022:
- Start a new project wizard, which can be done in two ways
- Using the startup window
- Launch Visual Studio
- In the right-hand sidebar of the launch window that appears Create new project
- Already running in Visual Studio
- File / New-Project
- Using the startup window
-
In the Create new project wizard, select the Console app (and NOT the Console app (.NET Framework) template, including the C# one. That it is C# is indicated by the top left corner of the template icon. If you don't see it in the list, you have to search/filter for it. You can search for it by typing "console" in the top search bar. Or by using the drop-down boxes below: in the first (language selector) "C#", in the third (project type selector) "Console".
Creating a project
-
Next button at the bottom of the wizard window, on the next wizard page:
- Project name: Hello World
- Location: in the labs, work in the c:\work\
folder, you have write access to it. - Solution name: Hello World (this should be written in by the time we get here)
- Place solution and project in the same directory: no tick (but not particularly significant).
-
Next button at the bottom of the wizard window, on the next wizard page:
- Framework: .NET 8 (Long-term support).
- Check the "Do not use top level statements" checkbox (we'll explain this in a moment).
The project also creates a new solution, whose structure can be viewed in the Visual Studio Solution Explorer window. A solution can consist of several projects, and a project can consist of several files. A solution is a summary of the entire working environment (it includes a file with the extension .sln
), while the output of a project is typically a file .exe
or .dll
, i.e. a component of a complex application/system. The project file extension for C# applications is .csproj
.
The content of our Program.cs
file is as follows:
namespace HelloWorld
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
Take a Console.ReadKey()
line:
namespace HelloWorld
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Console.ReadKey();
}
}
}
-
Run the application (e.g. using the F5 key).
The structure of the code is very similar to Java and C++. Our classes are organised into namespaces. You can define a namespace with the keyword
namespace
. You can "scope" namespaces with theusing
keyword. e.g:using System.Collections.Generic;
-
In a console C# application, you specify the entry point of your application by writing a static function called
Main
. Our class name can be anything, VS generated a class calledProgram
in our case. The parameter list of theMain
function is bound: either no parameters are given, or astring[]
is given, in which the command line arguments are given at runtime. - in .NET, the
Console
class of theSystem
namespace is used to handle standard input and output. With the static operationWriteLine
you can write a line, withReadKey
you can wait for a key to be pressed.
Top level statements, Implicit and static usings and namespaces
When the project was created, we previously checked the "Do not use top level statements" checkbox. If we had not done this, we would have found only one meaningful line in our Program.cs
file:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello World!");
This is functionally equivalent to the code above containing the Program
class and its Main
function. Let's look at what makes this possible (you can read more about them here https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/top-level-statements, both new in C# 10):
- Top level statements. The idea is that you can write code directly in a single source file without any class/
Main
and other function definitions in the project. In this case, behind the scenes, the compiler puts this into a staticMain
function of a class we don't see. The motivation for its introduction was to reduce boilerplate code for very simple, "script-like" applications. - Implicit global usings. Depending on exactly what project type you have created, certain base namespaces will be automatically using behind the scenes in all source files (the compiler uses the global using directive for this). The point is: this way, developers don't have to use certain frequently used namespaces (e.g.
System.IO
,System.Collections.Generic
, etc.) as source files. -
Static using. It is possible to use static classes instead of namespaces in C#, so it is not important to write them when using them. A common case is the use of the
Console
orMath
class.using static System.Console; namespace ConsoleApp12 { internal class Program { static void Main(string[] args) { WriteLine("Hello, World!"); } } }
-
File-level namespaces. In C# 10, we also get a simplification when declaring namespaces, because it is no longer mandatory to use brackets, so the given namespace will be valid for the whole file, e.g.:
namespace HelloWorld; internal class Program { // ... }
Inconsistent visibility or inconsistent accessibility error
During the semester, you may encounter translation error messages complaining about inconsistent visibility or inconsistent accessibility when implementing programming tasks. This phenomenon is due to the possibility to control the visibility of each type (class, interface, etc.) in a .NET environment:
internal
or no visibility is specified: the type is visible only inside the assembly (.exe, .dll)/projectpublic
: the type is visible to other assemblies/projects
The easiest way to avoid this error is to define all our types as public, e.g.:
public class HardDisk
{
// ...
}
Theoretical overview¶
The sub-chapters do not contain exercises, but provide students with an introduction to the related theoretical topics, illustrated with examples.
A) Theory of the relationship between the UML class diagram and code [student]*¶
The material is available here: The relationship between the UML class diagram and code. The relationship between the UML class diagram and code. This topic was covered in the previous semester in the Software Engineering course.
B) Interface and abstract (parent) class [student]*¶
The material is available here: Interface and abstract (base) class. Interface and abstract (base) class.
Topics:
- Abstract class concept and definition in C#
- Interface concepts and definitions in C#
- Comparison of abstract base class and interface
2. Task - Illustrate the relationship between UML and code¶
Task description - Equipment inventory¶
Task: We were asked to develop a computer parts inventory application. Read more:
- You need to be able to handle different types of parts. Initially,
HardDisk
,SoundCard
andLedDisplay
types should be supported, but the system should be easily extensible to new types. - The data related to the parts are: year of purchase, age (calculated), purchase price and current price (calculated), but may also include type-specific data (e.g. capacity for
HardDisk
). - The actual price depends on the type of part, the purchase price and the year of production of the part. For example, the older the part, the bigger the discount, but the discount depends on the part type.
- You must be able to list the parts in stock.
- The
LedDisplay
class must be derived from anDisplayBase
class, and the source code of theDisplayBase
class cannot be changed. In this example this does not make much sense, but in practice we often encounter similar situations where the framework/platform we are using requires us to derive from a built-in class. Typically, this is the case when working with windows, forms, custom control types: we have to derive them from the framework's built-in classes, and we don't have (or at least certainly don't want to change) the source code of the framework - e.g. Java, .NET. In our example, we simulate this situation by specifying a derivation fromDisplayBase
.
The implementation is simplified considerably: the parts are only stored in memory, and the listing is as simple as possible, simply by writing the data of the registered parts to the console.
During the initial discussions, we receive the following information from the client: an internal staff member has already started the development, but due to lack of time, they have only reached a half-finished solution. Part of our task is to understand the semi-finished solution and to implement the task from there.
Class Diagram¶
Let's open the source code solution from our customer, which we can do by following the steps below.
To do this, clone the Git repository of the initial project, available online on GitHub, to a new folder of its own within C:\Work
: e.g: C:\Work\NEPTUN\lab1
. In this new folder, open a command line or powershell and run the following git command:
git clone https://github.com/bmeviauab00/lab-modellkod-kiindulo.git
Note
You will read more about Git as a source code management system in the context of the first homework assignment.
Open the Visual Studio solution src/EquipmentInventory.sln in the cloned folder.
In Solution Explorer, run through the files by eye. It would help to understand the relationships between classes by displaying them on a class diagram. Let's include a class diagram in our project. In the Solution Explorer, right-click on the project (not the solution!), select Add/New Item from the pop-up menu, then in the window that appears, select Class Diagram, enter Main.cd as the name of the diagram at the bottom of the window, and OK-close the window.
Missing Class Diagram template
If the Class Diagram item does not appear in the list, then the appropriate component of VS is not installed. You can read more about this in the Prerequisites section of this document.
The chart file Main.cd
will then appear in Solution Explorer, double-click on it to open it. Our chart is currently empty. From Solution Explorer, drag&drop the .cs source files onto the diagram. VS then looks at what classes are in these source files and decomposes them into UML classes. Build the layout as shown in the following figure (you can display the members of the classes by clicking on the double arrow in the top right corner of their rectangle):
Starting class diagram
You can also view the source code for the classes, either by double-clicking on the corresponding class on the diagram or by opening the .cs files from Solution Explorer. Here's what we see:
- The
SoundCard
,HardDisk
andLedDisplay
classes are relatively well developed, with the necessary attributes and query functions. - The
LedDisplay
is derived from theDisplayBase
class as required. EquipmentInventory
is responsible for the inventory of parts in stock, but practically none of this is implemented.- We find an interface
IEquipment
with operationsGetAge
andGetPrice
EquipmentInventory¶
Let's start working on a solution. First, let's lay down the basic concepts. In the EquipmentInventory
class, we store a heterogeneous collection of different types of equipment. This is the key to consistent parts management, so that our solution can be easily extended with new parts types.
As discussed earlier, unified management can be achieved either by implementing a common base class or a common interface. In our case, the common base class (e.g. EquipmentBase
) seems to be dropped, because by introducing it, the LedDisplay
class would have two base classes: the mandatory DisplayBase
, and the EquipmentBase
that we introduce for uniform management. This is not possible, in a .NET environment a class can have only one base class. The solution to modify DisplayBase
to be derived from EquipmentBase
is not possible according to our requirement (it was a requirement that its source code cannot be modified). This leaves the interface-based approach. This was probably the conclusion of the previous developer of the application, which is why he introduced the IEquipment
interface.
Add a generic list of items of type IEquipment
(not property but field!) to the EquipmentInventory
class. Its visibility - in an effort to be unified - should be private
. The name should be equipment
(no "s" at the end, in English the plural of equipment is also equipment). To add a member variable, we use the Visual Studio Class Details window. If the window is not visible, it can be displayed by selecting View / Other Windows / Class Details.
Class Details
The member variable type is therefore List
. The type of .NET List
is a dynamically stretching generic array (like ArrayList
in Java).
Looking at the EquipmentInventory
class in the diagram, we see that only the name of the member variable is displayed, not the type. Right-click on the background of the diagram and select Display Full Signature from the Change Members Format menu. The chart will then display the type of member variables and the full signature of the operations.
EquipmentInventory
By double-clicking on the EquipmentInventory
class, you can navigate to the source code, and as you can see, it does indeed appear in the code as a member variable of type list:
class EquipmentInventory
{
private List<IEquipment> equipment;
On the one hand, we're happy about this because Visual Studio supports round-trip engineering: changes to the model are immediately reflected in the code, and vice versa. On the other hand, we have previously discussed that if a class has a collection of members from another class, then it "fits" in the UML model as a type 1-more association relation between the two classes. This is not yet the case in our model. Fortunately, the VS modelling interface can be made to display this type of connection in this form. To do this, right-click on the equipment tag variable on the diagram and select Show as Collection Association from the menu. The IEquipment
interface should then be moved to the right to allow enough space on the diagram to display the association relationship and the role on the relationship:
Collection association
The double arrow ending on the "plural" side is not standard UML, but don't be too sad about it, it's not important. We are certainly pleased that the arrow representing the relationship at the end of the IEquipment
role shows the name (and even the exact type) of the member variable.
Navigate to the source code of EquipmentInventory
and write the constructor that initializes the equipment
collection
public EquipmentInventory()
{
equipment = new List<IEquipment>();
}
Then write the ListAll
method, which prints the age of the elements and their current values:
public void ListAll()
{
foreach (IEquipment eq in equipment)
{
Console.WriteLine($"Age: {eq.GetAge()}\tÉrtéke: {eq.GetPrice()}");
}
}
Iterate through the elements using the foreach
statement. When using the foreach
statement, the in
keyword should be followed by a collection and preceded by a variable declaration (in this case IEquipment eq
), where type is the element type of the collection. In each iteration, this variable takes the iteration value of the collection.
Console.WriteLine
is either a simple string or, as in this case, a formatting string. The substitutions are solved by string interpolation: the values to be substituted must be given between `. If string interpolation is used, the string must start with
$`.
Write a function called AddEquipment
that adds a new device to the inventory:
public void AddEquipment(IEquipment eq)
{
equipment.Add(eq);
}
IEquipment implementers¶
We have previously decided to use the IEquipment
interface to manage the different component types in a uniform way. In our example, both SoundCard
and HardDisk
have GetAge()
and GetPrice()
methods, yet we cannot manage them in a unified way (e.g., store them in a common list). To do this, we need to get both classes to implement the IEquipment
interface. Change their source:
public class SoundCard : IEquipment
public class HardDisk : IEquipment
Then we need to implement the methods in the IEquipment
interface in the SoundCard
and HardDisk
classes. We find that there is nothing to do with this now, the GetPrice
and GetAge
functions are already written in both places.
As a test, in our Main
function in Program.cs
, create an EquipmentInventory
object, populate it with HardDisk
and SoundCard
objects, and then list the object on the console. If 2021 is not the current year, in the following rows, copy the year 2021 to the current year and the year 2020 to a smaller number!
static void Main( string[] args )
{
EquipmentInventory ei = new EquipmentInventory();
ei.AddEquipment(new HardDisk(2021, 30000, 80));
ei.AddEquipment(new HardDisk(2020, 25000, 120));
ei.AddEquipment(new HardDisk(2020, 25000, 250));
ei.AddEquipment(new SoundCard(2021, 8000));
ei.AddEquipment(new SoundCard(2020, 7000));
ei.AddEquipment(new SoundCard(2020, 6000));
ei.ListAll();
}
Running the application, we find that although our solution is rudimentary, it works:
Console output
Continue with the LedDisplay
class. The DisplayBase
base class source code cannot be modified due to requirements. But this doesn't cause any problems, our LedDisplay
class will implement the IEquipment
interface, so modify the code accordingly:
public class LedDisplay : DisplayBase, IEquipment
In the LedDisplay
class, the functions in the interface must already be written:
public double GetPrice()
{
return this.price;
}
public int GetAge()
{
return DateTime.Today.Year - this.manufacturingYear;
}
Let's extend our Main
function by adding two LedDisplay
objects to our set (again, if 2021 is not the current year, we should rewrite 2021 to the current year in the following lines, and 2020 to a smaller number!
ei.AddEquipment(new LedDisplay(2020, 80000, 17, 16));
ei.AddEquipment(new LedDisplay (2021, 70000, 17, 12));
ei.ListAll();
Console.ReadKey();
As a test, run the application.
3. Task - Application of the interface and the abstract primitive class¶
Interface problems¶
Evaluate our current interface-based solution.
One of the main problems is that our code is full of code duplication that destroys maintainability and extensibility:
- The
yearOfCreation
andnewPrice
tags are common to all part types (except the specialLedDisplay
), and must be copy-pasted when a new type is introduced. - The implementation of the
GetAge
function is the same for all component types (except for the specialLedDisplay
), also copy-paste "propagated". - The lines of the constructors
yearOfCreation
andnewPrice
initializing tags are also duplicated in each class.
Although this code duplication does not seem significant at the moment, the situation is getting worse as new component types are introduced, and it is better to prevent future pains in time.
The other problem is that the listing of parts data is currently painfully incomplete, with no part type (only age and price). To display the type, the IEquipment interface must be extended, e.g. by introducing an operation called GetDescription
. Let's add a GetDescription
function to the interface!
public interface IEquipment
{
double GetPrice();
int GetAge();
string GetDescription();
}
Then every class implementing the IEquipment
interface would have to implement this method, which is a lot of work for many classes (and often not even feasible for a multi-component application, i.e. one with several DLLs, when they are not in the hands of a single developer). Run the Build command to check that after adding GetDescription
, you get compilation errors in three places.
Specifying default implementation in interface
It is worth knowing that starting from C# 8 (or .NET or .NET Core runtime, not supported under .NET Framework), interface operations can be given default implementation (default interface methods), so to solve the above problem you don't need an abstract class, but interface can no longer have member variables. More information here: default interface methods.
public interface IEquipment
{
double GetPrice();
int GetAge();
string GetDescription() { return "EquipmentBase"; }
}
Abstract class¶
A solution to both problems is the introduction of a common abstract base class (except for the LedDisplay
class, which we will come back to). We can move the code common to descendants into it, and provide a default implementation for the newly introduced GetDescription
operation. Let our new abstract base class be called EquipmentBase
. The question is whether the IEquipment
interface is still needed, or whether it can be completely replaced by the new EquipmentBase
class. We need to keep the IEquipment
interface, because we cannot derive our LedDisplay class from EquipmentBase
: it already has a mandatory base class, DisplayBase
: for this reason, EquipmentInventory in our enhanced solution also refers to the various components as IEquipment
interface.
Let's start the transformation. Let our class diagram be the active tab. From the Toolbox, drag&drop an Abstract Class element onto the diagram, name it EquipmentBase
.
Toolbox - abstract class
In the following, we need to make the SoundCard
and HardDisk
classes derive from EquipmentBase
(LedDisplay
already has another base class, so we cannot do this there). To do this, select the Inheritance link in the Toolbox, then draw a line from the child class to the base class for both SoundCard
and HardDisk
.
In the next step, let's modify the code so that HardDisk
and SoundCard
do not implement the IEquipment
interface separately, but rather their common base class EquipmentBase
implement it once. To do this, modify the EquipmentBase class to implement the interface (either by drawing an inheritance link from EquipmentBase
to IEquipment
on the diagram, or by modifying the source code of EquipmentBase
). Delete the implementation of IEquipment
from the HardDisk
and SoundCard
classes (the base class already implements it).
The relevant parts of our diagram and source code will then look like this:
EquipmentBase and HardDisk/SoundCard
public abstract class EquipmentBase : IEquipment
public class HardDisk : EquipmentBase
public class SoundCard : EquipmentBase
Our code is not yet turning, for several reasons. The EquipmentBase
implements the IEquipment
interface, but it does not yet implement the interface operations. Either generate the methods using the smart tag, or type them according to the following principles:
- The
newPrice
andyearOfCreation
are duplicated in theHardDisk
andSoundCard
classes: move (not copy!) them to the commonEquipmentBase
base class and giveprotected
visibility. - The
GetAge
operation is duplicated in theHardDisk
andSoundCard
classes, delete the implementation from these and move it to theEquipmentBase
class. - The
GetPrice
operation is included in the base class as an abstract operation. This is a deliberate design decision, so we force descendant classes to override this operation anyway. - In the case of
GetDescription
, the opposite is true: it is defined as virtual (and not abstract), i.e. we provide an implementation in the base class. This way, descendants are not forced to override the operation.
The code corresponding to the above is:
public abstract class EquipmentBase : IEquipment
{
protected int yearOfCreation;
protected int newPrice;
public int GetAge()
{
return DateTime.Today.Year - yearOfCreation;
}
public abstract double GetPrice();
public virtual string GetDescription()
{
return "EquipmentBase";
}
}
Some additional thoughts on the code fragment:
- For abstract classes, the keyword
abstract
must be written before the wordclass
. - For abstract operations, the keyword
abstract
must be specified - in a .NET environment, you can control whether an operation is virtual or not. In this respect, it is similar to C++. To make an operation virtual, the keyword
virtual
must be specified for the operation. Reminder: define an operation as virtual if its descendants overdefine it. Only then is it guaranteed that the descendant version will be called when invoking the given operation on an ancestor reference.
Descendants¶
In the next step, let's move on to the EquipmentBase
descendants. When overriding abstract and virtual operations in C#, you must specify the override
keyword in the descendant. First, the GetPrice
operation is redefined:
public override double GetPrice()
{
return yearOfCreation < (DateTime.Today.Year - 4)
? 0
: newPrice - (DateTime.Today.Year - yearOfCreation) * 5000;
}
public override double GetPrice()
{
return yearOfCreation < (DateTime.Today.Year - 4)
? 0
: newPrice - (DateTime.Today.Year - yearOfCreation) * 2000;
}
In the next step, the GetDescription
operation is written in the HardDisk
and SoundCard
classes. Since the virtual function of the base class is being overridden here, the override
keyword must also be specified:
public override string GetDescription()
{
return "Hard Disk";
}
public override string GetDescription()
{
return "Sound Card";
}
One might ask why the designers of the C# language decided to add an extra keyword to the definition of operations, which was not necessary in the case of C++. The reason is simple: the code is more expressive. Looking at the descendant code, the word override
immediately makes it clear whether this operation is abstract or virtual in one of the base classes, without having to look at the code of all the ancestors.
Base class of LedDisplay¶
The base class of our LedDisplay
class is bound, its code cannot be modified, so we cannot derive it from EquipmentBase
. We cannot delete the GetAge
operation, this code duplication is preserved here (but only for LedDisplay
, which is only one class among many!).
Note
In fact, with a little extra work we could get rid of this duplication. This would require a static helper function in one of the classes (e.g. EquipmentBase
), which would get the year of manufacture and return the age. EquipmentBase.GetAge
and LedDisplay.GetAge
would use this helper function to produce their output.
In our LedDisplay
class, we are yet to write GetDescription
:
public string GetDescription()
{
return "Led Display";
}
Note that we have NOT specified the override
keyword here. When an interface function is implemented, override
is not required/allowed to be written.
Use GetDescription¶
Modify the EquipmentInventory.ListAll
operation to also write the description of the items to the output:
public void ListAll()
{
foreach (IEquipment eq in equipment)
{
Console.WriteLine($"Description: {eq.GetDescription()}\t" +
$"Age: {eq.GetAge()}\tÉrtéke: {eq.GetPrice()}");
}
}
This gives a more informative output when the application is run:
Console output
Constructor code duplication¶
Looking at our code, there is one more duplication. All EquipmentBase
descendants (HardDisk
, SoundCard
) have these two lines in their constructor:
this.yearOfCreation = yearOfCreation;
this.newPrice = newPrice;
If you think about it, these yearOfCreation
and newPrice
members are defined in the base class, so it should be his responsibility to initialize them anyway. Let's add a corresponding constructor in EquipmentBase
:
public EquipmentBase(int yearOfCreation, int newPrice)
{
this.yearOfCreation = yearOfCreation;
this.newPrice = newPrice;
}
Remove the initialization of the two members from the constructor of the descendants HardDisk
and SoundCard
, and instead invoke the base class’s constructor by referencing the base
keyword:
public HardDisk(int yearOfCreation, int newPrice, int capacityGB)
: base(yearOfCreation, newPrice)
{
this.capacityGB = capacityGB;
}
public SoundCard(int yearOfCreation, int newPrice)
: base(yearOfCreation, newPrice)
{
}
Evaluation¶
By using a combination of interface and abstract base class, we have managed to develop the solution with the least compromise:
- By referring to
IEquipment
as an interface, we can uniformly handle all types of parts, even those where the base class was bound (using abstract base classes alone would not have achieved this). - By introducing the
EquipmentBase
abstract base class, we were able to put the code common to different part types into a common base, with one exception, thus avoiding code duplication. - By introducing the
EquipmentBase
abstract ancestor, we can specify a default implementation for newly introducedIEquipment
operations (e.g.GetDescripton
), so we are not forced to specify it in everyIEquipment
implementation class.
Finally, let's take a look at the UML (like) class diagram of our solution:
Ultimate class diagram
Static interfaces
The latest addition to C# 11 is the definition of static interface members, which allows you to require an implementing class to have members that do not refer to the object instance, but rather the class must have a specific static member. Read more
Note - optional homework exercise¶
Our solution does not support the display of component specific data (e.g. capacity for HardDisk
) during listing. To do this, the writing of component data to a formatted string should be moved from the EqipmentInventory
class to the component classes, following the principles below:
- To do this, we can introduce an
GetFormattedString
operation in theIEquipment
interface, which returns an object of typestring
. Alternatively, you can override the ToString()operation of
System.Object. indeed, in .NET, all types are implicitly derived fromSystem.Object
, which has a virtualToString()
operation. - In
EquipmentBase
we write the formatting of the common tags (description, price, age) into a string. - If a component also has type-specific data, then its class overrides the function that formats it into a string: this function must first call its ancestor (using the
base
keyword), then append its own formatted data to it, and return with this string.