Skip to content

Commit dea7ecf

Browse files
ignite1771ohbus
andauthored
iluwatar#1317 Special Case Pattern (iluwatar#1624)
* iluwatar#1317 Add Special Case Pattern To focus on pattern itself, I implement DB and maintenance lock by the singleton instance. * iluwatar#1317 Add special cases unit tests Assert the logger output (ref: https://stackoverflow.com/a/52229629) * iluwatar#1317 Add README.md Add Special Case Pattern README * iluwatar#1317 Format: add a new line to end of file Co-authored-by: Subhrodip Mohanta <subhrodipmohanta@gmail.com>
1 parent bbc4fdf commit dea7ecf

21 files changed

+1084
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@
200200
<module>factory</module>
201201
<module>separated-interface</module>
202202
<module>data-transfer-object-enum-impl</module>
203+
<module>special-case</module>
203204
</modules>
204205

205206
<repositories>

special-case/README.md

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
---
2+
layout: pattern
3+
title: Special Case
4+
folder: special-case
5+
permalink: /patterns/special-case/
6+
categories: Behavioral
7+
tags:
8+
- Extensibility
9+
---
10+
11+
## Intent
12+
13+
Define some special cases, and encapsulates them into subclasses that provide different special behaviors.
14+
15+
## Explanation
16+
17+
Real world example
18+
19+
> In an e-commerce system, presentation layer expects application layer to produce certain view model.
20+
> We have a successful scenario, in which receipt view model contains actual data from the purchase,
21+
> and a couple of failure scenarios.
22+
23+
In plain words
24+
25+
> Special Case pattern allows returning non-null real objects that perform special behaviors.
26+
27+
In [Patterns of Enterprise Application Architecture](https://martinfowler.com/books/eaa.html) says
28+
the difference from Null Object Pattern
29+
30+
> If you’ll pardon the unresistable pun, I see Null Object as special case of Special Case.
31+
32+
**Programmatic Example**
33+
34+
To focus on the pattern itself, we implement DB and maintenance lock of the e-commerce system by the singleton instance.
35+
36+
```java
37+
public class Db {
38+
private static Db instance;
39+
private Map<String, User> userName2User;
40+
private Map<User, Account> user2Account;
41+
private Map<String, Product> itemName2Product;
42+
43+
public static Db getInstance() {
44+
if (instance == null) {
45+
synchronized (Db.class) {
46+
if (instance == null) {
47+
instance = new Db();
48+
instance.userName2User = new HashMap<>();
49+
instance.user2Account = new HashMap<>();
50+
instance.itemName2Product = new HashMap<>();
51+
}
52+
}
53+
}
54+
return instance;
55+
}
56+
57+
public void seedUser(String userName, Double amount) {
58+
User user = new User(userName);
59+
instance.userName2User.put(userName, user);
60+
Account account = new Account(amount);
61+
instance.user2Account.put(user, account);
62+
}
63+
64+
public void seedItem(String itemName, Double price) {
65+
Product item = new Product(price);
66+
itemName2Product.put(itemName, item);
67+
}
68+
69+
public User findUserByUserName(String userName) {
70+
if (!userName2User.containsKey(userName)) {
71+
return null;
72+
}
73+
return userName2User.get(userName);
74+
}
75+
76+
public Account findAccountByUser(User user) {
77+
if (!user2Account.containsKey(user)) {
78+
return null;
79+
}
80+
return user2Account.get(user);
81+
}
82+
83+
public Product findProductByItemName(String itemName) {
84+
if (!itemName2Product.containsKey(itemName)) {
85+
return null;
86+
}
87+
return itemName2Product.get(itemName);
88+
}
89+
90+
public class User {
91+
private String userName;
92+
93+
public User(String userName) {
94+
this.userName = userName;
95+
}
96+
97+
public String getUserName() {
98+
return userName;
99+
}
100+
101+
public ReceiptDto purchase(Product item) {
102+
return new ReceiptDto(item.getPrice());
103+
}
104+
}
105+
106+
public class Account {
107+
private Double amount;
108+
109+
public Account(Double amount) {
110+
this.amount = amount;
111+
}
112+
113+
public MoneyTransaction withdraw(Double price) {
114+
if (price > amount) {
115+
return null;
116+
}
117+
return new MoneyTransaction(amount, price);
118+
}
119+
120+
public Double getAmount() {
121+
return amount;
122+
}
123+
}
124+
125+
public class Product {
126+
private Double price;
127+
128+
public Product(Double price) {
129+
this.price = price;
130+
}
131+
132+
public Double getPrice() {
133+
return price;
134+
}
135+
}
136+
}
137+
138+
public class MaintenanceLock {
139+
private static final Logger LOGGER = LoggerFactory.getLogger(MaintenanceLock.class);
140+
141+
private static MaintenanceLock instance;
142+
private boolean lock = true;
143+
144+
public static MaintenanceLock getInstance() {
145+
if (instance == null) {
146+
synchronized (MaintenanceLock.class) {
147+
if (instance == null) {
148+
instance = new MaintenanceLock();
149+
}
150+
}
151+
}
152+
return instance;
153+
}
154+
155+
public boolean isLock() {
156+
return lock;
157+
}
158+
159+
public void setLock(boolean lock) {
160+
this.lock = lock;
161+
LOGGER.info("Maintenance lock is set to: " + lock);
162+
}
163+
}
164+
```
165+
166+
Let's first introduce presentation layer, the receipt view model interface and its implementation of successful scenario.
167+
168+
```java
169+
public interface ReceiptViewModel {
170+
void show();
171+
}
172+
173+
public class ReceiptDto implements ReceiptViewModel {
174+
175+
private static final Logger LOGGER = LoggerFactory.getLogger(ReceiptDto.class);
176+
177+
private Double price;
178+
179+
public ReceiptDto(Double price) {
180+
this.price = price;
181+
}
182+
183+
public Double getPrice() {
184+
return price;
185+
}
186+
187+
@Override
188+
public void show() {
189+
LOGGER.info("Receipt: " + price + " paid");
190+
}
191+
}
192+
```
193+
194+
And here are the implementations of failure scenarios, which are the special cases.
195+
196+
```java
197+
public class DownForMaintenance implements ReceiptViewModel {
198+
private static final Logger LOGGER = LoggerFactory.getLogger(DownForMaintenance.class);
199+
200+
@Override
201+
public void show() {
202+
LOGGER.info("Down for maintenance");
203+
}
204+
}
205+
206+
public class InvalidUser implements ReceiptViewModel {
207+
private static final Logger LOGGER = LoggerFactory.getLogger(InvalidUser.class);
208+
209+
private final String userName;
210+
211+
public InvalidUser(String userName) {
212+
this.userName = userName;
213+
}
214+
215+
@Override
216+
public void show() {
217+
LOGGER.info("Invalid user: " + userName);
218+
}
219+
}
220+
221+
public class OutOfStock implements ReceiptViewModel {
222+
223+
private static final Logger LOGGER = LoggerFactory.getLogger(OutOfStock.class);
224+
225+
private String userName;
226+
private String itemName;
227+
228+
public OutOfStock(String userName, String itemName) {
229+
this.userName = userName;
230+
this.itemName = itemName;
231+
}
232+
233+
@Override
234+
public void show() {
235+
LOGGER.info("Out of stock: " + itemName + " for user = " + userName + " to buy");
236+
}
237+
}
238+
239+
public class InsufficientFunds implements ReceiptViewModel {
240+
private static final Logger LOGGER = LoggerFactory.getLogger(InsufficientFunds.class);
241+
242+
private String userName;
243+
private Double amount;
244+
private String itemName;
245+
246+
public InsufficientFunds(String userName, Double amount, String itemName) {
247+
this.userName = userName;
248+
this.amount = amount;
249+
this.itemName = itemName;
250+
}
251+
252+
@Override
253+
public void show() {
254+
LOGGER.info("Insufficient funds: " + amount + " of user: " + userName
255+
+ " for buying item: " + itemName);
256+
}
257+
}
258+
```
259+
260+
Second, here's the application layer, the application services implementation and the domain services implementation.
261+
262+
```java
263+
public class ApplicationServicesImpl implements ApplicationServices {
264+
private DomainServicesImpl domain = new DomainServicesImpl();
265+
266+
@Override
267+
public ReceiptViewModel loggedInUserPurchase(String userName, String itemName) {
268+
if (isDownForMaintenance()) {
269+
return new DownForMaintenance();
270+
}
271+
return this.domain.purchase(userName, itemName);
272+
}
273+
274+
private boolean isDownForMaintenance() {
275+
return MaintenanceLock.getInstance().isLock();
276+
}
277+
}
278+
279+
public class DomainServicesImpl implements DomainServices {
280+
public ReceiptViewModel purchase(String userName, String itemName) {
281+
Db.User user = Db.getInstance().findUserByUserName(userName);
282+
if (user == null) {
283+
return new InvalidUser(userName);
284+
}
285+
286+
Db.Account account = Db.getInstance().findAccountByUser(user);
287+
return purchase(user, account, itemName);
288+
}
289+
290+
private ReceiptViewModel purchase(Db.User user, Db.Account account, String itemName) {
291+
Db.Product item = Db.getInstance().findProductByItemName(itemName);
292+
if (item == null) {
293+
return new OutOfStock(user.getUserName(), itemName);
294+
}
295+
296+
ReceiptDto receipt = user.purchase(item);
297+
MoneyTransaction transaction = account.withdraw(receipt.getPrice());
298+
if (transaction == null) {
299+
return new InsufficientFunds(user.getUserName(), account.getAmount(), itemName);
300+
}
301+
302+
return receipt;
303+
}
304+
}
305+
```
306+
307+
Finally, the client send requests the application services to get the presentation view.
308+
309+
```java
310+
// DB seeding
311+
LOGGER.info("Db seeding: " + "1 user: {\"ignite1771\", amount = 1000.0}, "
312+
+ "2 products: {\"computer\": price = 800.0, \"car\": price = 20000.0}");
313+
Db.getInstance().seedUser("ignite1771", 1000.0);
314+
Db.getInstance().seedItem("computer", 800.0);
315+
Db.getInstance().seedItem("car", 20000.0);
316+
317+
var applicationServices = new ApplicationServicesImpl();
318+
ReceiptViewModel receipt;
319+
320+
LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
321+
receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
322+
receipt.show();
323+
MaintenanceLock.getInstance().setLock(false);
324+
LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
325+
receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
326+
receipt.show();
327+
LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "tv");
328+
receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv");
329+
receipt.show();
330+
LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "car");
331+
receipt = applicationServices.loggedInUserPurchase("ignite1771", "car");
332+
receipt.show();
333+
LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "computer");
334+
receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer");
335+
receipt.show();
336+
```
337+
338+
Program output of every request:
339+
340+
```
341+
Down for maintenance
342+
Invalid user: abc123
343+
Out of stock: tv for user = ignite1771 to buy
344+
Insufficient funds: 1000.0 of user: ignite1771 for buying item: car
345+
Receipt: 800.0 paid
346+
```
347+
348+
## Class diagram
349+
350+
![alt text](./etc/special_case_urm.png "Special Case")
351+
352+
## Applicability
353+
354+
Use the Special Case pattern when
355+
356+
* You have multiple places in the system that have the same behavior after a conditional check
357+
for a particular class instance, or the same behavior after a null check.
358+
* Return a real object that performs the real behavior, instead of a null object that performs nothing.
359+
360+
## Tutorial
361+
362+
* [Special Case Tutorial](https://www.codinghelmet.com/articles/reduce-cyclomatic-complexity-special-case)
363+
364+
## Credits
365+
366+
* [How to Reduce Cyclomatic Complexity Part 2: Special Case Pattern](https://www.codinghelmet.com/articles/reduce-cyclomatic-complexity-special-case)
367+
* [Patterns of Enterprise Application Architecture](https://martinfowler.com/books/eaa.html)
368+
* [Special Case](https://www.martinfowler.com/eaaCatalog/specialCase.html)

0 commit comments

Comments
 (0)