From c3c5b897f308f613c252182a02cdd4cfa7150fa3 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Fri, 24 Dec 2021 21:14:27 +0100 Subject: [PATCH] Refactor to add readonly interfaces Using more granular interfaces adds to expressiveness of the code. Knowing what needs to mutate the state explicitly helps easier understanding of the code and therefore increases the maintainability. --- src/application/Context/IApplicationContext.ts | 10 +++++++--- .../Context/State/CategoryCollectionState.ts | 2 +- .../Context/State/Code/ApplicationCode.ts | 4 ++-- .../Context/State/Filter/IUserFilter.ts | 5 ++++- .../Context/State/ICategoryCollectionState.ts | 15 ++++++++++----- .../Context/State/Selection/IUserSelection.ts | 7 +++++-- .../Code/CodeButtons/TheCodeButtons.vue | 10 +++++----- src/presentation/components/Code/TheCodeArea.vue | 4 ++-- .../Scripts/Menu/Selector/SelectionTypeHandler.ts | 6 +++--- .../components/Scripts/Menu/TheOsChanger.vue | 4 ++-- .../components/Scripts/Menu/TheScriptsMenu.vue | 6 +++--- .../components/Scripts/View/Cards/CardList.vue | 4 ++-- .../SelectableTree/Node/RevertToggle.vue | 4 ++-- .../components/Scripts/View/TheScriptsView.vue | 6 +++--- src/presentation/components/Shared/StatefulVue.ts | 5 +++-- src/presentation/components/TheSearchBar.vue | 8 ++++---- 16 files changed, 58 insertions(+), 42 deletions(-) diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index 6d3e815e..9a8090a3 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -1,12 +1,16 @@ -import { ICategoryCollectionState } from './State/ICategoryCollectionState'; +import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IApplication } from '@/domain/IApplication'; -export interface IApplicationContext { +export interface IReadOnlyApplicationContext { readonly app: IApplication; - readonly state: ICategoryCollectionState; + readonly state: IReadOnlyCategoryCollectionState; readonly contextChanged: IEventSource; +} + +export interface IApplicationContext extends IReadOnlyApplicationContext { + readonly state: ICategoryCollectionState; changeContext(os: OperatingSystem): void; } diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 0f31539e..7638a384 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -5,7 +5,7 @@ import { UserSelection } from './Selection/UserSelection'; import { IUserSelection } from './Selection/IUserSelection'; import { ICategoryCollectionState } from './ICategoryCollectionState'; import { IApplicationCode } from './Code/IApplicationCode'; -import { ICategoryCollection } from '../../../domain/ICategoryCollection'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; export class CategoryCollectionState implements ICategoryCollectionState { diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index 94214cc4..f0a4db23 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -2,7 +2,7 @@ import { CodeChangedEvent } from './Event/CodeChangedEvent'; import { CodePosition } from './Position/CodePosition'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; -import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; +import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { UserScriptGenerator } from './Generation/UserScriptGenerator'; import { EventSource } from '@/infrastructure/Events/EventSource'; import { IApplicationCode } from './IApplicationCode'; @@ -16,7 +16,7 @@ export class ApplicationCode implements IApplicationCode { private scriptPositions = new Map(); constructor( - userSelection: IUserSelection, + userSelection: IReadOnlyUserSelection, private readonly scriptingDefinition: IScriptingDefinition, private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) { if (!userSelection) { throw new Error('userSelection is null or undefined'); } diff --git a/src/application/Context/State/Filter/IUserFilter.ts b/src/application/Context/State/Filter/IUserFilter.ts index 6de39103..1cec5486 100644 --- a/src/application/Context/State/Filter/IUserFilter.ts +++ b/src/application/Context/State/Filter/IUserFilter.ts @@ -1,10 +1,13 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IFilterResult } from './IFilterResult'; -export interface IUserFilter { +export interface IReadOnlyUserFilter { readonly currentFilter: IFilterResult | undefined; readonly filtered: IEventSource; readonly filterRemoved: IEventSource; +} + +export interface IUserFilter extends IReadOnlyUserFilter { setFilter(filter: string): void; removeFilter(): void; } diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index af846bc6..01664bc8 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -1,13 +1,18 @@ -import { IUserFilter } from './Filter/IUserFilter'; -import { IUserSelection } from './Selection/IUserSelection'; +import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter'; +import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection'; import { IApplicationCode } from './Code/IApplicationCode'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; -export interface ICategoryCollectionState { +export interface IReadOnlyCategoryCollectionState { readonly code: IApplicationCode; + readonly os: OperatingSystem; + readonly filter: IReadOnlyUserFilter; + readonly selection: IReadOnlyUserSelection; + readonly collection: ICategoryCollection; +} + +export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { readonly filter: IUserFilter; readonly selection: IUserSelection; - readonly collection: ICategoryCollection; - readonly os: OperatingSystem; } diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts index 0e23e7b3..1c5d7229 100644 --- a/src/application/Context/State/Selection/IUserSelection.ts +++ b/src/application/Context/State/Selection/IUserSelection.ts @@ -3,18 +3,21 @@ import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; import { IEventSource } from '@/infrastructure/Events/IEventSource'; -export interface IUserSelection { +export interface IReadOnlyUserSelection { readonly changed: IEventSource>; readonly selectedScripts: ReadonlyArray; + isSelected(scriptId: string): boolean; areAllSelected(category: ICategory): boolean; isAnySelected(category: ICategory): boolean; +} + +export interface IUserSelection extends IReadOnlyUserSelection { removeAllInCategory(categoryId: number): void; addOrUpdateAllInCategory(categoryId: number, revert: boolean): void; addSelectedScript(scriptId: string, revert: boolean): void; addOrUpdateSelectedScript(scriptId: string, revert: boolean): void; removeSelectedScript(scriptId: string): void; selectOnly(scripts: ReadonlyArray): void; - isSelected(scriptId: string): boolean; selectAll(): void; deselectAll(): void; } diff --git a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue index e0c5d5ff..871df1f1 100644 --- a/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue +++ b/src/presentation/components/Code/CodeButtons/TheCodeButtons.vue @@ -32,13 +32,13 @@ import Dialog from '@/presentation/components/Shared/Dialog.vue'; import IconButton from './IconButton.vue'; import MacOsInstructions from './MacOsInstructions.vue'; import { Environment } from '@/application/Environment/Environment'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { CodeRunner } from '@/infrastructure/CodeRunner'; -import { IApplicationContext } from '@/application/Context/IApplicationContext'; +import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext'; @Component({ components: { @@ -70,7 +70,7 @@ export default class TheCodeButtons extends StatefulVue { await executeCode(context); } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os; this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS; this.fileName = buildFileName(newState.collection.scripting); @@ -91,7 +91,7 @@ export default class TheCodeButtons extends StatefulVue { } } -function saveCode(fileName: string, state: ICategoryCollectionState) { +function saveCode(fileName: string, state: IReadOnlyCategoryCollectionState) { const content = state.code.current; const type = getType(state.collection.scripting.language); SaveFileDialog.saveFile(content, fileName, type); @@ -115,7 +115,7 @@ function buildFileName(scripting: IScriptingDefinition) { return fileName; } -async function executeCode(context: IApplicationContext) { +async function executeCode(context: IReadOnlyApplicationContext) { const runner = new CodeRunner(); await runner.runCode( /*code*/ context.state.code.current, diff --git a/src/presentation/components/Code/TheCodeArea.vue b/src/presentation/components/Code/TheCodeArea.vue index 0203a424..ee76f4e2 100644 --- a/src/presentation/components/Code/TheCodeArea.vue +++ b/src/presentation/components/Code/TheCodeArea.vue @@ -17,7 +17,7 @@ import 'ace-builds/webpack-resolver'; import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent'; import { IScript } from '@/domain/IScript'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory'; import Responsive from '@/presentation/components/Shared/Responsive.vue'; import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective'; @@ -45,7 +45,7 @@ export default class TheCodeArea extends StatefulVue { } } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.destroyEditor(); this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language); const appCode = newState.code; diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts index 2e630d08..456f6a30 100644 --- a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts +++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts @@ -2,7 +2,7 @@ import { IScript } from '@/domain/IScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { scrambledEqual } from '@/application/Common/Array'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; export enum SelectionType { Standard, @@ -34,7 +34,7 @@ export class SelectionTypeHandler { } interface ISingleTypeHandler { - isSelected: (state: ICategoryCollectionState) => boolean; + isSelected: (state: IReadOnlyCategoryCollectionState) => boolean; select: (state: ICategoryCollectionState) => void; } @@ -62,7 +62,7 @@ function getRecommendationLevelSelector(level: RecommendationLevel): ISingleType }; } -function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) { +function hasAllSelectedLevelOf(level: RecommendationLevel, state: IReadOnlyCategoryCollectionState) { const scripts = state.collection.getScriptsByLevel(level); const selectedScripts = state.selection.selectedScripts; return areAllSelected(scripts, selectedScripts); diff --git a/src/presentation/components/Scripts/Menu/TheOsChanger.vue b/src/presentation/components/Scripts/Menu/TheOsChanger.vue index 53251193..8bcd4e13 100644 --- a/src/presentation/components/Scripts/Menu/TheOsChanger.vue +++ b/src/presentation/components/Scripts/Menu/TheOsChanger.vue @@ -13,7 +13,7 @@ import { Component } from 'vue-property-decorator'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ApplicationFactory } from '@/application/ApplicationFactory'; import MenuOptionList from './MenuOptionList.vue'; import MenuOptionListItem from './MenuOptionListItem.vue'; @@ -38,7 +38,7 @@ export default class TheOsChanger extends StatefulVue { context.changeContext(newOs); } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.currentOs = newState.os; this.$forceUpdate(); // v-bind:class is not updated otherwise } diff --git a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue index 25fc669e..b7764b0a 100644 --- a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue +++ b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue @@ -15,7 +15,7 @@ import TheOsChanger from './TheOsChanger.vue'; import TheSelector from './Selector/TheSelector.vue'; import TheViewChanger from './View/TheViewChanger.vue'; import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { IEventSubscription } from '@/infrastructure/Events/IEventSource'; @Component({ @@ -37,11 +37,11 @@ export default class TheScriptsMenu extends StatefulVue { protected initialize(): void { return; } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.subscribe(newState); } - private subscribe(state: ICategoryCollectionState) { + private subscribe(state: IReadOnlyCategoryCollectionState) { this.listeners.push(state.filter.filterRemoved.on(() => { this.isSearching = false; })); diff --git a/src/presentation/components/Scripts/View/Cards/CardList.vue b/src/presentation/components/Scripts/View/Cards/CardList.vue index c65adf17..95aa225a 100644 --- a/src/presentation/components/Scripts/View/Cards/CardList.vue +++ b/src/presentation/components/Scripts/View/Cards/CardList.vue @@ -31,7 +31,7 @@ import { Component } from 'vue-property-decorator'; import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; import { ICategory } from '@/domain/ICategory'; import { hasDirective } from './NonCollapsingDirective'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; @Component({ components: { @@ -56,7 +56,7 @@ export default class CardList extends StatefulVue { this.activeCategoryId = isExpanded ? categoryId : undefined; } - protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.setCategories(newState.collection.actions); this.activeCategoryId = undefined; } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue index f2c2f089..57f76a81 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue @@ -19,7 +19,7 @@ import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; import { INode } from './INode'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { getReverter } from './Reverter/ReverterFactory'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; @Component export default class RevertToggle extends StatefulVue { @@ -37,7 +37,7 @@ export default class RevertToggle extends StatefulVue { this.handler.selectWithRevertState(this.isReverted, context.state.selection); } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.updateStatus(newState.selection.selectedScripts); this.events.unsubscribeAll(); this.events.register(newState.selection.changed.on((scripts) => this.updateStatus(scripts))); diff --git a/src/presentation/components/Scripts/View/TheScriptsView.vue b/src/presentation/components/Scripts/View/TheScriptsView.vue index f045a135..3f915875 100644 --- a/src/presentation/components/Scripts/View/TheScriptsView.vue +++ b/src/presentation/components/Scripts/View/TheScriptsView.vue @@ -36,7 +36,7 @@ import { Component, Prop } from 'vue-property-decorator'; import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { ApplicationFactory } from '@/application/ApplicationFactory'; /** Shows content of single category or many categories */ @@ -74,12 +74,12 @@ export default class TheScriptsView extends StatefulVue { filter.removeFilter(); } - protected handleCollectionState(newState: ICategoryCollectionState): void { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { this.events.unsubscribeAll(); this.subscribeState(newState); } - private subscribeState(state: ICategoryCollectionState) { + private subscribeState(state: IReadOnlyCategoryCollectionState) { this.events.register( state.filter.filterRemoved.on(() => { this.isSearching = false; diff --git a/src/presentation/components/Shared/StatefulVue.ts b/src/presentation/components/Shared/StatefulVue.ts index 2f761d99..fd4dc8da 100644 --- a/src/presentation/components/Shared/StatefulVue.ts +++ b/src/presentation/components/Shared/StatefulVue.ts @@ -3,7 +3,7 @@ import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { buildContext } from '@/application/Context/ApplicationContextFactory'; import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection'; // @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91 @@ -26,7 +26,8 @@ export abstract class StatefulVue extends Vue { } protected abstract handleCollectionState( - newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void; + newState: IReadOnlyCategoryCollectionState, + oldState: IReadOnlyCategoryCollectionState | undefined): void; protected getCurrentContext(): Promise { return StatefulVue.instance.getValue(); } diff --git a/src/presentation/components/TheSearchBar.vue b/src/presentation/components/TheSearchBar.vue index 968bb116..28750423 100644 --- a/src/presentation/components/TheSearchBar.vue +++ b/src/presentation/components/TheSearchBar.vue @@ -13,9 +13,9 @@ import { Component, Watch } from 'vue-property-decorator'; import { StatefulVue } from '@/presentation/components/Shared/StatefulVue'; import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective'; -import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter'; +import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter'; import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; -import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; +import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; @Component( { directives: { NonCollapsing }, @@ -36,7 +36,7 @@ export default class TheSearchBar extends StatefulVue { } } - protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) { + protected handleCollectionState(newState: IReadOnlyCategoryCollectionState) { const totalScripts = newState.collection.totalScripts; this.searchPlaceHolder = `Search in ${totalScripts} scripts`; this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : ''; @@ -44,7 +44,7 @@ export default class TheSearchBar extends StatefulVue { this.subscribeFilter(newState.filter); } - private subscribeFilter(filter: IUserFilter) { + private subscribeFilter(filter: IReadOnlyUserFilter) { this.events.register(filter.filtered.on((result) => this.handleFiltered(result))); this.events.register(filter.filterRemoved.on(() => this.handleFilterRemoved())); }