500 lines
17 KiB
Plaintext
500 lines
17 KiB
Plaintext
' ============================================================
|
||
' 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
|