Skip to content

Commit f48865b

Browse files
committed
grrrr
1 parent 754f458 commit f48865b

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Class Diagram Quick Start
2+
3+
## Access Modifiers
4+
5+
Access modifiers are used for marking members only.
6+
7+
- `-`: private
8+
- `+`: public
9+
- `#`: protected
10+
- `~`: package(for some languages that are not fully object-oriented)
11+
12+
## Relationships
13+
14+
Relationships are used for marking type only.
15+
16+
### Association
17+
18+
`-->` or `<--` or `<-->` for unidirectional and bidirectional. The weakest relationship.
19+
20+
> [!NOTE]
21+
> All relationships except inheritance and implementation can have bidirectional case.
22+
23+
### Inheritance
24+
25+
`<|--`: inheritance from left to right. `Child <|-- Parent` for example. Unidirectional only.
26+
27+
### Aggregation
28+
29+
`o--`: aggregation, type on the right are used inside the type on the left while left is independent. `Car o-- Wheel` for example.
30+
31+
### Composition
32+
33+
`*--`: composition, an extra rule for class diagram that marks the left as a whole and contains the right, both can't be reasonably exists alone, `Human *-- Heart`
34+
35+
### Implementation
36+
37+
`<|..`: the left implements the left interface.
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
# Observer
2+
3+
## Motivation
4+
5+
- To be informed when status changed
6+
- To be informed when certain things happened.
7+
- Listen to events and get notified when they occurs
8+
9+
- `INotifyPropertyChanged`
10+
- `INotifyPropertyChanging`
11+
- `IObservable<T>`
12+
- `IObserver<T>`
13+
- `ObervableCollection<T>`
14+
- `BindingList<T>`
15+
16+
> Observer is the object to be informed when event occurs
17+
> Observable is the object generating the event.
18+
19+
## By Event
20+
21+
C# has builtin event support. Use `+=` to subscribe a method.
22+
23+
> [!NOTE]
24+
> Event handler is the Observer
25+
> `Player` is the Observable
26+
27+
```cs
28+
Player player = new() { Id = 1 };
29+
Player enemy = new() { Id = 2 };
30+
31+
player.OnHurt += (object? sender, PlayerOnHurtArgs args) => // event handler method is the Observer// [!code highlight]
32+
Console.WriteLine($"Player {(sender as Player)?.Id ?? -1} get hurt by damage {args.Damage} from player {args.Id}"); // [!code highlight]
33+
34+
player.GetHurt(1, enemy);
35+
36+
class Player
37+
{
38+
public int Id { get; init; }
39+
public int Health { get; private set; } = 100;
40+
public event EventHandler<PlayerOnHurtArgs>? OnHurt;
41+
public void GetHurt(int damage, Player another)
42+
{
43+
Health -= damage;
44+
OnHurt?.Invoke(this, new(damage, another.Id)); // [!code highlight]
45+
}
46+
}
47+
48+
record PlayerOnHurtArgs(int Damage, int Id);
49+
```
50+
51+
## Weak Event
52+
53+
When a method of an instance was added to an event, the containing object of the method will never be collected by GC.
54+
55+
```cs
56+
Player player = new() { Id = 1 };
57+
Player? enemy = new() { Id = 2 };
58+
59+
player.OnHurt += enemy.OnHurtHandler; // subscribes a handler from another instance // [!code highlight]
60+
WeakReference<Player> enemyRef = new(enemy);
61+
enemy = null;
62+
GC.Collect(); // should enemy be collected? // [!code highlight]
63+
Console.WriteLine($"enemy is {(enemyRef.TryGetTarget(out _) ? "alive" : "collected")}"); // <- enemy is alive // [!code highlight]
64+
65+
class Player
66+
{
67+
public int Id { get; init; }
68+
public int Health { get; private set; } = 100;
69+
public event EventHandler<PlayerOnHurtArgs>? OnHurt;
70+
public void GetHurt(int damage, Player enemy)
71+
{
72+
Health -= damage;
73+
OnHurt?.Invoke(this, new(damage, enemy.Id));
74+
}
75+
public void OnHurtHandler(object? sender, PlayerOnHurtArgs args) // [!code ++]
76+
{ // [!code ++]
77+
Console.WriteLine($"Player {(sender as Player)?.Id ?? -1} get hurt by damage {args.Damage} from player {args.Id}"); // [!code ++]
78+
} // [!code ++]
79+
}
80+
81+
record PlayerOnHurtArgs(int Damage, int Id);
82+
```
83+
84+
That's why weak event pattern matters.
85+
```cs
86+
Player player = new() { Id = 1 };
87+
Player? enemy = new() { Id = 2 };
88+
89+
player.OnHurt += enemy.OnHurtHandler; // subscribes a handler from another instance // [!code --]
90+
WeakEventManager<Player, PlayerOnHurtArgs>.AddHandler(player. "OnHurt", enemy.OnHurtHandler) // [!code ++]
91+
WeakReference<Player> enemyRef = new(enemy);
92+
enemy = null;
93+
GC.Collect(); // should enemy be collected? // [!code highlight]
94+
Console.WriteLine($"enemy is {(enemyRef.TryGetTarget(out _) ? "alive" : "collected")}"); // <- enemy is collected // [!code highlight]
95+
96+
class Player
97+
{
98+
public int Id { get; init; }
99+
public int Health { get; private set; } = 100;
100+
public event EventHandler<PlayerOnHurtArgs>? OnHurt;
101+
public void GetHurt(int damage, Player enemy)
102+
{
103+
Health -= damage;
104+
OnHurt?.Invoke(this, new(damage, enemy.Id));
105+
}
106+
public void OnHurtHandler(object? sender, PlayerOnHurtArgs args) // [!code ++]
107+
{ // [!code ++]
108+
Console.WriteLine($"Player {(sender as Player)?.Id ?? -1} get hurt by damage {args.Damage} from player {args.Id}"); // [!code ++]
109+
} // [!code ++]
110+
}
111+
112+
record PlayerOnHurtArgs(int Damage, int Id);
113+
```
114+
> [!WARNING]
115+
> `WeakEventManager` is not available in .NET Core
116+
117+
## Observer and Observable
118+
119+
The major downside of event in C# is **event leak**, which happens when event handlers remain subscribed and prevent objects from being garbage collected, leading to memory leaks.
120+
121+
.NET has builtin types to implement the same event mechanism with `System.IObserver<T>` and `System.IObservable<T>`
122+
123+
```mermaid
124+
classDiagram
125+
class IObserver~T~ {
126+
<<interface>>
127+
+OnNext(value: T) void
128+
}
129+
class IObservable~T~ {
130+
<<interface>>
131+
+Subscribe(observer: IObserver~T~) IDisposable
132+
}
133+
class IDisposable {
134+
<<interface>>
135+
+Dispose() void
136+
}
137+
class PlayerObserver{
138+
+OnNext(value: PlayerEventArgs) void
139+
}
140+
class Player {
141+
-subscriptions: HashSet~Subscription~
142+
+Subscribe(observer: IObserver~PlayerEventArgs~) IDisposable
143+
+Attack(enemy: Player, damage: int) void
144+
}
145+
class PlayerSubscription {
146+
-player: Player
147+
+Observer: IObserver~PlayerEventArgs~
148+
+Dispose() void
149+
}
150+
class PlayerEventArgs {
151+
+PlayerId: int
152+
}
153+
class OnAttackEventArgs {
154+
+PlayerId: int
155+
+EnemyId: int
156+
+Damage: int
157+
}
158+
Player ..|> IObservable
159+
Player ..> PlayerEventArgs
160+
Player ..> IObserver
161+
PlayerObserver ..|> IObserver
162+
PlayerObserver ..> PlayerEventArgs
163+
PlayerSubscription ..|> IDisposable
164+
PlayerSubscription <.. Player
165+
PlayerEventArgs <|-- OnAttackEventArgs
166+
```
167+
168+
- `Subscribe` acts like `+=`
169+
- `Dispose` acts like `-=`
170+
171+
```cs
172+
Player player = new() { Id = 1 };
173+
Player? enemy = new() { Id = 2 };
174+
var subscriber = new PlayerObserver();
175+
using var _ = player.Subscribe(subscriber);
176+
player.Attack(enemy, 100); // [!code highlight]
177+
178+
class Player : IObservable<PlayerEventArgs>
179+
{
180+
public int Id { get; init; }
181+
public int Health { get; private set; } = 100;
182+
private readonly HashSet<Subscription> subscriptions = []; // events as collections // [!code highlight]
183+
184+
// subscribes an event just like `+=` when using builtin event // [!code highlight]
185+
public IDisposable Subscribe(IObserver<PlayerEventArgs> observer) // [!code highlight]
186+
{
187+
var subscription = new Subscription(this, observer);
188+
subscriptions.Add(subscription);
189+
return subscription;
190+
}
191+
192+
public void Attack(Player enemy, int damage)
193+
{
194+
// ...
195+
foreach (var sub in subscriptions)
196+
{
197+
sub.Observer.OnNext(new OnAttackEventArgs { EnemyId = enemy.Id, Damage = damage, PlayerId = Id });
198+
}
199+
}
200+
// a subscription should know which one is being subscribed and who is the observer. // [!code highlight]
201+
// observer take a `PlayerEventArgs` which is a base EventArgs, allowing different type of subscription to be inside `subscriptions`
202+
private class Subscription(Player player, IObserver<PlayerEventArgs> observer) : IDisposable // [!code highlight]
203+
{
204+
private readonly Player player = player;
205+
public IObserver<PlayerEventArgs> Observer { get; } = observer;
206+
207+
public void Dispose()
208+
{
209+
player.subscriptions.Remove(this);
210+
}
211+
}
212+
}
213+
214+
class PlayerEventArgs
215+
{
216+
public int PlayerId { get; set; }
217+
}
218+
class OnAttackEventArgs : PlayerEventArgs
219+
{
220+
public int EnemyId { get; set; }
221+
public int Damage { get; set; }
222+
}
223+
class PlayerObserver : IObserver<PlayerEventArgs>
224+
{
225+
public void OnCompleted() { }
226+
227+
public void OnError(Exception error) { }
228+
229+
public void OnNext(PlayerEventArgs value)
230+
{
231+
if (value is OnAttackEventArgs args)
232+
{
233+
Console.WriteLine($"Enemy id: {args.EnemyId} was attacked by player id: {args.PlayerId} with damage {args.Damage}");
234+
}
235+
// ...
236+
}
237+
}
238+
```

0 commit comments

Comments
 (0)