* #1317 Add Special Case Pattern To focus on pattern itself, I implement DB and maintenance lock by the singleton instance. * #1317 Add special cases unit tests Assert the logger output (ref: https://stackoverflow.com/a/52229629) * #1317 Add README.md Add Special Case Pattern README * #1317 Format: add a new line to end of file Co-authored-by: Subhrodip Mohanta <subhrodipmohanta@gmail.com>
This commit is contained in:
		
							
								
								
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							@@ -200,6 +200,7 @@
 | 
			
		||||
        <module>factory</module>
 | 
			
		||||
        <module>separated-interface</module>
 | 
			
		||||
        <module>data-transfer-object-enum-impl</module>
 | 
			
		||||
        <module>special-case</module>
 | 
			
		||||
    </modules>
 | 
			
		||||
 | 
			
		||||
    <repositories>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										368
									
								
								special-case/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										368
									
								
								special-case/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,368 @@
 | 
			
		||||
---
 | 
			
		||||
layout: pattern
 | 
			
		||||
title: Special Case
 | 
			
		||||
folder: special-case
 | 
			
		||||
permalink: /patterns/special-case/
 | 
			
		||||
categories: Behavioral
 | 
			
		||||
tags:
 | 
			
		||||
 - Extensibility
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 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](https://martinfowler.com/books/eaa.html) 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.
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
public class Db {
 | 
			
		||||
  private static Db instance;
 | 
			
		||||
  private Map<String, User> userName2User;
 | 
			
		||||
  private Map<User, Account> user2Account;
 | 
			
		||||
  private Map<String, Product> itemName2Product;
 | 
			
		||||
 | 
			
		||||
  public static Db getInstance() {
 | 
			
		||||
    if (instance == null) {
 | 
			
		||||
      synchronized (Db.class) {
 | 
			
		||||
        if (instance == null) {
 | 
			
		||||
          instance = new Db();
 | 
			
		||||
          instance.userName2User = new HashMap<>();
 | 
			
		||||
          instance.user2Account = new HashMap<>();
 | 
			
		||||
          instance.itemName2Product = new HashMap<>();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void seedUser(String userName, Double amount) {
 | 
			
		||||
    User user = new User(userName);
 | 
			
		||||
    instance.userName2User.put(userName, user);
 | 
			
		||||
    Account account = new Account(amount);
 | 
			
		||||
    instance.user2Account.put(user, account);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void seedItem(String itemName, Double price) {
 | 
			
		||||
    Product item = new Product(price);
 | 
			
		||||
    itemName2Product.put(itemName, item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public User findUserByUserName(String userName) {
 | 
			
		||||
    if (!userName2User.containsKey(userName)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return userName2User.get(userName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public Account findAccountByUser(User user) {
 | 
			
		||||
    if (!user2Account.containsKey(user)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return user2Account.get(user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public Product findProductByItemName(String itemName) {
 | 
			
		||||
    if (!itemName2Product.containsKey(itemName)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return itemName2Product.get(itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class User {
 | 
			
		||||
    private String userName;
 | 
			
		||||
 | 
			
		||||
    public User(String userName) {
 | 
			
		||||
      this.userName = userName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getUserName() {
 | 
			
		||||
      return userName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ReceiptDto purchase(Product item) {
 | 
			
		||||
      return new ReceiptDto(item.getPrice());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class Account {
 | 
			
		||||
    private Double amount;
 | 
			
		||||
 | 
			
		||||
    public Account(Double amount) {
 | 
			
		||||
      this.amount = amount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public MoneyTransaction withdraw(Double price) {
 | 
			
		||||
      if (price > amount) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      return new MoneyTransaction(amount, price);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Double getAmount() {
 | 
			
		||||
      return amount;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class Product {
 | 
			
		||||
    private Double price;
 | 
			
		||||
 | 
			
		||||
    public Product(Double price) {
 | 
			
		||||
      this.price = price;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Double getPrice() {
 | 
			
		||||
      return price;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class MaintenanceLock {
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(MaintenanceLock.class);
 | 
			
		||||
 | 
			
		||||
  private static MaintenanceLock instance;
 | 
			
		||||
  private boolean lock = true;
 | 
			
		||||
 | 
			
		||||
  public static MaintenanceLock getInstance() {
 | 
			
		||||
    if (instance == null) {
 | 
			
		||||
      synchronized (MaintenanceLock.class) {
 | 
			
		||||
        if (instance == null) {
 | 
			
		||||
          instance = new MaintenanceLock();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public boolean isLock() {
 | 
			
		||||
    return lock;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void setLock(boolean lock) {
 | 
			
		||||
    this.lock = lock;
 | 
			
		||||
    LOGGER.info("Maintenance lock is set to: " + lock);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Let's first introduce presentation layer, the receipt view model interface and its implementation of successful scenario.
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
public interface ReceiptViewModel {
 | 
			
		||||
  void show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class ReceiptDto implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(ReceiptDto.class);
 | 
			
		||||
 | 
			
		||||
  private Double price;
 | 
			
		||||
 | 
			
		||||
  public ReceiptDto(Double price) {
 | 
			
		||||
    this.price = price;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public Double getPrice() {
 | 
			
		||||
    return price;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Receipt: " + price + " paid");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
And here are the implementations of failure scenarios, which are the special cases.
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
public class DownForMaintenance implements ReceiptViewModel {
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(DownForMaintenance.class);
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Down for maintenance");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class InvalidUser implements ReceiptViewModel {
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(InvalidUser.class);
 | 
			
		||||
 | 
			
		||||
  private final String userName;
 | 
			
		||||
 | 
			
		||||
  public InvalidUser(String userName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Invalid user: " + userName);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class OutOfStock implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(OutOfStock.class);
 | 
			
		||||
 | 
			
		||||
  private String userName;
 | 
			
		||||
  private String itemName;
 | 
			
		||||
 | 
			
		||||
  public OutOfStock(String userName, String itemName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
    this.itemName = itemName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Out of stock: " + itemName + " for user = " + userName + " to buy");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class InsufficientFunds implements ReceiptViewModel {
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(InsufficientFunds.class);
 | 
			
		||||
 | 
			
		||||
  private String userName;
 | 
			
		||||
  private Double amount;
 | 
			
		||||
  private String itemName;
 | 
			
		||||
 | 
			
		||||
  public InsufficientFunds(String userName, Double amount, String itemName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
    this.amount = amount;
 | 
			
		||||
    this.itemName = itemName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Insufficient funds: " + amount + " of user: " + userName
 | 
			
		||||
        + " for buying item: " + itemName);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Second, here's the application layer, the application services implementation and the domain services implementation.
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
public class ApplicationServicesImpl implements ApplicationServices {
 | 
			
		||||
  private DomainServicesImpl domain = new DomainServicesImpl();
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public ReceiptViewModel loggedInUserPurchase(String userName, String itemName) {
 | 
			
		||||
    if (isDownForMaintenance()) {
 | 
			
		||||
      return new DownForMaintenance();
 | 
			
		||||
    }
 | 
			
		||||
    return this.domain.purchase(userName, itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private boolean isDownForMaintenance() {
 | 
			
		||||
    return MaintenanceLock.getInstance().isLock();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class DomainServicesImpl implements DomainServices {
 | 
			
		||||
  public ReceiptViewModel purchase(String userName, String itemName) {
 | 
			
		||||
    Db.User user = Db.getInstance().findUserByUserName(userName);
 | 
			
		||||
    if (user == null) {
 | 
			
		||||
      return new InvalidUser(userName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Db.Account account = Db.getInstance().findAccountByUser(user);
 | 
			
		||||
    return purchase(user, account, itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private ReceiptViewModel purchase(Db.User user, Db.Account account, String itemName) {
 | 
			
		||||
    Db.Product item = Db.getInstance().findProductByItemName(itemName);
 | 
			
		||||
    if (item == null) {
 | 
			
		||||
      return new OutOfStock(user.getUserName(), itemName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ReceiptDto receipt = user.purchase(item);
 | 
			
		||||
    MoneyTransaction transaction = account.withdraw(receipt.getPrice());
 | 
			
		||||
    if (transaction == null) {
 | 
			
		||||
      return new InsufficientFunds(user.getUserName(), account.getAmount(), itemName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return receipt;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Finally, the client send requests the application services to get the presentation view.
 | 
			
		||||
 | 
			
		||||
```java
 | 
			
		||||
    // DB seeding
 | 
			
		||||
    LOGGER.info("Db seeding: " + "1 user: {\"ignite1771\", amount = 1000.0}, "
 | 
			
		||||
        + "2 products: {\"computer\": price = 800.0, \"car\": price = 20000.0}");
 | 
			
		||||
    Db.getInstance().seedUser("ignite1771", 1000.0);
 | 
			
		||||
    Db.getInstance().seedItem("computer", 800.0);
 | 
			
		||||
    Db.getInstance().seedItem("car", 20000.0);
 | 
			
		||||
 | 
			
		||||
    var applicationServices = new ApplicationServicesImpl();
 | 
			
		||||
    ReceiptViewModel receipt;
 | 
			
		||||
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    MaintenanceLock.getInstance().setLock(false);
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "car");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "car");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "computer");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer");
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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 
 | 
			
		||||
 | 
			
		||||
* [Special Case Tutorial](https://www.codinghelmet.com/articles/reduce-cyclomatic-complexity-special-case)
 | 
			
		||||
 | 
			
		||||
## Credits
 | 
			
		||||
 | 
			
		||||
* [How to Reduce Cyclomatic Complexity Part 2: Special Case Pattern](https://www.codinghelmet.com/articles/reduce-cyclomatic-complexity-special-case)
 | 
			
		||||
* [Patterns of Enterprise Application Architecture](https://martinfowler.com/books/eaa.html)
 | 
			
		||||
* [Special Case](https://www.martinfowler.com/eaaCatalog/specialCase.html)
 | 
			
		||||
							
								
								
									
										119
									
								
								special-case/etc/special-case.urm.puml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								special-case/etc/special-case.urm.puml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
@startuml
 | 
			
		||||
left to right direction
 | 
			
		||||
package com.iluwatar.specialcase {
 | 
			
		||||
  class App {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    + App()
 | 
			
		||||
    + main(args : String[]) {static}
 | 
			
		||||
  }
 | 
			
		||||
  interface ApplicationServices {
 | 
			
		||||
    + loggedInUserPurchase(String, String) : ReceiptViewModel {abstract}
 | 
			
		||||
  }
 | 
			
		||||
  class ApplicationServicesImpl {
 | 
			
		||||
    - domain : DomainServicesImpl
 | 
			
		||||
    + ApplicationServicesImpl()
 | 
			
		||||
    - isDownForMaintenance() : boolean
 | 
			
		||||
    + loggedInUserPurchase(userName : String, itemName : String) : ReceiptViewModel
 | 
			
		||||
  }
 | 
			
		||||
  class Db {
 | 
			
		||||
    - instance : Db {static}
 | 
			
		||||
    - itemName2Product : Map<String, Product>
 | 
			
		||||
    - user2Account : Map<User, Account>
 | 
			
		||||
    - userName2User : Map<String, User>
 | 
			
		||||
    + Db()
 | 
			
		||||
    + findAccountByUser(user : User) : Account
 | 
			
		||||
    + findProductByItemName(itemName : String) : Product
 | 
			
		||||
    + findUserByUserName(userName : String) : User
 | 
			
		||||
    + getInstance() : Db {static}
 | 
			
		||||
    + seedItem(itemName : String, price : Double)
 | 
			
		||||
    + seedUser(userName : String, amount : Double)
 | 
			
		||||
  }
 | 
			
		||||
  class Account {
 | 
			
		||||
    - amount : Double
 | 
			
		||||
    + Account(this$0 : Double)
 | 
			
		||||
    + getAmount() : Double
 | 
			
		||||
    + withdraw(price : Double) : MoneyTransaction
 | 
			
		||||
  }
 | 
			
		||||
  class Product {
 | 
			
		||||
    - price : Double
 | 
			
		||||
    + Product(this$0 : Double)
 | 
			
		||||
    + getPrice() : Double
 | 
			
		||||
  }
 | 
			
		||||
  class User {
 | 
			
		||||
    - userName : String
 | 
			
		||||
    + User(this$0 : String)
 | 
			
		||||
    + getUserName() : String
 | 
			
		||||
    + purchase(item : Product) : ReceiptDto
 | 
			
		||||
  }
 | 
			
		||||
  interface DomainServices {
 | 
			
		||||
  }
 | 
			
		||||
  class DomainServicesImpl {
 | 
			
		||||
    + DomainServicesImpl()
 | 
			
		||||
    - purchase(user : User, account : Account, itemName : String) : ReceiptViewModel
 | 
			
		||||
    + purchase(userName : String, itemName : String) : ReceiptViewModel
 | 
			
		||||
  }
 | 
			
		||||
  class DownForMaintenance {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    + DownForMaintenance()
 | 
			
		||||
    + show()
 | 
			
		||||
  }
 | 
			
		||||
  class InsufficientFunds {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    - amount : Double
 | 
			
		||||
    - itemName : String
 | 
			
		||||
    - userName : String
 | 
			
		||||
    + InsufficientFunds(userName : String, amount : Double, itemName : String)
 | 
			
		||||
    + show()
 | 
			
		||||
  }
 | 
			
		||||
  class InvalidUser {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    - userName : String
 | 
			
		||||
    + InvalidUser(userName : String)
 | 
			
		||||
    + show()
 | 
			
		||||
  }
 | 
			
		||||
  class MaintenanceLock {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    - instance : MaintenanceLock {static}
 | 
			
		||||
    - lock : boolean
 | 
			
		||||
    + MaintenanceLock()
 | 
			
		||||
    + getInstance() : MaintenanceLock {static}
 | 
			
		||||
    + isLock() : boolean
 | 
			
		||||
    + setLock(lock : boolean)
 | 
			
		||||
  }
 | 
			
		||||
  class MoneyTransaction {
 | 
			
		||||
    - amount : Double
 | 
			
		||||
    - price : Double
 | 
			
		||||
    + MoneyTransaction(amount : Double, price : Double)
 | 
			
		||||
  }
 | 
			
		||||
  class OutOfStock {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    - itemName : String
 | 
			
		||||
    - userName : String
 | 
			
		||||
    + OutOfStock(userName : String, itemName : String)
 | 
			
		||||
    + show()
 | 
			
		||||
  }
 | 
			
		||||
  class ReceiptDto {
 | 
			
		||||
    - LOGGER : Logger {static}
 | 
			
		||||
    - price : Double
 | 
			
		||||
    + ReceiptDto(price : Double)
 | 
			
		||||
    + getPrice() : Double
 | 
			
		||||
    + show()
 | 
			
		||||
  }
 | 
			
		||||
  interface ReceiptViewModel {
 | 
			
		||||
    + show() {abstract}
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
User --+ Db
 | 
			
		||||
Product --+ Db
 | 
			
		||||
MaintenanceLock -->  "-instance" MaintenanceLock
 | 
			
		||||
Db -->  "-instance" Db
 | 
			
		||||
ApplicationServicesImpl -->  "-domain" DomainServicesImpl
 | 
			
		||||
Account --+ Db
 | 
			
		||||
ApplicationServicesImpl ..|> ApplicationServices 
 | 
			
		||||
DomainServicesImpl ..|> DomainServices 
 | 
			
		||||
DownForMaintenance ..|> ReceiptViewModel 
 | 
			
		||||
InsufficientFunds ..|> ReceiptViewModel 
 | 
			
		||||
InvalidUser ..|> ReceiptViewModel 
 | 
			
		||||
OutOfStock ..|> ReceiptViewModel 
 | 
			
		||||
ReceiptDto ..|> ReceiptViewModel 
 | 
			
		||||
@enduml
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								special-case/etc/special_case_urm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								special-case/etc/special_case_urm.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 448 KiB  | 
							
								
								
									
										22
									
								
								special-case/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								special-case/pom.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>java-design-patterns</artifactId>
 | 
			
		||||
        <groupId>com.iluwatar</groupId>
 | 
			
		||||
        <version>1.24.0-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <artifactId>special-case</artifactId>
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.junit.jupiter</groupId>
 | 
			
		||||
            <artifactId>junit-jupiter-engine</artifactId>
 | 
			
		||||
            <scope>test</scope>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										47
									
								
								special-case/src/main/java/com/iluwatar/specialcase/App.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								special-case/src/main/java/com/iluwatar/specialcase/App.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * <p>The Special Case Pattern is a software design pattern that encapsulates particular cases
 | 
			
		||||
 * into subclasses that provide special behaviors.</p>
 | 
			
		||||
 *
 | 
			
		||||
 * <p>In this example ({@link ReceiptViewModel}) encapsulates all particular cases.</p>
 | 
			
		||||
 */
 | 
			
		||||
public class App {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Program entry point.
 | 
			
		||||
   */
 | 
			
		||||
  public static void main(String[] args) {
 | 
			
		||||
    // DB seeding
 | 
			
		||||
    LOGGER.info("Db seeding: " + "1 user: {\"ignite1771\", amount = 1000.0}, "
 | 
			
		||||
        + "2 products: {\"computer\": price = 800.0, \"car\": price = 20000.0}");
 | 
			
		||||
    Db.getInstance().seedUser("ignite1771", 1000.0);
 | 
			
		||||
    Db.getInstance().seedItem("computer", 800.0);
 | 
			
		||||
    Db.getInstance().seedItem("car", 20000.0);
 | 
			
		||||
 | 
			
		||||
    final var applicationServices = new ApplicationServicesImpl();
 | 
			
		||||
    ReceiptViewModel receipt;
 | 
			
		||||
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    MaintenanceLock.getInstance().setLock(false);
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "abc123" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("abc123", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "tv");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "car");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "car");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
    LOGGER.info("[REQUEST] User: " + "ignite1771" + " buy product: " + "computer");
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public interface ApplicationServices {
 | 
			
		||||
 | 
			
		||||
  ReceiptViewModel loggedInUserPurchase(String userName, String itemName);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public class ApplicationServicesImpl implements ApplicationServices {
 | 
			
		||||
 | 
			
		||||
  private DomainServicesImpl domain = new DomainServicesImpl();
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public ReceiptViewModel loggedInUserPurchase(String userName, String itemName) {
 | 
			
		||||
    if (isDownForMaintenance()) {
 | 
			
		||||
      return new DownForMaintenance();
 | 
			
		||||
    }
 | 
			
		||||
    return this.domain.purchase(userName, itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private boolean isDownForMaintenance() {
 | 
			
		||||
    return MaintenanceLock.getInstance().isLock();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										150
									
								
								special-case/src/main/java/com/iluwatar/specialcase/Db.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								special-case/src/main/java/com/iluwatar/specialcase/Db.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
public class Db {
 | 
			
		||||
 | 
			
		||||
  private static Db instance;
 | 
			
		||||
  private Map<String, User> userName2User;
 | 
			
		||||
  private Map<User, Account> user2Account;
 | 
			
		||||
  private Map<String, Product> itemName2Product;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the instance of Db.
 | 
			
		||||
   *
 | 
			
		||||
   * @return singleton instance of Db class
 | 
			
		||||
   */
 | 
			
		||||
  public static Db getInstance() {
 | 
			
		||||
    if (instance == null) {
 | 
			
		||||
      synchronized (Db.class) {
 | 
			
		||||
        if (instance == null) {
 | 
			
		||||
          instance = new Db();
 | 
			
		||||
          instance.userName2User = new HashMap<>();
 | 
			
		||||
          instance.user2Account = new HashMap<>();
 | 
			
		||||
          instance.itemName2Product = new HashMap<>();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Seed a user into Db.
 | 
			
		||||
   *
 | 
			
		||||
   * @param userName of the user
 | 
			
		||||
   * @param amount of the user's account
 | 
			
		||||
   */
 | 
			
		||||
  public void seedUser(String userName, Double amount) {
 | 
			
		||||
    User user = new User(userName);
 | 
			
		||||
    instance.userName2User.put(userName, user);
 | 
			
		||||
    Account account = new Account(amount);
 | 
			
		||||
    instance.user2Account.put(user, account);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Seed an item into Db.
 | 
			
		||||
   *
 | 
			
		||||
   * @param itemName of the item
 | 
			
		||||
   * @param price of the item
 | 
			
		||||
   */
 | 
			
		||||
  public void seedItem(String itemName, Double price) {
 | 
			
		||||
    Product item = new Product(price);
 | 
			
		||||
    itemName2Product.put(itemName, item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find a user with the userName.
 | 
			
		||||
   *
 | 
			
		||||
   * @param userName of the user
 | 
			
		||||
   * @return instance of User
 | 
			
		||||
   */
 | 
			
		||||
  public User findUserByUserName(String userName) {
 | 
			
		||||
    if (!userName2User.containsKey(userName)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return userName2User.get(userName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find an account of the user.
 | 
			
		||||
   *
 | 
			
		||||
   * @param user in Db
 | 
			
		||||
   * @return instance of Account of the user
 | 
			
		||||
   */
 | 
			
		||||
  public Account findAccountByUser(User user) {
 | 
			
		||||
    if (!user2Account.containsKey(user)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return user2Account.get(user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find a product with the itemName.
 | 
			
		||||
   *
 | 
			
		||||
   * @param itemName of the item
 | 
			
		||||
   * @return instance of Product
 | 
			
		||||
   */
 | 
			
		||||
  public Product findProductByItemName(String itemName) {
 | 
			
		||||
    if (!itemName2Product.containsKey(itemName)) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    return itemName2Product.get(itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class User {
 | 
			
		||||
 | 
			
		||||
    private String userName;
 | 
			
		||||
 | 
			
		||||
    public User(String userName) {
 | 
			
		||||
      this.userName = userName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getUserName() {
 | 
			
		||||
      return userName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ReceiptDto purchase(Product item) {
 | 
			
		||||
      return new ReceiptDto(item.getPrice());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class Account {
 | 
			
		||||
 | 
			
		||||
    private Double amount;
 | 
			
		||||
 | 
			
		||||
    public Account(Double amount) {
 | 
			
		||||
      this.amount = amount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Withdraw the price of the item from the account.
 | 
			
		||||
     *
 | 
			
		||||
     * @param price of the item
 | 
			
		||||
     * @return instance of MoneyTransaction
 | 
			
		||||
     */
 | 
			
		||||
    public MoneyTransaction withdraw(Double price) {
 | 
			
		||||
      if (price > amount) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
      return new MoneyTransaction(amount, price);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Double getAmount() {
 | 
			
		||||
      return amount;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public class Product {
 | 
			
		||||
 | 
			
		||||
    private Double price;
 | 
			
		||||
 | 
			
		||||
    public Product(Double price) {
 | 
			
		||||
      this.price = price;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Double getPrice() {
 | 
			
		||||
      return price;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,4 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public interface DomainServices {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public class DomainServicesImpl implements DomainServices {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Domain purchase with userName and itemName, with validation for userName.
 | 
			
		||||
   *
 | 
			
		||||
   * @param userName of the user
 | 
			
		||||
   * @param itemName of the item
 | 
			
		||||
   * @return instance of ReceiptViewModel
 | 
			
		||||
   */
 | 
			
		||||
  public ReceiptViewModel purchase(String userName, String itemName) {
 | 
			
		||||
    Db.User user = Db.getInstance().findUserByUserName(userName);
 | 
			
		||||
    if (user == null) {
 | 
			
		||||
      return new InvalidUser(userName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Db.Account account = Db.getInstance().findAccountByUser(user);
 | 
			
		||||
    return purchase(user, account, itemName);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Domain purchase with user, account and itemName,
 | 
			
		||||
   * with validation for whether product is out of stock
 | 
			
		||||
   * and whether user has insufficient funds in the account.
 | 
			
		||||
   *
 | 
			
		||||
   * @param user in Db
 | 
			
		||||
   * @param account in Db
 | 
			
		||||
   * @param itemName of the item
 | 
			
		||||
   * @return instance of ReceiptViewModel
 | 
			
		||||
   */
 | 
			
		||||
  private ReceiptViewModel purchase(Db.User user, Db.Account account, String itemName) {
 | 
			
		||||
    Db.Product item = Db.getInstance().findProductByItemName(itemName);
 | 
			
		||||
    if (item == null) {
 | 
			
		||||
      return new OutOfStock(user.getUserName(), itemName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ReceiptDto receipt = user.purchase(item);
 | 
			
		||||
    MoneyTransaction transaction = account.withdraw(receipt.getPrice());
 | 
			
		||||
    if (transaction == null) {
 | 
			
		||||
      return new InsufficientFunds(user.getUserName(), account.getAmount(), itemName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return receipt;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class DownForMaintenance implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(DownForMaintenance.class);
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Down for maintenance");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class InsufficientFunds implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(InsufficientFunds.class);
 | 
			
		||||
 | 
			
		||||
  private String userName;
 | 
			
		||||
  private Double amount;
 | 
			
		||||
  private String itemName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructor of InsufficientFunds.
 | 
			
		||||
   *
 | 
			
		||||
   * @param userName of the user
 | 
			
		||||
   * @param amount of the user's account
 | 
			
		||||
   * @param itemName of the item
 | 
			
		||||
   */
 | 
			
		||||
  public InsufficientFunds(String userName, Double amount, String itemName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
    this.amount = amount;
 | 
			
		||||
    this.itemName = itemName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Insufficient funds: " + amount + " of user: " + userName
 | 
			
		||||
        + " for buying item: " + itemName);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class InvalidUser implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(InvalidUser.class);
 | 
			
		||||
 | 
			
		||||
  private final String userName;
 | 
			
		||||
 | 
			
		||||
  public InvalidUser(String userName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Invalid user: " + userName);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class MaintenanceLock {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(MaintenanceLock.class);
 | 
			
		||||
 | 
			
		||||
  private static MaintenanceLock instance;
 | 
			
		||||
  private boolean lock = true;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the instance of MaintenanceLock.
 | 
			
		||||
   *
 | 
			
		||||
   * @return singleton instance of MaintenanceLock
 | 
			
		||||
   */
 | 
			
		||||
  public static MaintenanceLock getInstance() {
 | 
			
		||||
    if (instance == null) {
 | 
			
		||||
      synchronized (MaintenanceLock.class) {
 | 
			
		||||
        if (instance == null) {
 | 
			
		||||
          instance = new MaintenanceLock();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public boolean isLock() {
 | 
			
		||||
    return lock;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public void setLock(boolean lock) {
 | 
			
		||||
    this.lock = lock;
 | 
			
		||||
    LOGGER.info("Maintenance lock is set to: " + lock);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public class MoneyTransaction {
 | 
			
		||||
 | 
			
		||||
  private Double amount;
 | 
			
		||||
  private Double price;
 | 
			
		||||
 | 
			
		||||
  public MoneyTransaction(Double amount, Double price) {
 | 
			
		||||
    this.amount = amount;
 | 
			
		||||
    this.price = price;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class OutOfStock implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(OutOfStock.class);
 | 
			
		||||
 | 
			
		||||
  private String userName;
 | 
			
		||||
  private String itemName;
 | 
			
		||||
 | 
			
		||||
  public OutOfStock(String userName, String itemName) {
 | 
			
		||||
    this.userName = userName;
 | 
			
		||||
    this.itemName = itemName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Out of stock: " + itemName + " for user = " + userName + " to buy");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
 | 
			
		||||
public class ReceiptDto implements ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  private static final Logger LOGGER = LoggerFactory.getLogger(ReceiptDto.class);
 | 
			
		||||
 | 
			
		||||
  private Double price;
 | 
			
		||||
 | 
			
		||||
  public ReceiptDto(Double price) {
 | 
			
		||||
    this.price = price;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public Double getPrice() {
 | 
			
		||||
    return price;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public void show() {
 | 
			
		||||
    LOGGER.info("Receipt: " + price + " paid");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
public interface ReceiptViewModel {
 | 
			
		||||
 | 
			
		||||
  void show();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Application test.
 | 
			
		||||
 */
 | 
			
		||||
public class AppTest {
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  void shouldExecuteWithoutException() {
 | 
			
		||||
    assertDoesNotThrow(() -> App.main(new String[]{}));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,120 @@
 | 
			
		||||
package com.iluwatar.specialcase;
 | 
			
		||||
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.BeforeAll;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
import ch.qos.logback.classic.Level;
 | 
			
		||||
import ch.qos.logback.classic.Logger;
 | 
			
		||||
import ch.qos.logback.classic.spi.ILoggingEvent;
 | 
			
		||||
import ch.qos.logback.core.read.ListAppender;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.api.BeforeEach;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Special cases unit tests. (including the successful scenario {@link ReceiptDto})
 | 
			
		||||
 */
 | 
			
		||||
public class SpecialCasesTest {
 | 
			
		||||
  private static ApplicationServices applicationServices;
 | 
			
		||||
  private static ReceiptViewModel receipt;
 | 
			
		||||
 | 
			
		||||
  @BeforeAll
 | 
			
		||||
  static void beforeAll() {
 | 
			
		||||
    Db.getInstance().seedUser("ignite1771", 1000.0);
 | 
			
		||||
    Db.getInstance().seedItem("computer", 800.0);
 | 
			
		||||
    Db.getInstance().seedItem("car", 20000.0);
 | 
			
		||||
 | 
			
		||||
    applicationServices = new ApplicationServicesImpl();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @BeforeEach
 | 
			
		||||
  public void beforeEach() {
 | 
			
		||||
    MaintenanceLock.getInstance().setLock(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testDownForMaintenance() {
 | 
			
		||||
    final Logger LOGGER = (Logger) LoggerFactory.getLogger(DownForMaintenance.class);
 | 
			
		||||
 | 
			
		||||
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
 | 
			
		||||
    listAppender.start();
 | 
			
		||||
    LOGGER.addAppender(listAppender);
 | 
			
		||||
 | 
			
		||||
    MaintenanceLock.getInstance().setLock(true);
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase(null, null);
 | 
			
		||||
    receipt.show();
 | 
			
		||||
 | 
			
		||||
    List<ILoggingEvent> loggingEventList = listAppender.list;
 | 
			
		||||
    assertEquals("Down for maintenance", loggingEventList.get(0).getMessage());
 | 
			
		||||
    assertEquals(Level.INFO, loggingEventList.get(0).getLevel());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testInvalidUser() {
 | 
			
		||||
    final Logger LOGGER = (Logger) LoggerFactory.getLogger(InvalidUser.class);
 | 
			
		||||
 | 
			
		||||
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
 | 
			
		||||
    listAppender.start();
 | 
			
		||||
    LOGGER.addAppender(listAppender);
 | 
			
		||||
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("a", null);
 | 
			
		||||
    receipt.show();
 | 
			
		||||
 | 
			
		||||
    List<ILoggingEvent> loggingEventList = listAppender.list;
 | 
			
		||||
    assertEquals("Invalid user: a", loggingEventList.get(0).getMessage());
 | 
			
		||||
    assertEquals(Level.INFO, loggingEventList.get(0).getLevel());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testOutOfStock() {
 | 
			
		||||
    final Logger LOGGER = (Logger) LoggerFactory.getLogger(OutOfStock.class);
 | 
			
		||||
 | 
			
		||||
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
 | 
			
		||||
    listAppender.start();
 | 
			
		||||
    LOGGER.addAppender(listAppender);
 | 
			
		||||
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
 | 
			
		||||
    List<ILoggingEvent> loggingEventList = listAppender.list;
 | 
			
		||||
    assertEquals("Out of stock: tv for user = ignite1771 to buy"
 | 
			
		||||
        , loggingEventList.get(0).getMessage());
 | 
			
		||||
    assertEquals(Level.INFO, loggingEventList.get(0).getLevel());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testInsufficientFunds() {
 | 
			
		||||
    final Logger LOGGER = (Logger) LoggerFactory.getLogger(InsufficientFunds.class);
 | 
			
		||||
 | 
			
		||||
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
 | 
			
		||||
    listAppender.start();
 | 
			
		||||
    LOGGER.addAppender(listAppender);
 | 
			
		||||
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "car");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
 | 
			
		||||
    List<ILoggingEvent> loggingEventList = listAppender.list;
 | 
			
		||||
    assertEquals("Insufficient funds: 1000.0 of user: ignite1771 for buying item: car"
 | 
			
		||||
        , loggingEventList.get(0).getMessage());
 | 
			
		||||
    assertEquals(Level.INFO, loggingEventList.get(0).getLevel());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testReceiptDto() {
 | 
			
		||||
    final Logger LOGGER = (Logger) LoggerFactory.getLogger(ReceiptDto.class);
 | 
			
		||||
 | 
			
		||||
    ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
 | 
			
		||||
    listAppender.start();
 | 
			
		||||
    LOGGER.addAppender(listAppender);
 | 
			
		||||
 | 
			
		||||
    receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer");
 | 
			
		||||
    receipt.show();
 | 
			
		||||
 | 
			
		||||
    List<ILoggingEvent> loggingEventList = listAppender.list;
 | 
			
		||||
    assertEquals("Receipt: 800.0 paid"
 | 
			
		||||
        , loggingEventList.get(0).getMessage());
 | 
			
		||||
    assertEquals(Level.INFO, loggingEventList.get(0).getLevel());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user