Skip to main content

clean-architecture-skimdown

SkimDown for Windows でクリーンアーキテクチャー風のレイアウト・DI・依存方向に従って新規サービス / I/F / ViewModel を追加または既存コードをリファクタする時に使う。トリガー語の例 - "新しい I/F を追加", "サービスを足す", "ViewModel を作る", "DI 登録", "層境界", "Application から System.IO を呼びたい", "Infrastructure 実装", "Singleton か Scoped か"。設計判断の根拠は ADR-0002 を参照。

Informations de source

Dépôt
runceel/SkimDownForWindows
Dernière activité de la source
1 juin 2026 à 10:30
Langue détectée de SKILL.md
japonais
Étoiles
6
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
clean-architecture-skimdown
description
SkimDown for Windows でクリーンアーキテクチャー風のレイアウト・DI・依存方向に従って新規サービス / I/F / ViewModel を追加または既存コードをリファクタする時に使う。トリガー語の例 - "新しい I/F を追加", "サービスを足す", "ViewModel を作る", "DI 登録", "層境界", "Application から System.IO を呼びたい", "Infrastructure 実装", "Singleton か Scoped か"。設計判断の根拠は ADR-0002 を参照。
# Clean Architecture for SkimDown for Windows ## どんな時に使うか - Application 層に新しい抽象 (`I<X>`) を追加する時 - Infrastructure 層に新しい実装 (`LocalXxx` / `WindowsXxx`) を追加する時 - ViewModel から外部 I/O を呼ぶ必要が出てきた時 - `static` のシングルトン / グローバルキャッシュを書きたくなった時 - PR レビューで「これは Application に置くべきか Presentation か」を判定する時 判断の正当性 (なぜこの構造になっているか) は **[ADR-0002](../../adr/0002-clean-architecture-layered-projects.md)** を参照。skill はそこで決まったことを実装時に**思い出すための圧縮表**。 ## レイヤーマップ ``` Presentation (SkimDownForWindows, net10.0-windows10.0.26100.0) - XAML / Page / Window / UserControl - コンポジションルート (App.xaml.cs, MainWindow.xaml.cs) - WindowsAppSDK 依存実装 (DispatcherQueueUiDispatcher, WindowService) - UI 文字列リソース (Strings/<locale>/Resources.resw + ResourceLoader) │ ├──> Infrastructure (SkimDownForWindows.Infrastructure, net10.0-windows10.0.26100.0) │ - LocalFileSystem, LocalMarkdownFileReader, FileSystemFolderWatcher, │ JsonSettingsRepository, FileAppLogger (IO/ 配下) │ - WindowsClipboardService, ExplorerShellService, │ UiSettingsThemeProvider, LauncherExternalUriService (Windows/ 配下) │ - WinRT + System.IO のみ参照。Microsoft.WindowsAppSDK は不参照 │ └──> Application (SkimDownForWindows.Application, net10.0) - Abstractions/ : I<X> 抽象 (11 個) - Markdown/ : MarkdownScanner, MarkdownTreeBuilder, InitialSelectionPicker, LinkResolver (純粋サービス) - ViewModels/ : MainPageViewModel : IDisposable - Models/ : AppSettings, FolderState, MarkdownTreeItem, LoadRequest, RecentFolderEntry - CommandLine/ : CommandLineLauncher - Utilities/ : PathHelpers (純粋 static) - DependencyInjection/ : AddSkimDownApplication 拡張メソッド │ └──> Domain (SkimDownForWindows.Domain, net10.0) - AppTheme, SidebarPosition, LinkKind, LinkClassification - 副作用ゼロ。依存ゼロ。 ``` ## 依存方向は内向きのみ ``` Presentation ──> Infrastructure ──> Application ──> Domain ↑ └──── 逆方向参照は禁止 ``` - Application は **Infrastructure を知らない**。`Microsoft.Extensions.DependencyInjection.Abstractions` 経由でしか具象を知らない - Infrastructure は Application の抽象を実装するためだけに Application を参照する - Domain は何にも依存しない ## 必ず守るルール (圧縮版) > 詳細は [copilot-instructions.md の「必ず守るルール」](../../copilot-instructions.md) を参照。本 skill ではコーディング時に必要な要点のみ。 1. **Application / ViewModel から外部 I/O 型を `using` しない** - 禁止: `System.IO.File`, `System.IO.Directory`, `System.Diagnostics.Process`, `Windows.UI.ViewManagement.*`, `Windows.ApplicationModel.DataTransfer.*`, `Windows.ApplicationModel.Resources.*`, `Windows.Storage.*`, `Windows.System.Launcher` - 必ず `I<X>` 抽象経由で呼ぶ - 例外: `Application/Utilities/PathHelpers` は `System.IO.Path` の**純粋計算 API のみ**使用可 - `Windows.ApplicationModel.Resources` (`ResourceLoader`) は **Presentation 専有** (ADR-0006)。Application 層は UI 文字列を保持せず、Page 側のコードビハインドで解決する 2. **可変状態を持つ `static` シングルトンを書かない** - `static MyService Instance => ...` / `static class WindowManager` 禁止 - DI Singleton 登録で置き換える - 純粋ヘルパー (`static class PathHelpers` 内の `IsMarkdownFile` 等) は可 3. **WebView2 への入力は `PostWebMessageAsJson` のみ** - `NavigateToString` 禁止 (SPEC: 二重 origin 分離の維持) - 外部リンクは `IExternalUriLauncher` 経由 4. **新規サービスは抽象を Application、実装を Infrastructure に置く** - Infrastructure は `Microsoft.WindowsAppSDK` を参照しない - WindowsAppSDK に依存する実装 (Dispatcher / Window 系) は **Presentation の `Composition/` 配下**に置く 5. **XAML / Page / UserControl / ResourceDictionary は App プロジェクトに閉じる** - クラスライブラリに `.xaml` を置かない 6. **UI 文字列は `Strings/<locale>/Resources.resw` に置く** (ADR-0006) - XAML は `x:Uid="<UidName>"`、resw は `<UidName>.<PropertyName>` で書く - 動的文字列はコードビハインドで `ResourceLoader.GetForViewIndependentUse().GetString("Category/Key")` で取得 - a11y 文言 (`AutomationProperties.Name`, `ToolTipService.ToolTip`) も resw に書く (ハードコード禁止) ## 新規 I/F を追加する 4 ステップ ファイル I/O 系の例 (たとえば「ファイルに書き込みたい」)。 ### Step 1: Application に抽象を追加 ```csharp // src/SkimDownForWindows.Application/Abstractions/IXxxWriter.cs namespace SkimDownForWindows.Application.Abstractions; /// <summary>...用途を 1-2 行で書く...</summary> public interface IXxxWriter { Task WriteAsync(string path, string content, CancellationToken ct = default); } ``` - 名前空間 = `SkimDownForWindows.Application.Abstractions` - ファイル名 = I/F 名と一致 (`IXxxWriter.cs`) - メンバーには XML doc コメント (用途・例外動作・しきい値) - 必要なら `IDisposable` を継承 (リソース保持型) ### Step 2: Infrastructure に実装を追加 ```csharp // src/SkimDownForWindows.Infrastructure/IO/LocalXxxWriter.cs using SkimDownForWindows.Application.Abstractions; namespace SkimDownForWindows.Infrastructure.IO; internal sealed class LocalXxxWriter : IXxxWriter { ... } ``` - IO 系は `Infrastructure/IO/`、WinRT 系は `Infrastructure/Windows/` - 名前空間 = `SkimDownForWindows.Infrastructure.IO` または `.Windows` - クラスは `internal sealed`、DI 経由でのみ生成される ### Step 3: DI 登録 ```csharp // src/SkimDownForWindows.Infrastructure/DependencyInjection/InfrastructureServiceCollectionExtensions.cs public static IServiceCollection AddSkimDownInfrastructure(this IServiceCollection services) { ... services.AddSingleton<IXxxWriter, LocalXxxWriter>(); // または AddScoped return services; } ``` - 抽象 → 実装で登録 - ライフサイクル選択は次節を参照 ### Step 4: 利用側 (ViewModel など) で DI 解決 - コンストラクタインジェクションで受け取る (`new` しない) - フィールドは `private readonly` ```csharp public MainPageViewModel(..., IXxxWriter xxxWriter, ...) { _xxxWriter = xxxWriter; } ``` ## DI ライフサイクル早見表 | サービス種別 | ライフサイクル | 例 | |---|---|---| | 状態を持たない / プロセス全体で 1 つで OK | **Singleton** | `IFileSystem`, `IMarkdownFileReader`, `ISettingsRepository`, `IClipboardService`, `IShellService`, `ISystemThemeProvider`, `IExternalUriLauncher`, `IAppLogger` | | ウィンドウ寿命に紐づく / `IDisposable` で resource を持つ | **Scoped** | `IFolderWatcher`, `MainPageViewModel`, `MarkdownScanner`, `MarkdownTreeBuilder`, `InitialSelectionPicker`, `LinkResolver`, `CommandLineLauncher` | | WindowsAppSDK 依存 (Dispatcher / Window) | Presentation 側で **Singleton** | `IUiDispatcher`, `IWindowService` | > Scoped を選ぶときは `MainWindow.Closed` で `IServiceScope.Dispose()` が呼ばれることを前提にしている。Scoped を破ると watcher のイベント購読がリークするので注意。 ## ファイル配置の早見表 | 種類 | 配置 | 例 | |---|---|---| | 値オブジェクト・enum (依存ゼロ) | `SkimDownForWindows.Domain/` | `AppTheme.cs`, `LinkClassification.cs` | | 抽象 I/F | `SkimDownForWindows.Application/Abstractions/` | `IFileSystem.cs`, `IFolderWatcher.cs` | | 永続化 DTO / UI バインドモデル | `SkimDownForWindows.Application/Models/` | `AppSettings.cs`, `MarkdownTreeItem.cs` | | 純粋サービス (副作用ゼロ or `IFileSystem` のみ) | `SkimDownForWindows.Application/Markdown/` | `MarkdownScanner.cs`, `LinkResolver.cs` | | ViewModel | `SkimDownForWindows.Application/ViewModels/` | `MainPageViewModel.cs` | | パスユーティリティ (`static class`) | `SkimDownForWindows.Application/Utilities/` | `PathHelpers.cs` | | コマンドライン引数の解釈 | `SkimDownForWindows.Application/CommandLine/` | `CommandLineLauncher.cs` | | DI 拡張メソッド (Application) | `SkimDownForWindows.Application/DependencyInjection/` | `ApplicationServiceCollectionExtensions.cs` | | DI 拡張メソッド (Infrastructure) | `SkimDownForWindows.Infrastructure/DependencyInjection/` | `InfrastructureServiceCollectionExtensions.cs` | | ファイル I/O 実装 | `SkimDownForWindows.Infrastructure/IO/` | `LocalFileSystem.cs`, `JsonSettingsRepository.cs` | | WinRT 実装 | `SkimDownForWindows.Infrastructure/Windows/` | `WindowsClipboardService.cs`, `ExplorerShellService.cs` | | コンポジションルート / WindowsAppSDK 依存実装 | `SkimDownForWindows/Composition/` | `DispatcherQueueUiDispatcher.cs`, `WindowService.cs` | | XAML / Page / Window / UserControl | `SkimDownForWindows/` | `MainWindow.xaml`, `MainPage.xaml`, `MarkdownPreview.xaml` | | UI 文字列リソース | `SkimDownForWindows/Strings/<locale>/` | `Strings/en-US/Resources.resw` | ## 命名規約 - 抽象は `I<Name>` (例: `IFileSystem`) - 既定実装は具体名 (例: `LocalFileSystem`, `JsonSettingsRepository`) - DI 拡張は `AddSkimDown<Layer>` (例: `AddSkimDownApplication`, `AddSkimDownInfrastructure`) - 名前空間 = プロジェクト名 + サブフォルダー名 ## やってはいけないこと (Anti-patterns) ### ❌ ViewModel から `System.IO` を直呼び ```csharp // ❌ NG public async Task SelectAndLoadAsync(string path) { var text = await File.ReadAllTextAsync(path); // Application 層から直呼び ... } ``` ```csharp // ✅ OK - IMarkdownFileReader 経由 public async Task SelectAndLoadAsync(string path) { var text = await _reader.ReadAsync(path); ... } ``` ### ❌ Application 層から `Windows.*` を using ```csharp // ❌ NG - Application プロジェクトに Windows.* を持ち込む using Windows.ApplicationModel.DataTransfer; ... Clipboard.SetContent(new DataPackage()); ``` ```csharp // ✅ OK - IClipboardService 経由 _clipboard.SetText(path); ``` ### ❌ 具象クラスを `new` ```csharp // ❌ NG public MainPageViewModel() { _scanner = new MarkdownScanner(); // DI で解決すべき _settings = new JsonSettingsRepository(); } ``` ```csharp // ✅ OK - コンストラクタインジェクション public MainPageViewModel(MarkdownScanner scanner, ISettingsRepository settings, ...) { _scanner = scanner; _settings = settings; } ``` > 純粋な値オブジェクト (`new AppSettings()`, `new MarkdownTreeItem(...)`) は OK。 ### ❌ `static` シングルトン ```csharp // ❌ NG public static class WindowManager { public static List<Window> Windows { get; } = new(); } ``` ```csharp // ✅ OK - IWindowService を Singleton 登録 services.AddSingleton<IWindowService, WindowService>(); ``` ### ❌ Infrastructure で WindowsAppSDK を using ```csharp // ❌ NG - Infrastructure プロジェクトで using Microsoft.UI.Dispatching; // WindowsAppSDK は Infrastructure では参照しない ``` ```csharp // ✅ OK - Presentation 側 (Composition/) に置く // src/SkimDownForWindows/Composition/DispatcherQueueUiDispatcher.cs ``` ### ❌ クラスライブラリに `.xaml` ```csharp // ❌ NG - SkimDownForWindows.Application に // MarkdownPreview.xaml を置く ``` ```csharp // ✅ OK - App プロジェクトに置く // src/SkimDownForWindows/MarkdownPreview.xaml ``` ### ❌ UI 文字列をハードコード ```xml <!-- ❌ NG - XAML に直接英語を埋め込む --> <MenuFlyoutItem Text="Open Folder…" Click="OnOpenFolderClick" /> ``` ```xml <!-- ✅ OK - x:Uid で resw 経由 --> <MenuFlyoutItem x:Uid="OpenFolderMenuItem" Click="OnOpenFolderClick" /> <!-- Strings/en-US/Resources.resw に <data name="OpenFolderMenuItem.Text"> --> ``` ```csharp // ❌ NG - code-behind で英語を組み立てる MarkdownCountText.Text = count == 1 ? "1 markdown file" : $"{count} markdown files"; ``` ```csharp // ✅ OK - ResourceLoader 経由 MarkdownCountText.Text = count == 1 ? _strings.GetString("MarkdownCount/OneFile") : string.Format(_strings.GetString("MarkdownCount/ManyFiles"), count); ``` ```csharp // ❌ NG - Application 層に Windows.ApplicationModel.Resources を持ち込む // src/SkimDownForWindows.Application/ViewModels/MainPageViewModel.cs using Windows.ApplicationModel.Resources; // net10.0 ターゲットでは using 不可 ``` ### ❌ ADR 本文書き換え - Accepted な ADR の本文は書き換えない (typo / リンク修正以外) - 設計を変える時は新規 ADR を作り、旧 ADR の `ステータス` を `Superseded by NNNN` に書き換える ## チェックリスト (PR 時セルフレビュー) - [ ] 追加した型は適切なプロジェクト / フォルダーに配置されているか - [ ] 名前空間 = プロジェクト名 + サブフォルダー名 になっているか - [ ] Application 層に `System.IO` / `System.Diagnostics.Process` / `Windows.*` の using が無いか - [ ] `new` で具象クラスを生成していないか (値オブジェクトを除く) - [ ] DI 登録のライフサイクル (Singleton / Scoped) は妥当か - [ ] `IDisposable` を必要とするサービスは Scoped で登録されているか - [ ] WindowsAppSDK 依存型を Infrastructure に持ち込んでいないか - [ ] クラスライブラリに `.xaml` を置いていないか - [ ] 新規 UI 文字列を resw に追加したか (`x:Uid` または `ResourceLoader.GetString` 経由) - [ ] Application / Infrastructure / Domain で `Windows.ApplicationModel.Resources` を using していないか - [ ] 既存の設計判断を変える必要があれば、新 ADR を起こしているか ## 設計判断を変えたい時 層境界・外部依存・横断ポリシー・セキュリティモデルを変える時は、コード変更前に **[ADR を新規作成](../../adr/README.md)** する。 1. `.github/adr/template.md` をコピーして `NNNN-kebab.md` を作成 2. コンテキスト / 決定 / 結果 / 検討した代替案を書く 3. レビューで合意後、`ステータス` を `Accepted` にしてマージ 4. 関連する `copilot-instructions.md` / この skill を更新 ## 参考リンク - [ADR-0001 アーキテクチャー判断を ADR として記録する](../../adr/0001-record-architecture-decisions.md) - [ADR-0002 クリーンアーキテクチャー風の層分割と DI コンテナの導入](../../adr/0002-clean-architecture-layered-projects.md) - [ADR-0006 UI 文字列ローカライズに MRT (resw) と ResourceLoader を採用する](../../adr/0006-localization-with-resw-and-resourceloader.md) - [copilot-instructions.md](../../copilot-instructions.md)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub