Skip to content

Commit

Permalink
Refactor DI for simplicity and type safety
Browse files Browse the repository at this point in the history
This commit improves the dependency injection mechanism by introducing a
custom `injectKey` function.

Key improvements are:

- Enforced type consistency during dependency registration and
  instantiation.
- Simplified injection process, abstracting away the complexity with a
  uniform API, regardless of the dependency's lifetime.
- Eliminated the possibility of `undefined` returns during dependency
  injection, promoting fail-fast behavior.
- Removed the necessity for type casting to `symbol` for injection keys
  in unit tests by using existing types.
- Consalidated imports, combining keys and injection functions in one
  `import` statement.
  • Loading branch information
undergroundwires committed Nov 9, 2023
1 parent aab0f7e commit 7770a9b
Show file tree
Hide file tree
Showing 51 changed files with 398 additions and 177 deletions.
9 changes: 5 additions & 4 deletions docs/presentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ To add a new dependency:
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
- **Singletons**: Shared across components, instantiated once.
- **Transients**: Factories yielding a new instance on every access.
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
- For singletons, invoke the factory method: `inject(symbolKey)()`.
- For transients, directly inject: `inject(symbolKey)`.
2. **Provide the dependency**:
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency.
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies.
- Example usage: `injectKey((keys) => keys.useCollectionState)`;

## Shared UI components

Expand Down
93 changes: 72 additions & 21 deletions src/presentation/bootstrapping/DependencyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,89 @@ import { InjectionKey, provide, inject } from 'vue';
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
import { useApplication } from '@/presentation/components/Shared/Hooks/UseApplication';
import { useAutoUnsubscribedEvents } from '@/presentation/components/Shared/Hooks/UseAutoUnsubscribedEvents';
import { useClipboard } from '@/presentation/components/Shared/Hooks/Clipboard/UseClipboard';
import { useCurrentCode } from '@/presentation/components/Shared/Hooks/UseCurrentCode';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { useClipboard } from '../components/Shared/Hooks/Clipboard/UseClipboard';
import { useCurrentCode } from '../components/Shared/Hooks/UseCurrentCode';
import {
AnyLifetimeInjectionKey, InjectionKeySelector, InjectionKeys, SingletonKey,
TransientKey, injectKey,
} from '@/presentation/injectionSymbols';
import { PropertyKeys } from '@/TypeHelpers';

export function provideDependencies(
context: IApplicationContext,
api: VueDependencyInjectionApi = { provide, inject },
) {
const registerSingleton = <T>(key: InjectionKey<T>, value: T) => api.provide(key, value);
const registerTransient = <T>(
key: InjectionKey<() => T>,
factory: () => T,
) => api.provide(key, factory);
const resolvers: Record<PropertyKeys<typeof InjectionKeys>, (di: DependencyRegistrar) => void> = {
useCollectionState: (di) => di.provide(
InjectionKeys.useCollectionState,
() => {
const { events } = di.injectKey((keys) => keys.useAutoUnsubscribedEvents);
return useCollectionState(context, events);
},
),
useApplication: (di) => di.provide(
InjectionKeys.useApplication,
useApplication(context.app),
),
useRuntimeEnvironment: (di) => di.provide(
InjectionKeys.useRuntimeEnvironment,
RuntimeEnvironment.CurrentEnvironment,
),
useAutoUnsubscribedEvents: (di) => di.provide(
InjectionKeys.useAutoUnsubscribedEvents,
useAutoUnsubscribedEvents,
),
useClipboard: (di) => di.provide(
InjectionKeys.useClipboard,
useClipboard,
),
useCurrentCode: (di) => di.provide(
InjectionKeys.useCurrentCode,
() => {
const { events } = di.injectKey((keys) => keys.useAutoUnsubscribedEvents);
const state = di.injectKey((keys) => keys.useCollectionState);
return useCurrentCode(state, events);
},
),
};
registerAll(Object.values(resolvers), api);
}

registerSingleton(InjectionKeys.useApplication, useApplication(context.app));
registerSingleton(InjectionKeys.useRuntimeEnvironment, RuntimeEnvironment.CurrentEnvironment);
registerTransient(InjectionKeys.useAutoUnsubscribedEvents, () => useAutoUnsubscribedEvents());
registerTransient(InjectionKeys.useCollectionState, () => {
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
return useCollectionState(context, events);
});
registerTransient(InjectionKeys.useClipboard, () => useClipboard());
registerTransient(InjectionKeys.useCurrentCode, () => {
const { events } = api.inject(InjectionKeys.useAutoUnsubscribedEvents)();
const state = api.inject(InjectionKeys.useCollectionState)();
return useCurrentCode(state, events);
});
function registerAll(
registrations: ReadonlyArray<(di: DependencyRegistrar) => void>,
api: VueDependencyInjectionApi,
) {
const registrar = new DependencyRegistrar(api);
Object.values(registrations).forEach((register) => register(registrar));
}

export interface VueDependencyInjectionApi {
provide<T>(key: InjectionKey<T>, value: T): void;
inject<T>(key: InjectionKey<T>): T;
}

class DependencyRegistrar {
constructor(private api: VueDependencyInjectionApi) {}

public provide<T>(
key: TransientKey<T>,
resolver: () => T,
): void;
public provide<T>(
key: SingletonKey<T>,
resolver: T,
): void;
public provide<T>(
key: AnyLifetimeInjectionKey<T>,
resolver: T | (() => T),
): void {
this.api.provide(key.key, resolver);
}

public injectKey<T>(key: InjectionKeySelector<T>): T {
const injector = this.api.inject.bind(this.api);
return injectKey(key, injector);
}
}
10 changes: 4 additions & 6 deletions src/presentation/components/Code/CodeButtons/CodeCopyButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@
</template>

