On Sunday, October 8, 2023 at 8:15:41 PM UTC-5, luserdroog wrote:
>
> [...]
To quote Kent Beck: "Make it work, make it right, make it fast"
You've finished step 1, so iteration and improvement should be straightforward.
Avoiding OO/modules can work but makes things progressively harder as the codebase size increases as everything is in the same namespace (informally speaking). Some functions are more related to each other than others so defining an explicit grouping and naming them gives you power over them and an organizing principle. An object/class/module accomplish this.
I'd suggest improving the separation of concerns as the intent of the code is non-obvious unless you run the application.
1. separate out your domain.
What are the abstract, platonic concepts of the application divorced from the interface and how you're storing them?
From what I can see you have: "Station" as the the lone top level entity and a number of supporting entities:
// TypeScript
class Station {
location: Location
position: Position
name: string
timeSpan: TimeSpan
presence: boolean
notes: string
}
enum Location { ... }
enum Position { ... }
2. Separate out your data access
The responsibility of loading/saving data should be separated from other concerns. Define and use the Repository pattern for each primary entity:
abstract class Repository<T> {
getById(id: string): Promise<T>
getAll(): Promise<T[]>
update(item: T): Promise<void>
save(item: T): Promise<void>
delete(id: string): Promise<void>
}
class LocationRepository extends Repository<Location> {
/* manipulate localstorage and such. You can also perform the mappings to/from JSON here */
}
3. Explicitly define your use cases.
The application you're writing should be ignorant of how data is stored and what the UI looks like. By defining explicit use cases you can obtain
"Screaming Architecture". In other words by simply looking at the project we know exactly what it's doing by only glancing at the code and not running it.
A Use Case is either a Command or a Query and can be build via the Command Pattern:
abstract class UseCase<T,U> {
abstract exec(t: T): U
}
abstract class Command<T> extends UseCase<T, void> {}
abstract class Query<U> extends UseCase<void, U> {}
This enforces the CQS principle <
https://en.wikipedia.org/wiki/Command%E2%80%93query_separation>
An example usage:
class ShowStationsUseCase extends Query<Station[]> {
constructor(
private repo: Repository<Station>
) { super() }
exec(): Station[] {
const stations = this.repo.getAll()
// sort or whatever data massaging you want
return stations
}
}
4. The presentation layer utilizes use cases instead of mixing business logic and such as it is now:
class MyStationListingComponent {
repository = new StationRepository()
useCase = new ShowStationsUseCase(this.repository)
...
constructor() {
super()
// in constructor, or maybe in a callback method:
const stations = this.useCase.exec()
this.present(stations)
}
}
This is a very impressionistic set of suggestions and is a variant of what is referred to as "Clean Architecture" which unifies the "Hexagonal" and "Onion" architectures:
<
https://www.plainionist.net/Implementing-Clean-Architecture/>