Singleton (Anti-) Pattern
I’ve implemented my first design patterns at college, while creating a web system during the software engineer course. My classmates and I needed a facade class with a single instance of it throughout the system. So, because we’re really smart, we’ve applied the Facade and Singleton pattern. Actually, we’ve implemented the patterns without knowledge of the design patterns catalog. For us, it was just a way to get what we need.This week, while doing my daily work, I realized how dangerous the Singleton pattern is. Well, I would have noticed it before, if I have used Test-First Programming. Even knowing the benefits of TDD, I decided to write some code in advance, since I don’t have much experience on the platform used and on its supported tests framework.
My task was to create a GPS abstraction and use it in a mobile Navigator module. My first thought was making my GPS abstraction a Singleton class. It looked very obvious for me: a mobile device has only one GPS and different GPS intances will provide the same data set. So, what I need is just a single GPS instance troughout the system.
After some minutes writing code, I’ve created abstractions similar to the Java sample below.
GPSProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| import java.util.List; public class GPSProvider implements LBSPositionObserver { private static GPSProvider INSTANCE = new GPSProvider(); private Position lastCoordinate; private List<GPSListener> listeners; private GPSProvider() { this .listeners = new ArrayList<GPSListener>(); } //retrive the class instance public static GPSProvider GetInstance() { return INSTANCE; } // perform GPS initialization public void initGPSService() { (...) } // add a new listener to the gps class public void attach(GPSListener listener) { this .listeners.add(listener); } // remove a listener from the class list public void dettach(GPSListener listener) { this .listeners.remove(listener); } @Override // set lastCoordinate and notify all listeners about the update public void positionUpdated(Position position) { this .lastCoordinate = position; for ( GPSListener listener : this .listeners ) { listener.positionUpdated(position); } } @Override // notify all listener about the update public void setStatus( int status) { for ( GPSListener listener : this .listeners ) { listener.statusUpdated(status); } } // return the last known coordinate public Position getLastCoordinate() { return this .lastCoordinate; } } |
1
2
3
4
5
6
7
| public interface GPSListener { void positionUpdated(Position position); void statusUpdated( int status); } |
1
2
3
4
5
6
7
| public interface LBSPositionObserver { void positionUpdated(Position position); void setStatus( int status); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| public class Navigator implements GPSListener { private NavigatorObserver observer; public Navigator(NavigatorObserver observer) { this .observer = observer; } // start navigation by listening to gps updates public void navigate(Route route) { GPSProvider gps = GPSProvider.GetInstance(); gps.attach( this ); } // pause navigation by not receiving gps updates public void pause() { GPSProvider gps = GPSProvider.GetInstance(); gps.dettach( this ); } @Override // everytime the position is updated, the navigator gives directions if needed public void positionUpdated(Position position) { // navigate user through route. int step = this .verifyStepUpdated(); if ( step != - 1 ) { this .observer.StepUpdated(step); } if ( this .achievedDestination() ) { this .observer.DestinationAchived(position); } } // calculate if achieved destination private boolean achievedDestination() { (...) } // verify is have to change direction private int verifyStepUpdated() { (...) } @Override public void statusUpdated( int status) { // send status to end user. } } |
That’s the Singleton disadvantage. I can’t inject a GPS mock in my Navigator module because I always use the native gps implementation represented in my Singleton GPSProvider class. Everytime I need some GPS information, I use the GPSProvider.GetInstance() static method to retrieve the only GPS instance I have access to.
To solve this problem, I found a simple solution: not using Singleton. I removed the Singleton pattern from GPSProvider and change every class that uses GPSProvider.GetInstance() to receive the current GPS intance. In the Navigator module, I passed the GPS instance through its class’ constructor.
GPSAbstractProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| import java.util.ArrayList; import java.util.List; public abstract class GPSAbstractProvider { private List<GPSListener> listeners; public GPSAbstractProvider() { this .listeners = new ArrayList<GPSListener>(); } public abstract void initGPSService(); public void attach(GPSListener listener) { this .listeners.add(listener); } public void dettach(GPSListener listener) { this .listeners.remove(listener); } public void notifyPositionUpdated(Position position) { for ( GPSListener listener : this .listeners ) { listener.positionUpdated(position); } } public void notifyStatusUpdated( int status) { for ( GPSListener listener : this .listeners ) { listener.statusUpdated(status); } } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class GPSProvider extends GPSAbstractProvider implements LBSPositionObserver { private Position lastCoordinate; private GPSProvider() { super (); } @Override // perform GPS initialization public void initGPSService() { (...) } @Override public void positionUpdated(Position position) { this .lastCoordinate = position; this .notifyPositionUpdated(position); } @Override public void setStatus( int status) { this .notifyStatusUpdated(status); } public Position getLastCoordinate() { return this .lastCoordinate; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| public class Navigator implements GPSListener { private NavigatorObserver observer; private GPSAbstractProvider gps; public Navigator(NavigatorObserver observer, GPSAbstractProvider gps) { this .observer = observer; this .gps = gps; } public void navigate(Route route) { this .gps.attach( this ); } public void pause() { this .gps.dettach( this ); } @Override public void positionUpdated(Position position) { // navigate user through route. int step = this .verifyStepUpdated(); if ( step != - 1 ) { this .observer.StepUpdated(step); } if ( this .achievedDestination() ) { this .observer.DestinationAchived(position); } } private boolean achievedDestination() { (...) } private int verifyStepUpdated() { (...) } @Override public void statusUpdated( int status) { // send status to end user. } } |
Doing this, testing my code became very simple. I inject my GPS mock to simulate GPS data in my tests, just as below.
NavigatorTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| import java.util.ArrayList; import java.util.List; import junit.framework.TestCase; public class NavigatorTest extends TestCase implements NavigatorObserver { private Navigator navigator; private GPSAbstractProvider gps; public void testNavigate() throws Exception { List<Position> gpsPositions = new ArrayList<Position>(); this .addPositions(gpsPositions); this .gps = new GPSTestProvider(gpsPositions); this .navigator = new Navigator( this , this .gps); this .navigator.navigate( this .createSimpleRoute()); gps.initGPSService(); assert (...); assert (...); } private Route createSimpleRoute() { (...) } private void addPositions(List<Position> gpsPositions) { (...) } @Override public void DestinationAchived(Position destination) { (...) } @Override public void StepUpdated( int step) { (...) } } |
In addition to the disavantage presented, Alex Miller discuss some points why he hates Singleton pattern.
From here, trying avoiding this pattern in situations like that. Think twice before applying Singleton in your project.
See you
No comments:
Post a Comment