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.
This commit is contained in:
undergroundwires
2021-12-24 21:14:27 +01:00
parent a1871a2982
commit c3c5b897f3
16 changed files with 58 additions and 42 deletions

View File

@@ -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<IApplicationContextChangedEvent>;
}
export interface IApplicationContext extends IReadOnlyApplicationContext {
readonly state: ICategoryCollectionState;
changeContext(os: OperatingSystem): void;
}

View File

@@ -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 {

View File

@@ -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<SelectedScript, CodePosition>();
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'); }

View File

@@ -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<IFilterResult>;
readonly filterRemoved: IEventSource<void>;
}
export interface IUserFilter extends IReadOnlyUserFilter {
setFilter(filter: string): void;
removeFilter(): void;
}

View File

@@ -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;
}

View File

@@ -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<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
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<IScript>): void;
isSelected(scriptId: string): boolean;
selectAll(): void;
deselectAll(): void;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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;
}));

View File

@@ -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;
}

View File

@@ -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)));

View File

@@ -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;

View File

@@ -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<IApplicationContext> {
return StatefulVue.instance.getValue();
}

View File

@@ -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()));
}