docs(architecture): add design pattern UML diagrams
94
ooad-docs/01_Builder_Pattern.puml
Normal file
@ -0,0 +1,94 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 1 of 7: BUILDER (Creational)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_01_Builder
|
||||
|
||||
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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/01_builder_pattern.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
ooad-docs/02_Singleton_Pattern.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
110
ooad-docs/02_Singleton_Pattern.puml
Normal file
@ -0,0 +1,110 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 2 of 7: SINGLETON (Creational)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_02_Singleton
|
||||
|
||||
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 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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/03_Facade_Pattern.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
97
ooad-docs/03_Facade_Pattern.puml
Normal file
@ -0,0 +1,97 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 3 of 7: FACADE (Structural)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_03_Facade
|
||||
|
||||
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 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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/04_Repository_Proxy_Pattern.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
101
ooad-docs/04_Repository_Proxy_Pattern.puml
Normal file
@ -0,0 +1,101 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 4 of 7: REPOSITORY / PROXY (Structural)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_04_Repository_Proxy
|
||||
|
||||
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 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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/05_Observer_Pattern.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
101
ooad-docs/05_Observer_Pattern.puml
Normal file
@ -0,0 +1,101 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 5 of 7: OBSERVER (Behavioral)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_05_Observer
|
||||
|
||||
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 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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/06_Strategy_Pattern.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
99
ooad-docs/06_Strategy_Pattern.puml
Normal file
@ -0,0 +1,99 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 6 of 7: STRATEGY (Behavioral)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_06_Strategy
|
||||
|
||||
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 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
|
||||
|
||||
@enduml
|
||||
BIN
ooad-docs/07_ChainOfResponsibility_Pattern.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
137
ooad-docs/07_ChainOfResponsibility_Pattern.puml
Normal file
@ -0,0 +1,137 @@
|
||||
' ============================================================
|
||||
' WALKGUIDE — DESIGN PATTERNS (GoF)
|
||||
' Flutter × Spring Boot × In-Device AI
|
||||
' Pattern 7 of 7: CHAIN OF RESPONSIBILITY (Behavioral)
|
||||
' ============================================================
|
||||
|
||||
@startuml WalkGuide_07_ChainOfResponsibility
|
||||
|
||||
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 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
|
||||
383
ooad-docs/CLASS_MAPPING.md
Normal file
@ -0,0 +1,383 @@
|
||||
# Mapping Class per Design Pattern — WalkGuide
|
||||
|
||||
> Dokumen ini memetakan setiap class dari **Class Diagram WalkGuide** ke design pattern yang menggunakannya, beserta peran class tersebut dalam pattern dan justifikasi pemilihannya.
|
||||
|
||||
---
|
||||
|
||||
## Daftar Isi
|
||||
|
||||
1. [Matriks Ringkasan](#1-matriks-ringkasan)
|
||||
2. [Builder Pattern](#2-builder-pattern--creational)
|
||||
3. [Singleton Pattern](#3-singleton-pattern--creational)
|
||||
4. [Facade Pattern](#4-facade-pattern--structural)
|
||||
5. [Repository / Proxy Pattern](#5-repository--proxy-pattern--structural)
|
||||
6. [Observer Pattern](#6-observer-pattern--behavioral)
|
||||
7. [Strategy Pattern](#7-strategy-pattern--behavioral)
|
||||
8. [Chain of Responsibility Pattern](#8-chain-of-responsibility-pattern--behavioral)
|
||||
|
||||
---
|
||||
|
||||
## 1. Matriks Ringkasan
|
||||
|
||||
| Pattern | Kategori | Class Utama (dari Class Diagram) |
|
||||
|---------|----------|----------------------------------|
|
||||
| Builder | Creational | `User`, `AuthService`, `FcmService`, `AuthDataResponse` |
|
||||
| Singleton | Creational | `TtsService`, `SttService`, `YoloDetector`, `WebSocketService`, `AgoraService`, `HapticService` |
|
||||
| Facade | Structural | `VoiceCommandHandler`, `GuardianDashboardService`, `GuardianController`, `VoiceCommandConfig` |
|
||||
| Repository (Proxy) | Structural | `WalkGuideRepositoryImpl`, `ActivityLogRepositoryImpl`, `LocationRepositoryImpl`, `ObstacleLogRepository`, `ActivityLogRepository` |
|
||||
| Observer | Behavioral | `WalkGuideBloc`, `SosBloc`, `WebSocketService`, `LocationBroadcaster`, `GuardianNotificationRepository` |
|
||||
| Strategy | Behavioral | `ObstacleAnalyzer`, `AiConfig`, `AiConfigService`, `TtsService`, `HapticService` |
|
||||
| Chain of Responsibility | Behavioral | `ApiClient`, `AuthInterceptor`, `ErrorInterceptor`, `JwtAuthFilter`, `SecurityConfig`, `JwtUtil`, `CustomUserDetailsService` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Builder Pattern — Creational
|
||||
|
||||
### Justifikasi
|
||||
|
||||
Entitas `User` memiliki **8 field** — tidak semua selalu terisi. Field `uniqueUserId` hanya ada untuk `ROLE_USER`, `fcmToken` di-update setiap login, dan `displayName` bersifat opsional. Tanpa Builder, constructor membutuhkan 8 parameter yang rawan salah urutan dan sulit dibaca. `FcmService` membangun payload Firebase `Message` secara kondisional (beberapa field hanya ada saat SOS, yang lain hanya saat notifikasi biasa) — Builder sangat cocok untuk kasus ini.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `User` | Backend — Entity | **Product** — objek yang dibangun step-by-step via `@Builder` Lombok | Entity block |
|
||||
| `AuthService` | Backend — Service | **Director/Client** — memanggil `User.builder()...build()` saat register | Service block |
|
||||
| `FcmService` | Backend — Service | **Client** — memanggil `Message.builder()` Firebase untuk bangun payload FCM | Service block |
|
||||
| `AuthDataResponse` | Backend — DTO | **Product 2** — response token dibangun via `AuthDataResponse.builder()` | DTO block |
|
||||
| `RefreshToken` | Backend — Entity | **Product 3** — dibuat via builder saat generate refresh token | Entity block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
AuthService ──uses──► User.builder()
|
||||
.email()
|
||||
.password()
|
||||
.displayName()
|
||||
.uniqueUserId()
|
||||
.role()
|
||||
.build()
|
||||
│
|
||||
▼
|
||||
User (Entity tersimpan ke UserRepository)
|
||||
|
||||
FcmService ──uses──► Message.builder()
|
||||
.setToken()
|
||||
.setNotification()
|
||||
.putData("type", "SOS_ALERT")
|
||||
.setAndroidConfig()
|
||||
.build()
|
||||
│
|
||||
▼
|
||||
FirebaseMessage (dikirim via FCM)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Singleton Pattern — Creational
|
||||
|
||||
### Justifikasi
|
||||
|
||||
WalkGuide memiliki beberapa **resource-heavy services** yang harus ada tepat satu instance:
|
||||
- `YoloDetector` memuat model `.tflite` ±6 MB ke memori — jika dibuat ulang setiap buka layar akan terjadi memory leak dan delay 2–3 detik.
|
||||
- `WebSocketService` mempertahankan satu koneksi STOMP ke backend — multiple instance menyebabkan event diterima dobel.
|
||||
- `TtsService` mengelola engine TTS — dua instance berbicara bersamaan akan saling menimpa.
|
||||
- `AgoraService` mengelola RTC engine — satu per sesi, tidak boleh duplikat.
|
||||
|
||||
Semua di-register via `GetIt` service locator sebagai singleton agar seluruh widget tree mendapat instance yang sama.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `TtsService` | Flutter — Service | **Singleton** — satu TTS engine untuk seluruh app | Services block |
|
||||
| `SttService` | Flutter — Service | **Singleton** — satu STT listener always-on | Services block |
|
||||
| `YoloDetector` | Flutter — AI | **Singleton** — satu model inference engine (6 MB) | AI block |
|
||||
| `WebSocketService` | Flutter — Service | **Singleton** — satu koneksi STOMP ke backend | Services block |
|
||||
| `AgoraService` | Flutter — Service | **Singleton** — satu RTC engine untuk VoIP call | Services block |
|
||||
| `HapticService` | Flutter — Service | **Singleton** — satu haptic controller | Services block |
|
||||
| `GetIt` (injection_container) | Flutter — DI | **ServiceLocator** — mengelola lifecycle semua singleton | Bootstrap |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
injection_container.dart
|
||||
│
|
||||
├─ sl.registerSingleton<TtsService>(TtsService())
|
||||
├─ sl.registerSingleton<SttService>(SttService())
|
||||
├─ sl.registerSingleton<YoloDetector>(YoloDetector())
|
||||
├─ sl.registerSingleton<WebSocketService>(WebSocketService())
|
||||
├─ sl.registerSingleton<AgoraService>(AgoraService())
|
||||
└─ sl.registerSingleton<HapticService>(HapticService())
|
||||
|
||||
WalkGuideBloc ──sl<YoloDetector>()──► YoloDetector (instance sama)
|
||||
WalkGuideBloc ──sl<TtsService>()───► TtsService (instance sama)
|
||||
VoiceCommandHandler ──sl<TtsService>()──► TtsService (instance sama, bukan baru)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Facade Pattern — Structural
|
||||
|
||||
### Justifikasi
|
||||
|
||||
Memproses satu perintah suara `"Send SOS"` membutuhkan koordinasi: `SttService`, command matcher, `SosBloc`, GPS, `TtsService` feedback, dan `GoRouter` navigasi — 6 subsystem berbeda. Tanpa Facade, setiap Widget harus inject dan tahu semua 6 dependency tersebut. `VoiceCommandHandler` menyederhanakan ini menjadi satu panggilan `processText(string)`.
|
||||
|
||||
Di backend, `GuardianController` membutuhkan data dari 5 repository berbeda untuk render dashboard Guardian. `GuardianDashboardService` menjadi Facade yang mengagregasi semuanya, sehingga controller cukup memanggil satu method.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `VoiceCommandHandler` | Flutter — Service | **Facade** — menyembunyikan koordinasi 6 subsystem voice | Services block |
|
||||
| `WalkGuideBloc` | Flutter — BLoC | **Client** — hanya tahu `processText()`, tidak tahu subsystem | BLoC block |
|
||||
| `TtsService` | Flutter — Service | **Subsystem** — salah satu dari yang dikoordinasikan | Services block |
|
||||
| `SttService` | Flutter — Service | **Subsystem** — salah satu dari yang dikoordinasikan | Services block |
|
||||
| `VoiceCommandConfig` | Backend — Entity | **Data** — konfigurasi perintah suara yang dibaca Facade | Entity block |
|
||||
| `VoiceCommandKey` | Backend — Enum | **Enum** — key identifier 14 perintah suara | Enum block |
|
||||
| `GuardianDashboardService` | Backend — Service | **Facade** — mengagregasi data dari 5 repository | Service block |
|
||||
| `GuardianController` | Backend — Controller | **Client** — cukup panggil `getGuardianHomeData()` | Controller block |
|
||||
| `LocationHistoryRepository` | Backend — Repository | **Subsystem** — data lokasi terakhir User | Repository block |
|
||||
| `SosEventRepository` | Backend — Repository | **Subsystem** — data SOS aktif User | Repository block |
|
||||
| `ActivityLogRepository` | Backend — Repository | **Subsystem** — log aktivitas User | Repository block |
|
||||
| `GuardianNotificationRepository` | Backend — Repository | **Subsystem** — jumlah notif belum dibaca | Repository block |
|
||||
| `PairingRelationRepository` | Backend — Repository | **Subsystem** — ambil userId yang dipasangkan ke Guardian | Repository block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
Widget ──processText("send sos")──► VoiceCommandHandler (Facade)
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
SttService SosBloc TtsService
|
||||
(stop listen) (TriggerSos) (speak feedback)
|
||||
|
||||
GuardianController ──getGuardianHomeData()──► GuardianDashboardService (Facade)
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
▼ ▼ ▼
|
||||
LocationHistoryRepo SosEventRepo ActivityLogRepo
|
||||
(last location) (active SOS) (recent activity)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Repository / Proxy Pattern — Structural
|
||||
|
||||
### Justifikasi
|
||||
|
||||
WalkGuide harus berjalan **offline-first** — tunanetra sering berada di area tanpa sinyal. Log obstacle, activity log, dan update lokasi harus tetap tersimpan lokal (SQLite/Drift) saat offline, kemudian di-sync ke backend saat koneksi kembali. `WalkGuideBloc` tidak boleh tahu apakah data berasal dari API atau SQLite — keputusan ini ada di Repository Implementation yang bertindak sebagai Proxy.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `WalkGuideRepository` (interface) | Flutter — Domain | **Subject Interface** — kontrak yang dipakai BLoC | Domain layer |
|
||||
| `WalkGuideRepositoryImpl` | Flutter — Data | **Proxy** — memutuskan API vs SQLite | Data layer |
|
||||
| `ActivityLogRepository` (interface) | Flutter — Domain | **Subject Interface** | Domain layer |
|
||||
| `ActivityLogRepositoryImpl` | Flutter — Data | **Proxy** | Data layer |
|
||||
| `ObstacleLogRepository` | Backend — Repository | **Real Subject** — JPA repository backend | Repository block |
|
||||
| `ActivityLogRepository` (JPA) | Backend — Repository | **Real Subject** — JPA repository backend | Repository block |
|
||||
| `LocationHistoryRepository` | Backend — Repository | **Real Subject** — JPA repository backend | Repository block |
|
||||
| `ObstacleLog` | Backend — Entity | **Data** — entitas yang disimpan/diambil | Entity block |
|
||||
| `ActivityLog` | Backend — Entity | **Data** — entitas yang disimpan/diambil | Entity block |
|
||||
| `LocationHistory` | Backend — Entity | **Data** — riwayat lokasi User | Entity block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
WalkGuideBloc
|
||||
│
|
||||
│ depends on interface only
|
||||
▼
|
||||
WalkGuideRepository (interface)
|
||||
│ implements
|
||||
▼
|
||||
WalkGuideRepositoryImpl (Proxy)
|
||||
│
|
||||
├─ if (isOnline) ──► REST API ──► ObstacleLogRepository (JPA)
|
||||
│ │
|
||||
│ ▼
|
||||
│ ObstacleLog (Entity)
|
||||
│
|
||||
└─ else ────────► SQLite/Drift ──► Local cache
|
||||
(Drift ORM)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Observer Pattern — Behavioral
|
||||
|
||||
### Justifikasi
|
||||
|
||||
Saat YOLO mendeteksi obstacle, puluhan komponen perlu bereaksi: overlay bounding box, TTS feedback, haptic, backend log, dan debug panel. Menghubungkan semua ini secara langsung akan menciptakan tight coupling. BLoC pattern adalah implementasi Observer murni — `WalkGuideBloc` sebagai Subject, widget sebagai Observer. Ketika state baru di-emit, semua subscriber otomatis bereaksi tanpa BLoC perlu tahu siapa saja Observer-nya.
|
||||
|
||||
Untuk Guardian, `WebSocketService` adalah Observer terhadap backend STOMP — Guardian menerima update lokasi User real-time tanpa polling.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `WalkGuideBloc` | Flutter — BLoC | **Concrete Subject** — emit state saat obstacle terdeteksi | BLoC block |
|
||||
| `SosBloc` | Flutter — BLoC | **Concrete Subject** — emit state saat SOS triggered | BLoC block |
|
||||
| `BlocBuilder` (widget) | Flutter — UI | **Observer** — rebuild UI saat state berubah | Widget layer |
|
||||
| `BlocListener` (widget) | Flutter — UI | **Observer** — side effects tanpa rebuild UI | Widget layer |
|
||||
| `WebSocketService` | Flutter — Service | **Subject** — Stream lokasi, SOS, notifikasi | Services block |
|
||||
| `LocationBroadcaster` | Backend — Component | **Publisher** — push event ke STOMP topics | Backend block |
|
||||
| `GuardianNotificationRepository` | Backend — Repository | **Data Store** — notifikasi yang perlu di-broadcast | Repository block |
|
||||
| `SosEventRepository` | Backend — Repository | **Data Store** — SOS event yang di-broadcast ke Guardian | Repository block |
|
||||
| `ActivityLogService` | Backend — Service | **Publisher** — broadcast activity log via LocationBroadcaster | Service block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
YoloDetector ──detect()──► DetectionResult
|
||||
│
|
||||
WalkGuideBloc (Subject)
|
||||
emit(ObstacleDetected)
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
BlocBuilder BlocListener BlocConsumer
|
||||
(UI rebuild) (side effects) (both)
|
||||
overlay bbox SnackBar error combined
|
||||
|
||||
Backend STOMP push:
|
||||
LocationBroadcaster ──/topic/location/{userId}──► WebSocketService (Subject)
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
GuardianMap GuardianDash NotifScreen
|
||||
(live marker) (SOS alert) (new notif)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Strategy Pattern — Behavioral
|
||||
|
||||
### Justifikasi
|
||||
|
||||
Guardian perlu mengkonfigurasi **cara WalkGuide memperingatkan** User tentang obstacle. Ada tiga mode: TTS saja (cocok untuk lingkungan tenang), TTS + haptic (default untuk sebagian besar situasi), haptic saja (lingkungan bising seperti pasar atau stasiun). Logika ini bisa berubah kapan saja dari dashboard Guardian tanpa restart app. Strategy Pattern memungkinkan penggantian algoritma alert di runtime tanpa mengubah `ObstacleAnalyzer` sedikit pun.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `ObstacleAlertStrategy` (interface) | Flutter — AI | **Strategy Interface** — kontrak `alert(result)` | Domain/AI layer |
|
||||
| `TtsOnlyStrategy` | Flutter — AI | **Concrete Strategy 1** — hanya TTS | Strategy layer |
|
||||
| `TtsWithHapticStrategy` | Flutter — AI | **Concrete Strategy 2** — TTS + haptic | Strategy layer |
|
||||
| `HapticOnlyStrategy` | Flutter — AI | **Concrete Strategy 3** — hanya haptic | Strategy layer |
|
||||
| `ObstacleAnalyzer` | Flutter — AI | **Context** — menyimpan dan mendelegasikan ke strategy | AI block |
|
||||
| `AiConfig` | Backend — Entity | **Config Source** — menyimpan `alertMode` yang dipilih Guardian | Entity block |
|
||||
| `AiConfigService` | Backend — Service | **Config Manager** — update config dan kirim FCM ke User | Service block |
|
||||
| `AiConfigRepository` | Backend — Repository | **Persistence** — simpan/ambil konfigurasi AI | Repository block |
|
||||
| `TtsService` | Flutter — Service | **Subsystem** — dipakai oleh TtsOnly dan TtsWithHaptic | Services block |
|
||||
| `HapticService` | Flutter — Service | **Subsystem** — dipakai oleh TtsWithHaptic dan HapticOnly | Services block |
|
||||
| `FcmService` | Backend — Service | **Notifier** — kirim FCM ke User saat config berubah | Service block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
Guardian PUT /guardian/ai-config
|
||||
│
|
||||
▼
|
||||
AiConfigService ──updateConfig()──► AiConfig (simpan alertMode)
|
||||
│
|
||||
└──► FcmService.sendToUser() ──► FCM ke User: "ai_config_updated"
|
||||
│
|
||||
Flutter terima FCM
|
||||
│
|
||||
switch (config.alertMode)
|
||||
┌─────────┼──────────┐
|
||||
▼ ▼ ▼
|
||||
TtsOnly TtsHaptic HapticOnly
|
||||
Strategy Strategy Strategy
|
||||
│
|
||||
ObstacleAnalyzer.setStrategy(new)
|
||||
│
|
||||
_strategy.alert(detectionResult)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Chain of Responsibility Pattern — Behavioral
|
||||
|
||||
### Justifikasi
|
||||
|
||||
Setiap HTTP request dari Flutter perlu melewati beberapa proses berurutan: (1) menambahkan JWT, (2) auto-refresh jika expired, (3) error mapping terpusat, (4) logging. Tanpa Chain of Responsibility, setiap Repository harus menulis ulang logika ini. Di backend, setiap request perlu (1) divalidasi JWT, (2) dicek role-nya sebelum mencapai controller bisnis. Chain memungkinkan setiap handler fokus pada tanggung jawabnya sendiri dan meneruskan ke handler berikutnya.
|
||||
|
||||
### Class yang Terlibat
|
||||
|
||||
| Class | Layer | Peran dalam Pattern | Lokasi di Class Diagram |
|
||||
|-------|-------|---------------------|-------------------------|
|
||||
| `Interceptor` (abstract Dio) | Flutter — Network | **Abstract Handler** | Network layer |
|
||||
| `AuthInterceptor` | Flutter — Network | **Concrete Handler 1** — inject token, auto-refresh | Network layer |
|
||||
| `ErrorInterceptor` | Flutter — Network | **Concrete Handler 2** — map HTTP error ke Failure | Network layer |
|
||||
| `LogInterceptor` | Flutter — Network | **Concrete Handler 3** — logging request/response | Network layer |
|
||||
| `ApiClient` | Flutter — Network | **Chain Builder** — mengatur urutan dan membangun chain | Network layer |
|
||||
| `OncePerRequestFilter` (Spring) | Backend — Security | **Abstract Handler** | Security block |
|
||||
| `JwtAuthFilter` | Backend — Security | **Concrete Handler 1** — validasi JWT, set SecurityContext | Security block |
|
||||
| `SecurityConfig` | Backend — Security | **Concrete Handler 2** — RBAC routing per URL pattern | Security block |
|
||||
| `JwtUtil` | Backend — Security | **Component** — dipakai JwtAuthFilter untuk decode/validate | Security block |
|
||||
| `CustomUserDetailsService` | Backend — Security | **Component** — dipakai JwtAuthFilter untuk load user dari DB | Security block |
|
||||
| `UserRepository` | Backend — Repository | **Data Source** — dipakai CustomUserDetailsService | Repository block |
|
||||
| `GlobalExceptionHandler` | Backend — Exception | **Error Handler** — menangkap exception dari seluruh chain | Exception block |
|
||||
|
||||
### Relasi Antar Class
|
||||
|
||||
```
|
||||
Flutter HTTP Request
|
||||
│
|
||||
▼
|
||||
[Chain[0]] AuthInterceptor ──onRequest()──► inject "Bearer {token}" ──► next()
|
||||
│
|
||||
▼
|
||||
[Chain[1]] ErrorInterceptor ──onRequest()──► pass-through ──► next()
|
||||
│
|
||||
▼
|
||||
[Chain[2]] LogInterceptor ──onRequest()──► log URL + body ──► next()
|
||||
│
|
||||
▼ (HTTP over network)
|
||||
│
|
||||
[Backend] JwtAuthFilter ──doFilterInternal()──► validate JWT ──► chain.doFilter()
|
||||
│
|
||||
▼
|
||||
[Backend] SecurityConfig ──securityFilterChain()──► cek role ──► pass
|
||||
│
|
||||
▼
|
||||
[Backend] Controller ──handleRequest()──► proses bisnis
|
||||
│
|
||||
▼
|
||||
[Backend] GlobalExceptionHandler ──► tangkap exception jika ada
|
||||
│
|
||||
▼ (HTTP Response)
|
||||
│
|
||||
[Chain[2]] LogInterceptor.onResponse() ──► log response
|
||||
│
|
||||
▼
|
||||
[Chain[1]] ErrorInterceptor.onResponse() ──► map error jika 4xx/5xx
|
||||
│
|
||||
▼
|
||||
[Chain[0]] AuthInterceptor.onError() ──► jika 401: refresh token + retry
|
||||
│
|
||||
▼
|
||||
Repository / BLoC menerima Either<Failure, Data>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ringkasan Jumlah Class per Pattern
|
||||
|
||||
| Pattern | Jumlah Class Terlibat | Layer |
|
||||
|---------|----------------------|-------|
|
||||
| Builder | 5 class | Backend |
|
||||
| Singleton | 7 class | Flutter |
|
||||
| Facade | 13 class | Flutter + Backend |
|
||||
| Repository/Proxy | 10 class | Flutter + Backend |
|
||||
| Observer | 9 class | Flutter + Backend |
|
||||
| Strategy | 11 class | Flutter + Backend |
|
||||
| Chain of Responsibility | 12 class | Flutter + Backend |
|
||||
| **Total unik** | **±40 class** | **Full Stack** |
|
||||
|
||||
> Semua class di atas dapat ditelusuri langsung di **Class Diagram WalkGuide** (file PDF yang dilampirkan).
|
||||