' ============================================================ ' WALKGUIDE — DESIGN PATTERNS (GoF) ' Flutter × Spring Boot × In-Device AI ' 7 Patterns: 2 Creational · 2 Structural · 3 Behavioral ' ============================================================ @startuml WalkGuide_Design_Patterns skinparam monochrome false skinparam shadowing false skinparam defaultFontName Arial skinparam defaultFontSize 12 skinparam roundCorner 10 skinparam ArrowColor #555555 skinparam ArrowThickness 1.2 skinparam class { BackgroundColor #FAFAFA BorderColor #AAAAAA HeaderBackgroundColor #E8E8E8 FontColor #222222 StereotypeFontColor #666666 AttributeFontColor #333333 } skinparam package { BackgroundColor #F5F5F5 BorderColor #888888 FontColor #333333 FontStyle bold } skinparam note { BackgroundColor #FFFDE7 BorderColor #F9A825 FontColor #444444 FontSize 11 } ' ============================================================ ' PATTERN 1 — BUILDER (Creational) ' ============================================================ package "① Builder Pattern [Creational]" #EEF6EE { interface "UserBuilder\n<>" as UserBuilder { + email(String) : UserBuilder + password(String) : UserBuilder + displayName(String) : UserBuilder + uniqueUserId(String) : UserBuilder + fcmToken(String) : UserBuilder + role(Role) : UserBuilder + build() : User } class "User\n<>" as User { - id : Long - email : String - password : String - displayName : String - uniqueUserId : String - fcmToken : String - role : Role - createdAt : Timestamp + {static} builder() : UserBuilder } class "FcmService\n<>" as FcmService { + sendPushNotification(userId, payload) + sendSosAlert(guardianId, lat, lng) - buildMessage(token, title, body) : Message } class "AuthService\n<>" as AuthService { + register(RegisterRequest) : AuthDataResponse + login(LoginRequest) : AuthDataResponse - buildAuthResponse(user, tokens) : AuthDataResponse } UserBuilder ..> User : <> User ..> UserBuilder : <> FcmService ..> "Firebase\nMessage.builder()" : uses AuthService ..> "AuthDataResponse\n.builder()" : uses } note bottom of UserBuilder Lombok @Builder pada User.java Memisahkan konstruksi objek kompleks dari representasinya. Optional fields (displayName, uniqueUserId, fcmToken) tidak perlu constructor overloading. end note ' ============================================================ ' PATTERN 2 — SINGLETON (Creational) ' ============================================================ package "② Singleton Pattern [Creational]" #EEF0FF { class "GetIt\n<>" as GetIt { - {static} _instance : GetIt + {static} instance : GetIt + registerSingleton(T) + get() : T } class "TtsService\n<>" as TtsService { - {static} _instance : TtsService - _flutterTts : FlutterTts + speak(String text) + speakImmediate(String text) + stop() + setLanguage(String lang) } class "SttService\n<>" as SttService { - {static} _instance : SttService - _speechToText : SpeechToText + startListening() + stopListening() + onResult : Stream } class "YoloDetector\n<>" as YoloDetector { - {static} _instance : YoloDetector - _interpreter : Interpreter - _labels : List + loadModel() + detect(CameraImage) : List + isRunning : bool } class "WebSocketService\n<>" as WebSocketService { - {static} _instance : WebSocketService - _stompClient : StompClient + connect(String token) + subscribe(String destination) + disconnect() } class "AgoraService\n<>" as AgoraService { - {static} _instance : AgoraService - _engine : RtcEngine + joinChannel(token, channel, uid) + leaveChannel() + muteLocalAudio(bool) } GetIt --> TtsService : registers &\nmanages GetIt --> SttService : registers &\nmanages GetIt --> YoloDetector : registers &\nmanages GetIt --> WebSocketService : registers &\nmanages GetIt --> AgoraService : registers &\nmanages } note right of GetIt injection_container.dart: sl.registerSingleton(TtsService()) sl.registerSingleton(YoloDetector()) sl.registerSingleton(WebSocketService()) Satu instance untuk seluruh lifecycle app. end note ' ============================================================ ' PATTERN 3 — FACADE (Structural) — Flutter ' ============================================================ package "③ Facade Pattern [Structural]" #FFF8E1 { class "VoiceCommandHandler\n<>" as VoiceCommandHandler { - _ttsService : TtsService - _sttService : SttService - _router : GoRouter - _walkGuideBloc : WalkGuideBloc - _sosBloc : SosBloc - _notifBloc : NotificationBloc + processText(String command) : void - _matchCommand(String) : VoiceCommandKey? - _executeCommand(VoiceCommandKey) : void } class "WalkGuideBloc\n<>" as WalkGuideBlocFacade { + onVoiceCommand(String text) } class "GuardianDashboardService\n<>" as GuardianDashboardService { - _locationService : LocationService - _activityService : ActivityLogService - _sosService : SosService - _notifService : NotificationService + getDashboard(guardianId) : DashboardResponse } class "SttService " as SttServiceFacade <> class "TtsService " as TtsServiceFacade <> class "GoRouter\n<>" as GoRouterFacade <> class "SosBloc " as SosBlocFacade <> class "LocationService\n<>" as LocationService <> class "ActivityLogService\n<>" as ActivityService <> class "SosService\n<>" as SosServiceFacade <> WalkGuideBlocFacade --> VoiceCommandHandler : processText() VoiceCommandHandler --> SttServiceFacade : delegates VoiceCommandHandler --> TtsServiceFacade : delegates VoiceCommandHandler --> GoRouterFacade : delegates VoiceCommandHandler --> SosBlocFacade : delegates GuardianDashboardService --> LocationService : aggregates GuardianDashboardService --> ActivityService : aggregates GuardianDashboardService --> SosServiceFacade : aggregates } note right of VoiceCommandHandler Client hanya panggil processText("start walkguide") tanpa perlu tahu kompleksitas di baliknya: matching phrase → execute command → TTS feedback → route navigation → BLoC event dispatch end note ' ============================================================ ' PATTERN 4 — REPOSITORY / PROXY (Structural) ' ============================================================ package "④ Repository Pattern [Structural — Proxy]" #FFF3E0 { interface "WalkGuideRepository\n<>" as WalkGuideRepo { + startSession() : Either + stopSession() : Either + logObstacle(ObstacleLogRequest) : Either + getObstacleLogs() : Either> } interface "ActivityLogRepository\n<>" as ActivityRepo { + getLogs(page) : Either> + savePending(ActivityLog) : Either + syncPending() : Either } class "WalkGuideRepositoryImpl\n<>" as WalkGuideRepoImpl { - _remoteDataSource : WalkGuideRemoteDataSource - _localDataSource : WalkGuideLocalDataSource - _connectivity : ConnectivityPlus + startSession() : Either + logObstacle(req) : Either - _isOnline() : bool } class "ActivityLogRepositoryImpl\n<>" as ActivityRepoImpl { - _remoteDataSource : ActivityRemoteDataSource - _localDataSource : ActivityLocalDataSource + getLogs(page) : Either + syncPending() : Either } class "WalkGuideRemoteDataSource\n<>" as RemoteDSWalk { + startSession() : void + logObstacle(req) : void ' POST /api/v1/user/obstacle } class "WalkGuideLocalDataSource\n<>" as LocalDSWalk { + cacheObstacle(ObstacleLog) : void + getPendingLogs() : List ' Drift ORM — offline first } WalkGuideRepo <|.. WalkGuideRepoImpl : implements ActivityRepo <|.. ActivityRepoImpl : implements WalkGuideRepoImpl --> RemoteDSWalk : online path WalkGuideRepoImpl --> LocalDSWalk : offline path } note bottom of WalkGuideRepoImpl Proxy memutuskan sumber data: if (isOnline) → fetch remote API else → baca/tulis SQLite (Drift) Domain layer tidak tahu perbedaannya. dartz Either untuk typed error. end note ' ============================================================ ' PATTERN 5 — OBSERVER (Behavioral) ' ============================================================ package "⑤ Observer Pattern [Behavioral]" #F3E5F5 { abstract class "Bloc\n<>" as BlocSubject { # stateController : StreamController + {abstract} on(EventHandler) + add(Event event) + emit(State state) + stream : Stream } class "WalkGuideBloc\n<>" as WalkGuideBlocObs { + on(_onStart) + on(_onStop) + on(_onFrame) + on(_onObstacle) - _yoloDetector : YoloDetector - _ttsService : TtsService - _hapticService : HapticService } class "BlocBuilder\n<>" as BlocBuilderWidget { + builder(ctx, state) : Widget ' Rebuilds UI on every state emission } class "BlocListener\n<>" as BlocListenerWidget { + listener(ctx, state) : void ' Side effects: TTS, haptic, navigation } class "WebSocketService\n<>" as WebSocketObs { - _stompClient : StompClient + subscribe(destination) : Stream + onLocationUpdate : StreamController + onSosAlert : StreamController + onNotification : StreamController } class "GuardianMapScreen\n<>" as GuardianMapObs { + onLocationUpdate(LocationData) ' Updates flutter_map markers in real-time } BlocSubject <|-- WalkGuideBlocObs : extends WalkGuideBlocObs --> BlocBuilderWidget : emits state\n(rebuilds UI) WalkGuideBlocObs --> BlocListenerWidget : emits state\n(side effects) WebSocketObs --> GuardianMapObs : notifies\nlive location } note left of WebSocketObs Guardian subscribe topic: /topic/location/{userId} — live map /queue/sos/{guardianId} — SOS alert /queue/notif/{userId} — notifications STOMP over SockJS (stomp_dart_client) end note ' ============================================================ ' PATTERN 6 — STRATEGY (Behavioral) ' ============================================================ package "⑥ Strategy Pattern [Behavioral]" #E8F5E9 { interface "ObstacleAlertStrategy\n<>" as AlertStrategy { + alert(DetectionResult result) : void } class "TtsOnlyStrategy\n<>" as TtsOnly { - _ttsService : TtsService + alert(result) : void ' TTS: "Caution! {label} {direction}. {distance}." } class "TtsWithHapticStrategy\n<>" as TtsHaptic { - _ttsService : TtsService - _hapticService : HapticService + alert(result) : void ' TTS + vibration pattern } class "HapticOnlyStrategy\n<>" as HapticOnly { - _hapticService : HapticService + alert(result) : void ' Vibration only (silent environment) } class "ObstacleAnalyzer\n<>" as ObstacleAnalyzerCtx { - _strategy : ObstacleAlertStrategy - _aiConfig : AiConfig + setStrategy(ObstacleAlertStrategy) + analyze(List) : void + prioritize(results) : DetectionResult + calculateDirection(bbox) : Direction } class "AiConfig\n<>" as AiConfigStrategy { + alertMode : AlertMode + confidenceThreshold : double + maxInferenceFps : int + enabledLabels : List } AlertStrategy <|.. TtsOnly : implements AlertStrategy <|.. TtsHaptic : implements AlertStrategy <|.. HapticOnly : implements ObstacleAnalyzerCtx --> AlertStrategy : uses ObstacleAnalyzerCtx ..> AiConfigStrategy : reads alertMode\nto select strategy } note right of ObstacleAnalyzerCtx Guardian dapat ganti strategy dari dashboard: alertMode = TTS_ONLY | TTS_HAPTIC | HAPTIC_ONLY PUT /api/v1/guardian/ai-config Strategy dipilih runtime tanpa ubah kode utama. end note ' ============================================================ ' PATTERN 7 — CHAIN OF RESPONSIBILITY (Behavioral) ' ============================================================ package "⑦ Chain of Responsibility [Behavioral]" #FCE4EC { ' --- Flutter: Dio Interceptor Chain --- abstract class "Interceptor\n<>" as DioInterceptor { + onRequest(options, handler) + onResponse(response, handler) + onError(error, handler) } class "AuthInterceptor\n<>" as AuthInterceptor { - _secureStorage : FlutterSecureStorage + onRequest(options, handler) ' Attach Bearer token ke setiap request + onError(error, handler) ' Jika 401: refresh token → retry request } class "ErrorInterceptor\n<>" as ErrorInterceptor { + onError(error, handler) ' Map HTTP error codes → Failure domain objects ' 401 → UnauthorizedFailure ' 404 → NotFoundFailure ' 500 → ServerFailure } class "LogInterceptor\n<>" as LogInterceptorFlutter { + onRequest(options, handler) + onResponse(response, handler) + onError(error, handler) ' Print request/response untuk debugging } class "ApiClient\n<>" as ApiClient { - _dio : Dio + get(url) : Response + post(url, data) : Response + put(url, data) : Response ' Interceptors dieksekusi berurutan } ' --- Backend: Spring Security Filter Chain --- abstract class "OncePerRequestFilter\n<>" as SpringFilter { + {abstract} doFilterInternal(req, res, chain) + doFilter(req, res, chain) } class "JwtAuthFilter\n<>" as JwtAuthFilter { - _jwtUtil : JwtUtil - _userDetailsService : UserDetailsService + doFilterInternal(req, res, chain) ' Extract JWT → validate → set SecurityContext ' Jika invalid: 401 Unauthorized langsung } class "SecurityConfig\n<>" as SecurityConfigChain { + securityFilterChain(http) : SecurityFilterChain ' /auth/** → permitAll ' /api/v1/** → authenticated ' ROLE_GUARDIAN vs ROLE_USER routing } class "Controller\n<>" as ControllerChain { + handleRequest(request) : ResponseEntity ' Request tiba hanya jika lolos semua filter } DioInterceptor <|-- AuthInterceptor : extends DioInterceptor <|-- ErrorInterceptor : extends DioInterceptor <|-- LogInterceptorFlutter : extends ApiClient --> AuthInterceptor : chain[0] AuthInterceptor --> ErrorInterceptor : passes to\nchain[1] ErrorInterceptor --> LogInterceptorFlutter : passes to\nchain[2] SpringFilter <|-- JwtAuthFilter : extends JwtAuthFilter --> SecurityConfigChain : passes to SecurityConfigChain --> ControllerChain : passes to\n(if authorized) } note bottom of ApiClient Flutter — Dio interceptor chain: HTTP Request → AuthInterceptor → ErrorInterceptor → LogInterceptor → Response Setiap handler bisa: proses, modifikasi, atau blok request. AuthInterceptor otomatis refresh JWT jika expired (401). end note note bottom of JwtAuthFilter Backend — Spring Security filter chain: HTTP Request → JwtAuthFilter → SecurityConfig → Controller JwtAuthFilter extract & validasi Bearer token setiap request. Jika invalid → langsung 401 tanpa lanjut ke controller. end note @enduml