diff --git a/pom.xml b/pom.xml index b6556245b..38e702761 100644 --- a/pom.xml +++ b/pom.xml @@ -200,6 +200,7 @@ factory separated-interface data-transfer-object-enum-impl + special-case diff --git a/special-case/README.md b/special-case/README.md new file mode 100644 index 000000000..e40ca3b7f --- /dev/null +++ b/special-case/README.md @@ -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 userName2User; + private Map user2Account; + private Map 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 + +![alt text](./etc/special_case_urm.png "Special Case") + +## 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) \ No newline at end of file diff --git a/special-case/etc/special-case.urm.puml b/special-case/etc/special-case.urm.puml new file mode 100644 index 000000000..07bb47eb1 --- /dev/null +++ b/special-case/etc/special-case.urm.puml @@ -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 + - user2Account : Map + - userName2User : Map + + 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 diff --git a/special-case/etc/special_case_urm.png b/special-case/etc/special_case_urm.png new file mode 100644 index 000000000..03ca646f3 Binary files /dev/null and b/special-case/etc/special_case_urm.png differ diff --git a/special-case/pom.xml b/special-case/pom.xml new file mode 100644 index 000000000..c62b9e759 --- /dev/null +++ b/special-case/pom.xml @@ -0,0 +1,22 @@ + + + + java-design-patterns + com.iluwatar + 1.24.0-SNAPSHOT + + 4.0.0 + + special-case + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + diff --git a/special-case/src/main/java/com/iluwatar/specialcase/App.java b/special-case/src/main/java/com/iluwatar/specialcase/App.java new file mode 100644 index 000000000..276b1dd85 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/App.java @@ -0,0 +1,47 @@ +package com.iluwatar.specialcase; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

The Special Case Pattern is a software design pattern that encapsulates particular cases + * into subclasses that provide special behaviors.

+ * + *

In this example ({@link ReceiptViewModel}) encapsulates all particular cases.

+ */ +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(); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServices.java b/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServices.java new file mode 100644 index 000000000..f756ccc43 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServices.java @@ -0,0 +1,6 @@ +package com.iluwatar.specialcase; + +public interface ApplicationServices { + + ReceiptViewModel loggedInUserPurchase(String userName, String itemName); +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServicesImpl.java b/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServicesImpl.java new file mode 100644 index 000000000..ebb9109bc --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/ApplicationServicesImpl.java @@ -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(); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/Db.java b/special-case/src/main/java/com/iluwatar/specialcase/Db.java new file mode 100644 index 000000000..847330ece --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/Db.java @@ -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 userName2User; + private Map user2Account; + private Map 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; + } + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/DomainServices.java b/special-case/src/main/java/com/iluwatar/specialcase/DomainServices.java new file mode 100644 index 000000000..a052eb20b --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/DomainServices.java @@ -0,0 +1,4 @@ +package com.iluwatar.specialcase; + +public interface DomainServices { +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/DomainServicesImpl.java b/special-case/src/main/java/com/iluwatar/specialcase/DomainServicesImpl.java new file mode 100644 index 000000000..400689ed9 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/DomainServicesImpl.java @@ -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; + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/DownForMaintenance.java b/special-case/src/main/java/com/iluwatar/specialcase/DownForMaintenance.java new file mode 100644 index 000000000..98a2cf89c --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/DownForMaintenance.java @@ -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"); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/InsufficientFunds.java b/special-case/src/main/java/com/iluwatar/specialcase/InsufficientFunds.java new file mode 100644 index 000000000..8fe714f80 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/InsufficientFunds.java @@ -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); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/InvalidUser.java b/special-case/src/main/java/com/iluwatar/specialcase/InvalidUser.java new file mode 100644 index 000000000..443fdc7bf --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/InvalidUser.java @@ -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); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/MaintenanceLock.java b/special-case/src/main/java/com/iluwatar/specialcase/MaintenanceLock.java new file mode 100644 index 000000000..29a5b4f81 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/MaintenanceLock.java @@ -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); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/MoneyTransaction.java b/special-case/src/main/java/com/iluwatar/specialcase/MoneyTransaction.java new file mode 100644 index 000000000..e3904964f --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/MoneyTransaction.java @@ -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; + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/OutOfStock.java b/special-case/src/main/java/com/iluwatar/specialcase/OutOfStock.java new file mode 100644 index 000000000..5359bed31 --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/OutOfStock.java @@ -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"); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/ReceiptDto.java b/special-case/src/main/java/com/iluwatar/specialcase/ReceiptDto.java new file mode 100644 index 000000000..81fc46bbe --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/ReceiptDto.java @@ -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"); + } +} diff --git a/special-case/src/main/java/com/iluwatar/specialcase/ReceiptViewModel.java b/special-case/src/main/java/com/iluwatar/specialcase/ReceiptViewModel.java new file mode 100644 index 000000000..482eef21f --- /dev/null +++ b/special-case/src/main/java/com/iluwatar/specialcase/ReceiptViewModel.java @@ -0,0 +1,6 @@ +package com.iluwatar.specialcase; + +public interface ReceiptViewModel { + + void show(); +} diff --git a/special-case/src/test/java/com/iluwatar/specialcase/AppTest.java b/special-case/src/test/java/com/iluwatar/specialcase/AppTest.java new file mode 100644 index 000000000..25799368b --- /dev/null +++ b/special-case/src/test/java/com/iluwatar/specialcase/AppTest.java @@ -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[]{})); + } +} diff --git a/special-case/src/test/java/com/iluwatar/specialcase/SpecialCasesTest.java b/special-case/src/test/java/com/iluwatar/specialcase/SpecialCasesTest.java new file mode 100644 index 000000000..5e83aa8d6 --- /dev/null +++ b/special-case/src/test/java/com/iluwatar/specialcase/SpecialCasesTest.java @@ -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 listAppender = new ListAppender<>(); + listAppender.start(); + LOGGER.addAppender(listAppender); + + MaintenanceLock.getInstance().setLock(true); + receipt = applicationServices.loggedInUserPurchase(null, null); + receipt.show(); + + List 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 listAppender = new ListAppender<>(); + listAppender.start(); + LOGGER.addAppender(listAppender); + + receipt = applicationServices.loggedInUserPurchase("a", null); + receipt.show(); + + List 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 listAppender = new ListAppender<>(); + listAppender.start(); + LOGGER.addAppender(listAppender); + + receipt = applicationServices.loggedInUserPurchase("ignite1771", "tv"); + receipt.show(); + + List 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 listAppender = new ListAppender<>(); + listAppender.start(); + LOGGER.addAppender(listAppender); + + receipt = applicationServices.loggedInUserPurchase("ignite1771", "car"); + receipt.show(); + + List 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 listAppender = new ListAppender<>(); + listAppender.start(); + LOGGER.addAppender(listAppender); + + receipt = applicationServices.loggedInUserPurchase("ignite1771", "computer"); + receipt.show(); + + List loggingEventList = listAppender.list; + assertEquals("Receipt: 800.0 paid" + , loggingEventList.get(0).getMessage()); + assertEquals(Level.INFO, loggingEventList.get(0).getLevel()); + } +}