ex-int-02-meet-the-teacher/readme.adoc
github-classroom[bot] 9331aaf8d0
add deadline
2025-04-24 13:39:28 +00:00

382 lines
No EOL
15 KiB
Text

[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/z5eNWieJ)
:sectnums:
:nofooter:
:toc: left
:icons: font
:data-uri:
:source-highlighter: highlightjs
:stem: latexmath
= Int.02 -- Meet the Teacher
This is an *extensive* assignment with the goal of processing the consultation hours of teachers (real data from a few years ago).
== Introduction
The application is split into several parts:
. Importing Data
** From multiple files and different sources
. Performing Sort Operations
** With a multitude of options
. Exporting Data
** In two different formats
. User Interaction
** A _modern_ console application
NOTE: You _really_ should not wait to the last minute before starting to work on this assignment! It _will_ take you some time to understand the relations between the multitude of components.
=== Sample Run [[sec:sample]]
Before reading more about the individual parts and their requirements watch a sample run of the application to get a feeling for what you are going to implement.
video::pics/sample_run.mp4[Sample Run,width=1000]
=== Tests
* Basic coverage with unit tests has been provided for you
** But _not_ complete, especially for edge cases => do not rely only on the tests
*** You are allowed to add tests, if you want to, of course
* Take a look at how we can make use of interfaces when writing unit tests
** Especially for _mocking_
** => you will write such tests _yourself_ in the future!
== Data Model
[plantuml]
----
@startuml
hide empty fields
hide empty methods
interface ICsvRepresentable {
+CsvData ToCsvData()
}
class CsvData <<sealed>> {
+char Separator [const]
+IReadOnlyList<string> HeaderNames [readonly]
+IReadOnlyList<string> Data [readonly]
+CsvData(IReadOnlyList<string> headerNames, IReadOnlyList<string> data)
+string GetHeader()
+string GetData()
+void Deconstruct(out IReadOnlyList<string> headerNames, out IReadOnlyList<string> data)
}
class Teacher {
+string Name [readonly]
+TimeFrame? ConsultingHour [init only]
+SchoolUnit? ConsultingHourUnit [init only]
+DayOfWeek? ConsultingHourWeekDay [init only]
+string? Room [init only]
-string? ConsultingHourTime [readonly]
+Teacher(string name)
+CsvData ToCsvData() [virtual]
}
class TeacherWithBusinessCard <<sealed>> {
-string BaseUrl [const]
+int Id [readonly]
+TeacherWithBusinessCard(string name, int id)
+CsvData ToCsvData() [override]
}
struct TimeFrame {
+TimeOnly Start [readonly]
+TimeOnly End [readonly]
+TimeFrame(TimeOnly start, TimeOnly end)
{static} +bool TryParse(string timeFrame, out TimeFrame? result)
+string ToString() [override]
}
enum SchoolUnit {
UE01
UE02
UE03
UE04
UE05
UE06
UE07
UE08
UE09
UE10
}
Teacher <|-- TeacherWithBusinessCard
Teacher o-- TimeFrame
Teacher o-- SchoolUnit
ICsvRepresentable <|.. Teacher
@enduml
----
The data model combines inheritance with interface implementation as it is common in modern applications.
* `ICsvRepresentable`
** Defines the _capability_ of a class to provide its own data in a format which can be easily processed to create a CSV file
* `CsvData`
** Contains data of a _single_ object in addition to the headers which define this data
** Some properties are of type `IReadOnlyList` -- *that's something new* => https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlylist-1?view=net-8.0[learn about it]
*** We'll need it quite often from now on
** We implement a `Deconstruct` method
*** That allows the compiler to perform a https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct[_deconstruct_ operation]
**** We can write things like `var (a, b) = objectWithAAndB`
*** *This is _not_ the same as a _destructor_! Do _not_ use destructors!*
* `Teacher`
** _All_ teachers are of type `Teacher`
** The name is required
*** Other properties are _optional_ (some colleagues do not offer a consulting hour)
** `DayOfWeek` is an `enum` defined in the framework -- https://learn.microsoft.com/en-us/dotnet/api/system.dayofweek?view=net-8.0[read, if you don't know it]
** `ConsultingHourTime` combines information from `ConsultingHour` & `ConsultingHourUnit`
*** Has to return `null` if components are `null`
*** Otherwise, returns format `HH:MM-HH:MM (UEXX)`
**** Example: `12:40-13:35 (UE06)`
* `TeacherWithBusinessCard`
** _Some_ teachers also want to display a business card, which requires a portrait picture, in addition to the regular information
** This picture is stored on a webserver, so we only have to deal with the id
** Such an image URI could look like this: `https://www.htl-leonding.at/media/teacher-avatar/1234`
*** _Fictional address, doesn't work!_
* `TimeFrame`
** Defines a time frame with a start and end time
*** Start time has to be before end time
** Implemented as a https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct[struct]
*** It's been some time since we last talked about structs.
Do you still remember the guidelines when to use one and when better to use a class?
If not, ask me to explain it again!
TIP: If you find the class diagrams hard to read (due to size) remember that you don't have images but those are gnerated from PlantUML code => you can look at individual parts, zoom in, use different editors or even simply look at the diagram source code
== Import
[plantuml]
----
@startuml
hide empty fields
hide empty methods
interface ITeacherDataImporter {
+IEnumerable<Teacher> LoadTeacherData()
}
class TeacherDataCsvImporter <<sealed>> {
-string _businessCardDataFilePath [readonly]
-string _generalDataFilePath [readonly]
+TeacherDataCsvImporter(string generalDataFilePath, string businessCardDataFilePath)
{static} -Dictionary<string, Teacher> CombineData(Dictionary<string, Teacher> generalData, IEnumerable<(string name, int id)> businessCardData)
-IEnumerable<(string name, int id)>? ReadTeacherBusinessCardData()
-Dictionary<string, Teacher>? ReadTeacherData()
}
class TeacherDataFakeImporter <<sealed>> {
}
ITeacherDataImporter <|.. TeacherDataCsvImporter
ITeacherDataImporter <|.. TeacherDataFakeImporter
@enduml
----
Data can be imported from various, different sources.
In the main part of the application we are only interested in getting the data, not how or from where.
To showcase this abstraction (and separation of concerns) we implement two 'data sources': one will read data from two CSV files and combine it to build the required format, while the other will simply fake importing and return sample data.
* `ITeacherDataImporter`
** Defines the capability to load `Teacher` data from a source
* `TeacherDataCsvImporter`
** Reads data from two CSV files
... One file contains general `Teacher` information
... The second file contains data only for `TeacherWithBusinessCard` teachers
** Reading data from a CSV file is easy for you by now, but be aware that some properties are _optional_
** The data from the two files has to be intelligently combined to create the required output
*** Hint: a collection which holds `Teacher` objects can _at the same time_ contain instances of _subclasses_
** Here you'll deal with `Dictionary` again -- we haven't used this collection extensively so far, maybe https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-8.0[you want to refresh your memory]
* `TeacherDataFakeImporter`
** This is just an example how different import providers could be used in this application, thanks to the interfaces
** Implementation has already been provided for you -- make sure you understand what's going on!
== Sorting
[plantuml]
----
@startuml
hide empty methods
hide empty fields
enum SortOrder
{
ByNameAsc
ByNameDesc
ByHourAsc
ByHourDesc
Room
}
abstract class ComparerBase {
#ComparerBase(bool ascending)
#bool Ascending [readonly]
}
interface IComparer<Teacher>
class ByHourComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
class ByNameComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
class ByRoomComparer <<sealed>> {
+int Compare(Teacher? x, Teacher? y)
}
IComparer <|.. ByHourComparer
IComparer <|.. ByNameComparer
IComparer <|.. ByRoomComparer
ComparerBase <|-- ByHourComparer
ComparerBase <|-- ByNameComparer
@enduml
----
Our application offers various sorting options.
These are implemented by different `Comparer` implementation.
TIP: Do you still remember the difference between https://learn.microsoft.com/en-us/dotnet/api/system.icomparable?view=net-8.0[`IComparable`] and https://learn.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=net-8.0[`IComparer`]?
* `ComparerBase`
** Base class for those comparers which support changing the sorting order from ascending to descending
* `ByHourComparer`
** Allows sorting of the teacher data by time of the consulting hour
** Sorting order can be ascending or descending
** _First_ the day of the week, and _then_ the unit is compared
** Hint: some properties are optional, remember how to deal with `null` values when sorting/comparing
* `ByNameComparer`
** Allows sorting of the teacher data by name
** Sorting order can be ascending or descending
* `ByRoomComparer`
** Allows sorting of the teacher data by room
** Sorting order is _always_ ascending
** Hint: `Room` can be `null`
== Export
CAUTION: The interactions between the individual components and the number of those is much more _complex_ than anything we did before -- do not hesitate to ask if you have trouble understanding!
[plantuml]
----
@startuml
hide empty methods
hide empty fields
enum ExportFormat
{
Csv
Html
}
class CsvDataExporter <<sealed>> {}
class DefaultExporterProvider <<sealed>> {}
class Exporter <<sealed>> {
-IComparer<Teacher> _comparer [readonly]
-IDataExporter _exporter [readonly]
+Exporter(SortOrder sortOrder, ExportFormat exportFormat, IExporterProvider exporterProvider)
+void Export(string fileName, List<Teacher> teachers)
}
abstract class FileExporterBase {
{abstract} #string FileExtension [readonly]
#void Export(string fileName, string content)
{static} #string LinesToText(IEnumerable<string> lines)
-string GetFilePath(string fileName)
}
class HtmlDataExporter <<sealed>> {
{static} -string CreateTableRow(IEnumerable<string> values, bool isHeader = false)
{static} -string WrapInTag(string content, string tag)
}
interface IDataExporter {
+void Export(IEnumerable<ICsvRepresentable> data, string? fileName)
}
interface IExporterProvider {
+IDataExporter GetExporter(ExportFormat exportFormat)
}
class SpectreTableExporter <<sealed>> {
+Table Table [private set]
+public SpectreTableExporter()
{static} -IEnumerable<string> FindMaxHeaderColumns(IEnumerable<CsvData> rows)
}
IDataExporter <|.. SpectreTableExporter
IDataExporter <|.. HtmlDataExporter
IDataExporter <|.. CsvDataExporter
FileExporterBase <|-- CsvDataExporter
FileExporterBase <|-- HtmlDataExporter
IExporterProvider <|.. DefaultExporterProvider
Exporter *-- IDataExporter
@enduml
----
Our application has a very _flexible_ export system.
Not only are two different file exports (CSV & HTML) supported, but an export to a specific in-memory data structure as well.
This requires a very sophisticated _decoupling_ of components => _many_ classes and interfaces.
Some parts also improve the _testability_ of the export functionality.
* `CsvDataExporter`
** Exports data to a CSV file
** This is very easy, because we are receiving `ICsvRepresentable` objects to export
** Several features of the base class help as well
* `DefaultExporterProvider`
** The default provider for exporters used in the application
* `Exporter`
** Can be configured to export data in a specific format and with a specific sorting order
** Before (finally) exporting, the data is sorted accordingly
*** For this task use the https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.sort?view=net-8.0[`Sort`] method of `List<T>`
* `FileExporterBase`
** Base class for those exporters which write data to a file
* `HtmlDataExporter`
** Exports data _encoded_ as an HTML table
*** Does _not_ create a full, valid HTML document -- only the data as HTML table
*** We use `.html` as file extension anyway
*** You know how to create such a table from WMC 😉
** You _have to_ use a https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/raw-string-literal[raw string literal]
*** This is useful when defining semi-structured data like JSON or HTML
*** You can (and should) even use indentation
** `CreateTableRow` will create either a `<tr><th>...</tr>` or `<tr><td>...</tr>` depending on the `isHeader` flag
* `IDataExporter`
** Defines the capability to export data of type `ICsvRepresentable`
* `IExporterProvider`
** Defines the capability to provide an exporter based on a specific, requested export format
** Primarily used for testing here -- could be used at runtime as well if the application expanded
* `SpectreTableExporter`
** Exports data to a `Table` as used by `Spectre.Console`
** We need this for our user interface
** To make it easier for you, these two methods have to be used:
... `table.AddColumn`
... `table.AddRow`
** In addition, you are _allowed_ (this time only, for now) to use the `ToArray` of `IReadOnlyList<string>`
** `FindMaxHeaderColumns`
*** Remember that the data to export can contain `Teacher` & `TeacherWithBusinessCard` objects at the same time
*** The table has to be set to the _max._ number of columns
**** `TeacherWithBusinessCard` has one more, which will simply stay empty for `Teacher` objects
*** We don't know if there are `TeacherWithBusinessCard` contained, so we have to _check_ to find the max. set of headers
TIP: Consider the provided, detailed XMLDoc as well, it offers additional insights
== User Interaction
To conclude this extensive assignment we want to give it the exciting user interface it deserves.
After all, the interface is what customers will see of your product, so we cannot ignore the importance of making a good impression altogether (despite concentrating on clean and *correct* code primarily).
So far, you are limited to console applications.
But those don't have to be boring!
This time we will be using the cross-platform, free & open-source https://www.nuget.org/packages/Spectre.Console[`Spectre.Console`] NuGet package.
It has a https://spectreconsole.net/[good documentation] which I implore you to check out.
There are many features, such as structure (tables, grids, rules,...), interactivity (prompts, loading indicators,...) and design (colors, fonts,...) -- see the examples below (taken from the package website).
.`Spectre.Console` Examples
[%collapsible]
====
image::pics/spectre_examples.png[Spectre.Console Examples]
====
Since this is the first time you have to work with this package, and because some features require callbacks -- which you know from WMC, but we haven't discussed in POSE so far -- most of the implementation has already been provided for you.
Still, some things need to be implemented -- look for the `TODO`.
A <<sec:sample,sample run>> has already been shown at the beginning of the document.
🎉 Congratulations -- you completed the assignment, learnt new things and had a lot of programming exercise 🎉