Skip to content

Commit

Permalink
Test and document abstract graphs (#181)
Browse files Browse the repository at this point in the history
Abstract graphs is a handy approach to declare common dependencies that are shared by multiple graphs.
  • Loading branch information
guyca authored Oct 3, 2024
1 parent c74be15 commit 2ed1851
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 1 deletion.
74 changes: 73 additions & 1 deletion packages/documentation/docs/documentation/usage/Graphs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ sidebar_position: 1
tags: [Graph, Lifecycle-bound]
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

## Introduction

In Object Oriented Programming, programs are organized around objects, where each object has a specific purpose. These objects can require other objects to perform their responsibilities. The required objects are called dependencies. Providing these dependencies manually is a tedious and error-prone process. The dependency injection pattern is a way to automate this process so you can focus on the logic of your application instead of writing boilerplate code.
Expand Down Expand Up @@ -146,7 +149,8 @@ Lifecycle-bound graphs are created when they are requested and are destroyed whe
## Graph composition
Graph composition is a powerful feature that allows you to create complex dependency graphs by combining smaller graphs. Composing graphs is useful when you want to reuse a graph in multiple places. For example, you might have a singleton graph that provides application-level dependencies. You might also have a lifecycle-bound graph that provides dependencies for a specific UI flow. You can compose these graphs together so that the lifecycle-bound graph can also inject the dependencies provided by the singleton graph.

To compose graphs, pass a `subgraphs` array to the `@Graph` decorator. The `subgraphs` array contains the graphs you want to "include" in your graph.
### Subgraphs
The most common method to compose graphs is to pass a `subgraphs` array to the `@Graph` decorator. The `subgraphs` array contains the graphs you want to "include" in your graph.

In the example below we declared a lifecycle-bound graph called `LoginGraph`. This graph provides a single dependency called `loginService` which has a dependency on `httpClient`. Since `httpClient` is exposed via the `ApplicationGraph`, we included it in the `subgraphs` array of our graph.

Expand All @@ -164,6 +168,74 @@ export class LoginGraph extends ObjectGraph {
}
```

### Abstract graphs
Abstract graphs are graphs that are not instantiated directly. Instead, they are used as a base for other graphs. Abstract graphs are useful when you want to define a set of dependencies that are shared between multiple graphs.

In the example below we declared an abstract graph called `ScreenGraph`. This graph provides a single dependency called `screenLogger` which is used to log messages from the screen. We want to show the name of the screen in the log messages, so the `ScreenLogger` requires the name of the screen as a constructor argument.

The `screenName` provider method is marked as `abstract` which means that it must be implemented by the parent class. This allows us to create multiple graphs that extend the `ScreenGraph` and provide the screen name.

```ts title="AbstractGraph.ts"
import {Graph, ObjectGraph, Provides} from 'react-obsidian';

export abstract class ScreenGraph extends ObjectGraph {
@Provides()
screenLogger(screenName: string) {
return new ScreenLogger(screenName);
}

// highlight-next-line
abstract screenName(): string; // This method must be implemented by the parent graphs
}
```

The following two graphs extend the base `ScreenGraph`. Each graph provides a different screen name and a service that is specific to that screen.

<Tabs>
<TabItem value="HomeGraph" label="HomeGraph">

```ts title="HomeGraph.ts"
import {Graph, ObjectGraph, Provides} from 'react-obsidian';

@Graph()
export class HomeGraph extends ScreenGraph {
@Provides()
override screenName() {
return 'HomeScreen';
}

@Provides()
homeService(screenLogger: ScreenLogger): HomeService {
return new HomeService(screenLogger);
}
}
```
</TabItem>
<TabItem value="ProfileGraph" label="ProfileGraph">

```ts title="ProfileGraph.ts"
import {Graph, ObjectGraph, Provides} from 'react-obsidian';

@Graph()
export class ProfileGraph extends ScreenGraph {
@Provides()
override screenName() {
return 'ProfileScreen';
}

@Provides()
profileService(screenLogger: ScreenLogger): ProfileService {
return new ProfileService(screenLogger);
}
}
```
</TabItem>
</Tabs>

:::note
Because abstract graphs aren't instantiated directly, they don't need to be annotated with the `@Graph` decorator. Abstract providers aren't annotated with the `@Provides` decorator for the same reason.
:::

## Typed dependencies
The `DependenciesOf` utility type creates a new type consisting the dependencies provided by a graph. This type can be used to type the dependencies of hooks or props required by components. This utility type takes two arguments: the graph and a union of the keys of the dependencies we want to inject.

Expand Down
57 changes: 57 additions & 0 deletions packages/react-obsidian/test/acceptance/abstractGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Graph,
ObjectGraph,
Obsidian,
Provides,
} from '../../src';

describe('abstract graph', () => {
it('should be able to create a graph', () => {
expect(new FooGraph()).toBeInstanceOf(FooGraph);
});

it('should be able to provide a value', () => {
expect(Obsidian.obtain(FooGraph).atomicDependency()).toBe('foo');
});

it('should be able to provide a composite value', () => {
expect(Obsidian.obtain(FooGraph).compositeDependency()).toBe('foobar');
});

it('should provide dependencies that depend on abstract dependencies', () => {
expect(Obsidian.obtain(FooGraph).dependsOnAbstractDependency()).toBe('depends on baz');
});
});


abstract class AbstractGraph extends ObjectGraph {
@Provides()
compositeDependency(atomicDependency: string, bar: string) {
return atomicDependency + bar;
}

@Provides()
atomicDependency() {
return 'foo';
}

@Provides()
dependsOnAbstractDependency(baz: string) {
return `depends on ${baz}`;
}

abstract baz(): string;
}

@Graph()
class FooGraph extends AbstractGraph {
@Provides()
bar() {
return 'bar';
}

@Provides()
override baz() {
return 'baz';
}
}

0 comments on commit 2ed1851

Please sign in to comment.