382 lines
No EOL
15 KiB
Text
382 lines
No EOL
15 KiB
Text
[](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 🎉 |