500 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

' ============================================================
' 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<<Builder>>" 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<<Entity>>" as User {
- id : Long
- email : String
- password : String
- displayName : String
- uniqueUserId : String
- fcmToken : String
- role : Role
- createdAt : Timestamp
+ {static} builder() : UserBuilder
}
class "FcmService\n<<Service>>" as FcmService {
+ sendPushNotification(userId, payload)
+ sendSosAlert(guardianId, lat, lng)
- buildMessage(token, title, body) : Message
}
class "AuthService\n<<Service>>" as AuthService {
+ register(RegisterRequest) : AuthDataResponse
+ login(LoginRequest) : AuthDataResponse
- buildAuthResponse(user, tokens) : AuthDataResponse
}
UserBuilder ..> User : <<creates>>
User ..> UserBuilder : <<returns>>
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<<ServiceLocator>>" as GetIt {
- {static} _instance : GetIt
+ {static} instance : GetIt
+ registerSingleton<T>(T)
+ get<T>() : T
}
class "TtsService\n<<Singleton>>" as TtsService {
- {static} _instance : TtsService
- _flutterTts : FlutterTts
+ speak(String text)
+ speakImmediate(String text)
+ stop()
+ setLanguage(String lang)
}
class "SttService\n<<Singleton>>" as SttService {
- {static} _instance : SttService
- _speechToText : SpeechToText
+ startListening()
+ stopListening()
+ onResult : Stream<String>
}
class "YoloDetector\n<<Singleton>>" as YoloDetector {
- {static} _instance : YoloDetector
- _interpreter : Interpreter
- _labels : List<String>
+ loadModel()
+ detect(CameraImage) : List<DetectionResult>
+ isRunning : bool
}
class "WebSocketService\n<<Singleton>>" as WebSocketService {
- {static} _instance : WebSocketService
- _stompClient : StompClient
+ connect(String token)
+ subscribe(String destination)
+ disconnect()
}
class "AgoraService\n<<Singleton>>" 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>(TtsService())
sl.registerSingleton<YoloDetector>(YoloDetector())
sl.registerSingleton<WebSocketService>(WebSocketService())
Satu instance untuk seluruh lifecycle app.
end note
' ============================================================
' PATTERN 3 — FACADE (Structural) — Flutter
' ============================================================
package "③ Facade Pattern [Structural]" #FFF8E1 {
class "VoiceCommandHandler\n<<Facade>>" 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<<Client>>" as WalkGuideBlocFacade {
+ onVoiceCommand(String text)
}
class "GuardianDashboardService\n<<Facade>>" as GuardianDashboardService {
- _locationService : LocationService
- _activityService : ActivityLogService
- _sosService : SosService
- _notifService : NotificationService
+ getDashboard(guardianId) : DashboardResponse
}
class "SttService " as SttServiceFacade <<Subsystem>>
class "TtsService " as TtsServiceFacade <<Subsystem>>
class "GoRouter\n<<Router>>" as GoRouterFacade <<Subsystem>>
class "SosBloc " as SosBlocFacade <<Subsystem>>
class "LocationService\n<<Service>>" as LocationService <<Subsystem>>
class "ActivityLogService\n<<Service>>" as ActivityService <<Subsystem>>
class "SosService\n<<Service>>" as SosServiceFacade <<Subsystem>>
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<<interface>>" as WalkGuideRepo {
+ startSession() : Either<Failure, void>
+ stopSession() : Either<Failure, void>
+ logObstacle(ObstacleLogRequest) : Either<Failure, void>
+ getObstacleLogs() : Either<Failure, List<ObstacleLog>>
}
interface "ActivityLogRepository\n<<interface>>" as ActivityRepo {
+ getLogs(page) : Either<Failure, List<ActivityLog>>
+ savePending(ActivityLog) : Either<Failure, void>
+ syncPending() : Either<Failure, void>
}
class "WalkGuideRepositoryImpl\n<<Proxy>>" as WalkGuideRepoImpl {
- _remoteDataSource : WalkGuideRemoteDataSource
- _localDataSource : WalkGuideLocalDataSource
- _connectivity : ConnectivityPlus
+ startSession() : Either<Failure, void>
+ logObstacle(req) : Either<Failure, void>
- _isOnline() : bool
}
class "ActivityLogRepositoryImpl\n<<Proxy>>" as ActivityRepoImpl {
- _remoteDataSource : ActivityRemoteDataSource
- _localDataSource : ActivityLocalDataSource
+ getLogs(page) : Either<Failure, List>
+ syncPending() : Either<Failure, void>
}
class "WalkGuideRemoteDataSource\n<<Remote>>" as RemoteDSWalk {
+ startSession() : void
+ logObstacle(req) : void
' POST /api/v1/user/obstacle
}
class "WalkGuideLocalDataSource\n<<SQLite/Drift>>" as LocalDSWalk {
+ cacheObstacle(ObstacleLog) : void
+ getPendingLogs() : List<ObstacleLog>
' 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<Failure, Data> untuk typed error.
end note
' ============================================================
' PATTERN 5 — OBSERVER (Behavioral)
' ============================================================
package "⑤ Observer Pattern [Behavioral]" #F3E5F5 {
abstract class "Bloc<Event, State>\n<<Subject>>" as BlocSubject {
# stateController : StreamController<State>
+ {abstract} on<E>(EventHandler)
+ add(Event event)
+ emit(State state)
+ stream : Stream<State>
}
class "WalkGuideBloc\n<<ConcreteSubject>>" as WalkGuideBlocObs {
+ on<StartWalkGuide>(_onStart)
+ on<StopWalkGuide>(_onStop)
+ on<CameraFrameReceived>(_onFrame)
+ on<ObstacleDetected>(_onObstacle)
- _yoloDetector : YoloDetector
- _ttsService : TtsService
- _hapticService : HapticService
}
class "BlocBuilder<WalkGuideBloc, WalkGuideState>\n<<Observer / Widget>>" as BlocBuilderWidget {
+ builder(ctx, state) : Widget
' Rebuilds UI on every state emission
}
class "BlocListener<WalkGuideBloc, WalkGuideState>\n<<Observer / SideEffect>>" as BlocListenerWidget {
+ listener(ctx, state) : void
' Side effects: TTS, haptic, navigation
}
class "WebSocketService\n<<Subject>>" as WebSocketObs {
- _stompClient : StompClient
+ subscribe(destination) : Stream<dynamic>
+ onLocationUpdate : StreamController
+ onSosAlert : StreamController
+ onNotification : StreamController
}
class "GuardianMapScreen\n<<Observer>>" 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<<Strategy>>" as AlertStrategy {
+ alert(DetectionResult result) : void
}
class "TtsOnlyStrategy\n<<ConcreteStrategy>>" as TtsOnly {
- _ttsService : TtsService
+ alert(result) : void
' TTS: "Caution! {label} {direction}. {distance}."
}
class "TtsWithHapticStrategy\n<<ConcreteStrategy>>" as TtsHaptic {
- _ttsService : TtsService
- _hapticService : HapticService
+ alert(result) : void
' TTS + vibration pattern
}
class "HapticOnlyStrategy\n<<ConcreteStrategy>>" as HapticOnly {
- _hapticService : HapticService
+ alert(result) : void
' Vibration only (silent environment)
}
class "ObstacleAnalyzer\n<<Context>>" as ObstacleAnalyzerCtx {
- _strategy : ObstacleAlertStrategy
- _aiConfig : AiConfig
+ setStrategy(ObstacleAlertStrategy)
+ analyze(List<DetectionResult>) : void
+ prioritize(results) : DetectionResult
+ calculateDirection(bbox) : Direction
}
class "AiConfig\n<<Config>>" as AiConfigStrategy {
+ alertMode : AlertMode
+ confidenceThreshold : double
+ maxInferenceFps : int
+ enabledLabels : List<String>
}
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<<Handler>>" as DioInterceptor {
+ onRequest(options, handler)
+ onResponse(response, handler)
+ onError(error, handler)
}
class "AuthInterceptor\n<<ConcreteHandler>>" 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<<ConcreteHandler>>" as ErrorInterceptor {
+ onError(error, handler)
' Map HTTP error codes → Failure domain objects
' 401 → UnauthorizedFailure
' 404 → NotFoundFailure
' 500 → ServerFailure
}
class "LogInterceptor\n<<ConcreteHandler>>" as LogInterceptorFlutter {
+ onRequest(options, handler)
+ onResponse(response, handler)
+ onError(error, handler)
' Print request/response untuk debugging
}
class "ApiClient\n<<Dio>>" 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<<Handler>>" as SpringFilter {
+ {abstract} doFilterInternal(req, res, chain)
+ doFilter(req, res, chain)
}
class "JwtAuthFilter\n<<ConcreteHandler>>" as JwtAuthFilter {
- _jwtUtil : JwtUtil
- _userDetailsService : UserDetailsService
+ doFilterInternal(req, res, chain)
' Extract JWT → validate → set SecurityContext
' Jika invalid: 401 Unauthorized langsung
}
class "SecurityConfig\n<<Config/Handler>>" as SecurityConfigChain {
+ securityFilterChain(http) : SecurityFilterChain
' /auth/** → permitAll
' /api/v1/** → authenticated
' ROLE_GUARDIAN vs ROLE_USER routing
}
class "Controller\n<<Endpoint>>" 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