スキル一覧に戻る
333-333-333

clean-architecture

by 333-333-333

Voice-activated vision assistant using computer vision and TTS to help blind users understand their surroundings through natural language descriptions. Built with React Native and Azure.

0🍴 0📅 2026年1月20日
GitHubで見るManusで実行

SKILL.md


name: clean-architecture description: > Clean Architecture + Screaming Architecture patterns for React/React Native. Trigger: When structuring features, separating concerns, or defining domain boundaries. license: Apache-2.0 metadata: author: 333-333-333 version: "1.0" scope: [mobile] auto_invoke: - "Structuring project architecture" - "Creating new features" - "Separating business logic from UI"

When to Use

  • Starting a new feature
  • Refactoring to separate concerns
  • Defining domain entities and use cases
  • Creating repository/service abstractions
  • Structuring a scalable codebase

Core Principles

Screaming Architecture

"Your architecture should scream the intent of the system" — Uncle Bob

The folder structure should tell you what the app DOES, not what framework it uses.

# Bad: Screams "I'm a React app"
src/
├── components/
├── hooks/
├── services/
└── utils/

# Good: Screams "I'm a vision assistant for blind users"
src/
├── voice/           # Voice commands feature
├── vision/          # Scene analysis feature
└── shared/          # Cross-cutting concerns

Clean Architecture Layers

┌─────────────────────────────────────────┐
│           Presentation Layer            │  ← UI, Components
├─────────────────────────────────────────┤
│           Application Layer             │  ← Use Cases, Orchestration
├─────────────────────────────────────────┤
│             Domain Layer                │  ← Entities, Business Rules
├─────────────────────────────────────────┤
│          Infrastructure Layer           │  ← External Services, APIs
└─────────────────────────────────────────┘

Dependency Rule: Always point INWARD
  - Presentation → Application → Domain ← Infrastructure
  - Domain knows NOTHING about outer layers

Feature Structure

Each feature follows the same internal structure:

src/voice/
├── domain/                    # Core business logic (pure)
│   ├── entities/
│   │   └── VoiceCommand.js    # Domain entity
│   ├── repositories/
│   │   └── VoiceRepository.js # Interface (port)
│   └── value-objects/
│       └── CommandType.js     # Immutable value
│
├── application/               # Use cases (orchestration)
│   ├── use-cases/
│   │   ├── ProcessCommand.js
│   │   └── ListenForWakeWord.js
│   └── ports/                 # Interfaces for infra
│       └── SpeechRecognizer.js
│
├── infrastructure/            # External implementations
│   ├── adapters/
│   │   └── RNVoiceAdapter.js  # Implements SpeechRecognizer
│   └── mappers/
│       └── VoiceEventMapper.js
│
└── presentation/              # UI layer
    ├── components/
    ├── hooks/
    │   └── useVoiceCommands.js
    └── screens/

Layer Details

Domain Layer

Purpose: Pure business logic. No dependencies on frameworks.

// domain/entities/VoiceCommand.js
export class VoiceCommand {
  constructor({ text, confidence, timestamp }) {
    this.text = text;
    this.confidence = confidence;
    this.timestamp = timestamp;
  }

  isValid() {
    return this.confidence > 0.7;
  }

  containsWakeWord(wakeWord = 'iris') {
    return this.text.toLowerCase().includes(wakeWord);
  }

  getIntent() {
    if (this.text.includes('describe')) return 'DESCRIBE';
    if (this.text.includes('repeat')) return 'REPEAT';
    if (this.text.includes('help')) return 'HELP';
    return 'UNKNOWN';
  }
}
// domain/repositories/VoiceRepository.js (interface)
export class VoiceRepository {
  async getLastCommand() {
    throw new Error('Not implemented');
  }
  
  async saveCommand(command) {
    throw new Error('Not implemented');
  }
}

Application Layer

Purpose: Use cases that orchestrate domain logic.

// application/use-cases/ProcessCommand.js
export class ProcessCommandUseCase {
  constructor({ voiceRepository, speechSynthesizer, visionService }) {
    this.voiceRepository = voiceRepository;
    this.speechSynthesizer = speechSynthesizer;
    this.visionService = visionService;
  }

