Browser docs

Special Case

Intent

Define some special cases, and encapsulates them into subclasses that provide different special behaviors.

Explanation

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    

Class diagram

alt text

Applicability

Use the Special Case pattern when

  • You have multiple places in the system that have the same behavior after a conditional check for a particular class instance, or the same behavior after a null check.
  • Return a real object that performs the real behavior, instead of a null object that performs nothing.

Tutorial

Credits