<script lang="ts">
import {
defineComponent, inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { defineComponent } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import IconButton from './IconButton.vue';
export default defineComponent({
components: {
IconButton,
},
setup() {
const { copyText } = inject(InjectionKeys.useClipboard)();
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
const { copyText } = injectKey((keys) => keys.useClipboard);
const { currentCode } = injectKey((keys) => keys.useCurrentCode);
async function copyCode() {
await copyText(currentCode.value);
Expand Down
10 changes: 4 additions & 6 deletions src/presentation/components/Code/CodeButtons/CodeRunButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
</template>

<script lang="ts">
import {
defineComponent, computed, inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
Expand All @@ -22,8 +20,8 @@ export default defineComponent({
IconButton,
},
setup() {
const { currentState, currentContext } = inject(InjectionKeys.useCollectionState)();
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState);
const { os, isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

<script lang="ts">
import {
defineComponent, ref, computed, inject,
defineComponent, ref, computed,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
Expand All @@ -34,8 +34,8 @@ export default defineComponent({
ModalDialog,
},
setup() {
const { currentState } = inject(InjectionKeys.useCollectionState)();
const { isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
const { currentState } = injectKey((keys) => keys.useCollectionState);
const { isDesktop } = injectKey((keys) => keys.useRuntimeEnvironment);
const areInstructionsVisible = ref(false);
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
</template>

<script lang="ts">
import { defineComponent, shallowRef, inject } from 'vue';
import { defineComponent, shallowRef } from 'vue';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
export default defineComponent({
components: {
TooltipWrapper,
AppIcon,
},
setup() {
const { copyText } = inject(InjectionKeys.useClipboard)();
const { copyText } = injectKey((keys) => keys.useClipboard);
const codeElement = shallowRef<HTMLElement | undefined>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@
<script lang="ts">
import {
defineComponent, PropType, computed,
inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
Expand All @@ -79,7 +78,7 @@ export default defineComponent({
},
},
setup(props) {
const { info } = inject(InjectionKeys.useApplication);
const { info } = injectKey((keys) => keys.useApplication);
const appName = computed<string>(() => info.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

<script lang="ts">
import {
defineComponent, computed, inject,
defineComponent, computed,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import CodeRunButton from './CodeRunButton.vue';
import CodeCopyButton from './CodeCopyButton.vue';
import CodeSaveButton from './Save/CodeSaveButton.vue';
Expand All @@ -22,7 +22,7 @@ export default defineComponent({
CodeSaveButton,
},
setup() {
const { currentCode } = inject(InjectionKeys.useCurrentCode)();
const { currentCode } = injectKey((keys) => keys.useCurrentCode);
const hasCode = computed<boolean>(() => currentCode.value.length > 0);
Expand Down
8 changes: 4 additions & 4 deletions src/presentation/components/Code/TheCodeArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

<script lang="ts">
import {
defineComponent, onUnmounted, onMounted, inject,
defineComponent, onUnmounted, onMounted,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
import { IScript } from '@/domain/IScript';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
Expand All @@ -37,8 +37,8 @@ export default defineComponent({
NonCollapsing,
},
setup(props) {
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const editorId = 'codeEditor';
let editor: ace.Ace.Editor | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
</template>

<script lang="ts">
import { defineComponent, ref, inject } from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { defineComponent, ref } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
Expand All @@ -81,8 +81,8 @@ export default defineComponent({
TooltipWrapper,
},
setup() {
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const { modifyCurrentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const currentSelection = ref(SelectionType.None);
Expand Down
10 changes: 4 additions & 6 deletions src/presentation/components/Scripts/Menu/TheOsChanger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
</template>

<script lang="ts">
import {
defineComponent, computed, inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import MenuOptionList from './MenuOptionList.vue';
import MenuOptionListItem from './MenuOptionListItem.vue';
Expand All @@ -30,8 +28,8 @@ export default defineComponent({
MenuOptionListItem,
},
setup() {
const { modifyCurrentContext, currentState } = inject(InjectionKeys.useCollectionState)();
const { application } = inject(InjectionKeys.useApplication);
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
const { application } = injectKey((keys) => keys.useApplication);
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
application.getSupportedOsList() ?? [])
Expand Down
10 changes: 4 additions & 6 deletions src/presentation/components/Scripts/Menu/TheScriptsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
</template>

<script lang="ts">
import {
defineComponent, ref, inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { defineComponent, ref } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
import TheOsChanger from './TheOsChanger.vue';
Expand All @@ -27,8 +25,8 @@ export default defineComponent({
TheViewChanger,
},
setup() {
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const { onStateChange } = injectKey((keys) => keys.useCollectionState);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const isSearching = ref(false);
Expand Down
5 changes: 2 additions & 3 deletions src/presentation/components/Scripts/View/Cards/CardList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@
<script lang="ts">
import {
defineComponent, ref, onMounted, onUnmounted, computed,
inject,
} from 'vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
import { hasDirective } from './NonCollapsingDirective';
import CardListItem from './CardListItem.vue';
Expand All @@ -48,7 +47,7 @@ export default defineComponent({
SizeObserver,
},
setup() {
const { currentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
const width = ref<number>(0);
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@

<script lang="ts">
import {
defineComponent, ref, watch, computed,
inject, shallowRef,
defineComponent, ref, watch, computed, shallowRef,
} from 'vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { injectKey } from '@/presentation/injectionSymbols';
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
Expand All @@ -78,8 +77,8 @@ export default defineComponent({
/* eslint-enable @typescript-eslint/no-unused-vars */
},
setup(props, { emit }) {
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
const { onStateChange, currentState } = injectKey((keys) => keys.useCollectionState);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const isExpanded = computed({
get: () => {
Expand Down

0 comments on commit 7770a9b

Please sign in to comment.