  async execute(command) {
    if (!command.isValid()) {
      return { success: false, reason: 'LOW_CONFIDENCE' };
    }

    if (!command.containsWakeWord()) {
      return { success: false, reason: 'NO_WAKE_WORD' };
    }

    const intent = command.getIntent();

    switch (intent) {
      case 'DESCRIBE':
        const description = await this.visionService.describeScene();
        await this.speechSynthesizer.speak(description);
        await this.voiceRepository.saveCommand(command);
        return { success: true, intent, description };

      case 'REPEAT':
        const lastCommand = await this.voiceRepository.getLastCommand();
        await this.speechSynthesizer.speak(lastCommand?.description || 'No hay descripción anterior');
        return { success: true, intent };

      default:
        await this.speechSynthesizer.speak('No entendí el comando');
        return { success: false, reason: 'UNKNOWN_INTENT' };
    }
  }
}
// application/ports/SpeechRecognizer.js (interface)
export class SpeechRecognizer {
  async startListening() { throw new Error('Not implemented'); }
  async stopListening() { throw new Error('Not implemented'); }
  onResult(callback) { throw new Error('Not implemented'); }
}

Infrastructure Layer

Purpose: Implementations of interfaces using external libraries.

// infrastructure/adapters/RNVoiceAdapter.js
import Voice from '@react-native-voice/voice';
import { SpeechRecognizer } from '../../application/ports/SpeechRecognizer';

export class RNVoiceAdapter extends SpeechRecognizer {
  constructor() {
    super();
    this.resultCallback = null;
  }

  async startListening() {
    Voice.onSpeechResults = (event) => {
      if (this.resultCallback && event.value?.[0]) {
        this.resultCallback(event.value[0]);
      }
    };
    await Voice.start('es-ES');
  }

  async stopListening() {
    await Voice.stop();
  }

  onResult(callback) {
    this.resultCallback = callback;
  }
}

Presentation Layer

Purpose: React components and hooks that consume use cases.

// presentation/hooks/useVoiceCommands.js
import { useState, useEffect, useCallback } from 'react';
import { ProcessCommandUseCase } from '../../application/use-cases/ProcessCommand';
import { VoiceCommand } from '../../domain/entities/VoiceCommand';

export function useVoiceCommands(dependencies) {
  const [isListening, setIsListening] = useState(false);
  const [lastResult, setLastResult] = useState(null);

  const processCommand = useMemo(
    () => new ProcessCommandUseCase(dependencies),
    [dependencies]
  );

  const handleVoiceResult = useCallback(async (text, confidence) => {
    const command = new VoiceCommand({
      text,
      confidence,
      timestamp: Date.now(),
    });

    const result = await processCommand.execute(command);
    setLastResult(result);
  }, [processCommand]);

  return { isListening, lastResult, handleVoiceResult };
}

Dependency Injection

// src/shared/di/container.js
import { RNVoiceAdapter } from '../../voice/infrastructure/adapters/RNVoiceAdapter';
import { ExpoSpeechAdapter } from '../../voice/infrastructure/adapters/ExpoSpeechAdapter';
import { TFLiteVisionAdapter } from '../../vision/infrastructure/adapters/TFLiteVisionAdapter';

// Create instances
const speechRecognizer = new RNVoiceAdapter();
const speechSynthesizer = new ExpoSpeechAdapter();
const visionService = new TFLiteVisionAdapter();

// Export configured dependencies
export const container = {
  speechRecognizer,
  speechSynthesizer,
  visionService,
};
// App.js
import { container } from './src/shared/di/container';

function App() {
  return (
    <DependencyProvider container={container}>
      <MainScreen />
    </DependencyProvider>
  );
}

Testing Benefits

// Use case test with mocked dependencies
describe('ProcessCommandUseCase', () => {
  it('should describe scene on DESCRIBE intent', async () => {
    const mockVision = { describeScene: jest.fn().mockResolvedValue('Una persona') };
    const mockSpeech = { speak: jest.fn() };
    const mockRepo = { saveCommand: jest.fn() };

    const useCase = new ProcessCommandUseCase({
      visionService: mockVision,
      speechSynthesizer: mockSpeech,
      voiceRepository: mockRepo,
    });

    const command = new VoiceCommand({
      text: 'iris describe',
      confidence: 0.9,
      timestamp: Date.now(),
    });

    const result = await useCase.execute(command);

    expect(result.success).toBe(true);
    expect(mockVision.describeScene).toHaveBeenCalled();
    expect(mockSpeech.speak).toHaveBeenCalledWith('Una persona');
  });
});

Rules

RuleReason
Domain has NO imports from other layersKeeps business logic pure and testable
Use cases receive dependencies via constructorEnables testing with mocks
Infrastructure implements domain interfacesDependency inversion
Presentation only talks to Application layerUI doesn't know about infra details
One use case = one actionSingle responsibility
Entities contain business rulesLogic lives with data

Anti-patterns

Don'tDo
Import React in domain layerKeep domain framework-agnostic
Call APIs directly from componentsGo through use cases
Put business logic in componentsMove to domain/application
Create circular dependenciesAlways depend inward
Giant use cases doing everythingSplit into focused use cases

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です