Define some special cases, and encapsulates them into subclasses that provide different special behaviors.
Real world example
In an e-commerce system, presentation layer expects application layer to produce certain view model. We have a successful scenario, in which receipt view model contains actual data from the purchase, and a couple of failure scenarios.
In plain words
Special Case pattern allows returning non-null real objects that perform special behaviors.
In Patterns of Enterprise Application Architecture says the difference from Null Object Pattern
If you’ll pardon the unresistable pun, I see Null Object as special case of Special Case.
Programmatic Example
To focus on the pattern itself, we implement DB and maintenance lock of the e-commerce system by the singleton instance.
1public class Db {
2 private static Db instance;
3 private Map<String, User> userName2User;
4 private Map<User, Account> user2Account;
5 private Map<String, Product> itemName2Product;
6
7 public static Db getInstance() {
8 if (instance == null) {
9 synchronized (Db.class) {
10 if (instance == null) {
11 instance = new Db();
12 instance.userName2User = new HashMap<>();
13 instance.user2Account = new HashMap<>();
14 instance.itemName2Product = new HashMap<>();
15 }
16 }
17 }
18 return instance;
19 }
20
21 public void seedUser(String userName, Double amount) {
22 User user = new User(userName);
23 instance.userName2User.put(userName, user);
24 Account account = new Account(amount);
25 instance.user2Account.put(user, account);
26 }
27
28 public void seedItem(String itemName, Double price) {
29 Product item = new Product(price);
30 itemName2Product.put(itemName, item);
31 }
32
33 public User findUserByUserName(String userName) {
34 if (!userName2User.containsKey(userName)) {
35 return null;
36 }
37 return userName2User.get(userName);
38 }
39
40 public Account findAccountByUser(User user) {
41 if (!user2Account.containsKey(user)) {
42 return null;
43 }
44 return user2Account.get(user);
45 }
46
47 public Product findProductByItemName(String itemName) {
48 if (!itemName2Product.containsKey(itemName)) {
49 return null;
50 }
51 return itemName2Product.get(itemName);
52 }
53
54 public class User {
55 private String userName;
56
57 public User(String userName) {
58 this.userName = userName;
59 }
60
61 public String getUserName() {
62 return userName;
63 }
64
65 public ReceiptDto purchase(Product item) {
66 return new ReceiptDto(item.getPrice());
67 }
68 }
69
70 public class Account {
71 private Double amount;
72
73 public Account(Double amount) {
74 this.amount = amount;
75 }
76
77 public MoneyTransaction withdraw(Double price) {
78 if (price > amount) {
79 return null;
80 }
81 return new MoneyTransaction(amount, price);
82 }
83
84 public Double getAmount() {
85 return amount;
86 }
87 }
88
89 public class Product {
90 private Double price;
91
92 public Product(Double price) {
93 this.price = price;
94 }
95
96 public Double getPrice() {
97 return price;
98 }
99 }
100}
101
102public class MaintenanceLock {
103 private static final Logger LOGGER = LoggerFactory.getLogger(MaintenanceLock.class);
104
105 private static MaintenanceLock instance;
106 private boolean lock = true;
107
108 public static MaintenanceLock getInstance() {
109 if (instance == null) {
110 synchronized (MaintenanceLock.class) {
111 if (instance == null) {
112 instance = new MaintenanceLock();
113 }
114 }
115 }
116 return instance;
117 }
118
119 public boolean isLock() {
120 return lock;
121 }
122
123 public void setLock(boolean lock) {
124 this.lock = lock;
125 LOGGER.info("Maintenance lock is set to: " + lock);
126 }
127}
Let’s first introduce presentation layer, the receipt view model interface and its implementation of successful scenario.
1public interface ReceiptViewModel {
2 void show();
3}
4
5public class ReceiptDto implements ReceiptViewModel {
6
7 private static final Logger LOGGER = LoggerFactory.getLogger(ReceiptDto.class);
8
9 private Double price;
10
11 public ReceiptDto(Double price) {
12 this.price = price;
13 }
14
15 public Double getPrice() {
16 return price;
17 }
18
19 @Override
20 public void show() {
21 LOGGER.info("Receipt: " + price + " paid");
22 }
23}
And here are the implementations of failure scenarios, which are the special cases.
1public class DownForMaintenance implements ReceiptViewModel {
2 private static final Logger LOGGER = LoggerFactory.getLogger(DownForMaintenance.class);
3
4 @Override
5 public void show() {
6 LOGGER.info("Down for maintenance");
7 }
8}
9
10public class InvalidUser implements ReceiptViewModel {
11 private static final Logger LOGGER = LoggerFactory.getLogger(InvalidUser.class);
12
13 private final String userName;
14
15 public InvalidUser(String userName) {
16 this.userName = userName;
17 }
18
19 @Override
20 public void show() {
21 LOGGER.info("Invalid user: " + userName);
22 }
23}
24
25public class OutOfStock implements ReceiptViewModel {
26
27 private static final Logger LOGGER = LoggerFactory.getLogger(OutOfStock.class);
28
29 private String userName;
30 private String itemName;
31
32 public OutOfStock(String userName, String itemName) {
33 this.userName = userName;
34 this.itemName = itemName;
35 }
36
37 @Override
38 public void show() {
39 LOGGER.info("Out of stock: " + itemName + " for user = " + userName + " to buy");
40 }
41}
42
43public class InsufficientFunds implements ReceiptViewModel {
44 private static final Logger LOGGER = LoggerFactory.getLogger(InsufficientFunds.class);
45
46 private String userName;
47 private Double amount;
48 private String itemName;
49
50 public InsufficientFunds(String userName, Double amount, String itemName) {
51 this.userName = userName;
52 this.amount = amount;
53 this.itemName = itemName;
54 }
55
56 @Override
57 public void show() {
58 LOGGER.info("Insufficient funds: " + amount + " of user: " + userName
59 + " for buying item: " + itemName);
60 }
61}
Second, here’s the application layer, the application services implementation and the domain services implementation.
1public class ApplicationServicesImpl implements ApplicationServices {
2 private DomainServicesImpl domain = new DomainServicesImpl();
3
4 @Override
5 public ReceiptViewModel loggedInUserPurchase(String userName, String itemName) {
6 if (isDownForMaintenance()) {
7 return new DownForMaintenance();
8 }
9 return this.domain.purchase(userName, itemName);
10 }
11
12 private boolean isDownForMaintenance() {
13 return MaintenanceLock.getInstance().isLock();
14 }
15}
16
17public class DomainServicesImpl implements DomainServices {
18 public ReceiptViewModel purchase(String userName, String itemName) {
19 Db.User user = Db.getInstance().findUserByUserName(userName);
20 if (user == null) {
21 return new InvalidUser(userName);
22 }
23
24 Db.Account account = Db.getInstance().findAccountByUser(user);
25 return purchase(user, account, itemName);
26 }
27
28 private ReceiptViewModel purchase(Db.User user, Db.Account account, String itemName) {
29 Db.Product item = Db.getInstance().findProductByItemName(itemName);
30 if (item == null) {
31 return new OutOfStock(user.getUserName(), itemName);
32 }
33
34 ReceiptDto receipt = user.purchase(item);
35 MoneyTransaction transaction = account.withdraw(receipt.getPrice());
36 if (transaction == null) {
37 return new InsufficientFunds(user.getUserName(), account.getAmount(), itemName);
38 }
39
40 return receipt;
41 }
42}
Finally, the client send requests the application services to get the presentation view.
1 // DB seeding
2 LOGGER.info("Db seeding: " + "1 user: {\"ignite1771\", amount = 1000.0}, "
3 + "2 products: {\"computer\": price = 800.0, \"car\": price = 20000.0}");
4 Db.getInstance().seedUser("ignite1771", 1000.0);
5 Db.getInstance().seedItem("computer", 800.0);
6 Db.getInstance().seedItem("car", 20000.0);
7
8 var applicationServices = new ApplicationServicesImpl();
9 ReceiptViewModel receipt;
10
11 LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
12 receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
13 receipt.show();
14 MaintenanceLock.getInstance().setLock(false);
15 LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
16 receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
17 receipt.show();
18 LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "tv");
19 receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv");
20 receipt.show();
21 LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "car");
22 receipt = applicationServices.loggedInUserPurchase("ignite1771", "car");
23 receipt.show();
24 LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "computer");
25 receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer");
26 receipt.show();
Program output of every request:
Down for maintenance
Invalid user: abc123
Out of stock: tv for user = ignite1771 to buy
Insufficient funds: 1000.0 of user: ignite1771 for buying item: car
Receipt: 800.0 paid
Use the Special Case pattern when