Sunday 28 July 2013

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;
 }
 
}
GPSListener.java
1
2
3
4
5
6
7
public interface GPSListener {
 
 void positionUpdated(Position position);
 
 void statusUpdated(int status);
 
}
LBSPositionObserver.java – native GPS interface
1
2
3
4
5
6
7
public interface LBSPositionObserver {
 
 void positionUpdated(Position position);
 
 void setStatus(int status);
 
}
Navigator.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
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.
 }
}
Now, since I’ve finished my Navigator module, I want to test it to make sure it’s working correctly. In order to test the Navigator module, I need a GPS data log and a route to walk through it, simulating a person walking and being navigated by the system. The route is not a problem because I pass it as a parameter of “navigate” method. However, there is no way to simulate a GPS log with the code above, unless I use some Dependency Injection Framework, which is not the case.
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);
 }
 }
 
}
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
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;
 }
 
}
Navigator.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
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.
 }
}
Much better! Notice that I still have just one single instance of my GPS class. All I have to do is creating my GPS at the beginning of the program and pass it through classes that use it.
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) {
 (...)
 }
 
}
As you can see, I can use any GPS provider in my tests, making them much more easy and flexible.
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