Compare commits

...

60 Commits

Author SHA1 Message Date
41dacc2e2e docs: update .all-contributorsrc [skip ci] 2020-08-11 14:29:45 +00:00
dc7bf6190d docs: update README.md [skip ci] 2020-08-11 14:29:44 +00:00
98d2dc0104 Merge pull request #1487 from RayYH/master
fix word typo
2020-08-11 17:29:06 +03:00
870d44b127 Fix layout 2020-08-11 17:26:05 +03:00
834f20911b Merge pull request #1488 from iluwatar/all-contributors/add-ashishtrivedi16
docs: add ashishtrivedi16 as a contributor
2020-08-11 17:23:22 +03:00
2b7949ce6a docs: update .all-contributorsrc [skip ci] 2020-08-11 14:21:59 +00:00
4ffab7da85 docs: update README.md [skip ci] 2020-08-11 14:21:58 +00:00
1683fbdf9c Clean up Transaction Script 2020-08-11 17:17:25 +03:00
d091e369ec Merge pull request #1480 from ashishtrivedi16/master
#1321 Transaction script model implementation
2020-08-11 16:59:48 +03:00
b91bbc773d fix word typo 2020-08-11 14:19:39 +08:00
3c07a42e9d #1321 Updated UML diagram 2020-08-11 00:23:00 +05:30
e99bcf772b #1321 Added comments about TS 2020-08-11 00:21:46 +05:30
2d16e5afbf Merge branch 'master' of https://github.com/iluwatar/java-design-patterns 2020-08-10 16:27:28 +03:00
2c6f1832b0 Merge pull request #1485 from amit1307/logging-api-gateway-1338
Add logging in API Gateway
2020-08-10 16:26:18 +03:00
04a2be0c99 Update readme 2020-08-10 16:25:06 +03:00
10815b6469 Fix tags 2020-08-10 16:05:52 +03:00
75b10ed657 #1321 2020-08-10 13:21:24 +05:30
108532d8dd #1321 2020-08-10 13:12:12 +05:30
5527cf4234 #1321 Renamed main class 2020-08-10 13:06:46 +05:30
4008ae41b5 #1321 2020-08-10 13:02:35 +05:30
8166a1835d Merge remote-tracking branch 'origin/master' 2020-08-10 13:01:30 +05:30
4c9cad5475 #1321 Updated README 2020-08-10 13:01:12 +05:30
24126edd86 Update Hotel.java 2020-08-10 12:54:46 +05:30
c0edac0046 Rename TransactionScriptApp.java to App.java 2020-08-10 12:53:13 +05:30
f5c337981b Rename Readme.md to README.md 2020-08-10 12:52:37 +05:30
5441db6582 Update pom.xml 2020-08-10 12:52:15 +05:30
87f3a4d956 Delete module-info.java 2020-08-10 12:48:55 +05:30
31d753e59d Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-10 00:25:43 +05:30
6cef98d41e Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-10 00:25:01 +05:30
7450456dae Add syntax highlighting 2020-08-09 21:54:54 +03:00
45e416928d Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-10 00:24:44 +05:30
94c131f7e9 Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-10 00:24:33 +05:30
5eb9b98e78 Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-10 00:24:20 +05:30
8305e9365d Fix typos 2020-08-09 21:49:00 +03:00
04e3368a81 Merge pull request #1482 from vINCENT8888801/private-class-data-explanation
Private class data explanation
2020-08-09 21:27:34 +03:00
d219a104c7 Add logging in API Gateway 2020-08-09 18:13:24 +01:00
700f5c6d27 corrected some typo 2020-08-09 18:22:40 +08:00
f4fa73cd48 Update README.md
Increase clarity
2020-08-09 16:28:40 +08:00
e09de2fb36 #1321 2020-08-09 01:58:19 +05:30
a59c9bba97 #1321 2020-08-09 01:39:51 +05:30
7acc5fbf95 #1321
Resolved conflicts and used var wherever possible
2020-08-09 01:33:19 +05:30
bdf2145b3f Merge branch 'master' into master 2020-08-09 01:17:07 +05:30
1e90d0d645 Merge pull request #1484 from iluwatar/all-contributors/add-vINCENT8888801
docs: add vINCENT8888801 as a contributor
2020-08-08 22:44:51 +03:00
5bfaeffecf Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-09 00:51:44 +05:30
c0acaf073b Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-09 00:51:36 +05:30
9f190c59c4 Update transaction-script/Readme.md
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2020-08-09 00:51:28 +05:30
08cec0e254 Update README.md 2020-08-09 03:10:43 +08:00
1f6e6f34ea Update README.md 2020-08-09 03:09:53 +08:00
fa68789cd5 #1321 Updated readme and add UML diagram 2020-08-08 15:49:14 +05:30
12d392931e #1321 Added UML diagram 2020-07-27 00:39:53 +05:30
4017c37b6f #1321 Fixed a test 2020-07-26 18:34:42 +05:30
9d191324ff #1321 2020-07-26 18:17:00 +05:30
75ab3b5245 #1321 Added Tests 2020-07-26 17:38:33 +05:30
e0fb2d014a #1321 Added comments and refactored code 2020-07-26 15:56:55 +05:30
c4b1b89f1f #1321 2020-07-19 22:20:15 +05:30
52e6ea2b68 #1321
Implemented DAO
2020-07-19 20:33:52 +05:30
112973482d #1321
Added basic structure and planned out the roadmap
2020-07-19 16:51:16 +05:30
58e3fbfc6a #1321
Added gitignore
2020-07-19 16:19:01 +05:30
bf01f3e68b #1321
Added Readme file
2020-07-19 15:35:53 +05:30
58b98c54e5 #1321
Initial folder setup
2020-07-19 15:30:30 +05:30
26 changed files with 1647 additions and 35 deletions

View File

@ -1093,6 +1093,24 @@
"contributions": [
"code"
]
},
{
"login": "ashishtrivedi16",
"name": "Ashish Trivedi",
"avatar_url": "https://avatars3.githubusercontent.com/u/23194128?v=4",
"profile": "https://www.linkedin.com/in/ashish-trivedi-218379135/",
"contributions": [
"code"
]
},
{
"login": "RayYH",
"name": "洪月阳",
"avatar_url": "https://avatars1.githubusercontent.com/u/41055099?v=4",
"profile": "https://rayyounghong.com",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 4,

View File

@ -9,7 +9,7 @@
[![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=alert_status)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-120-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-122-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
# Introduction
@ -248,6 +248,10 @@ This project is licensed under the terms of the MIT license.
<td align="center"><a href="https://github.com/ravening"><img src="https://avatars1.githubusercontent.com/u/10645273?v=4" width="100px;" alt=""/><br /><sub><b>Rakesh</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=ravening" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/vINCENT8888801"><img src="https://avatars0.githubusercontent.com/u/8037883?v=4" width="100px;" alt=""/><br /><sub><b>Wei Seng</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=vINCENT8888801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://www.linkedin.com/in/ashish-trivedi-218379135/"><img src="https://avatars3.githubusercontent.com/u/23194128?v=4" width="100px;" alt=""/><br /><sub><b>Ashish Trivedi</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=ashishtrivedi16" title="Code">💻</a></td>
<td align="center"><a href="https://rayyounghong.com"><img src="https://avatars1.githubusercontent.com/u/41055099?v=4" width="100px;" alt=""/><br /><sub><b>洪月阳</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=RayYH" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-enable -->

View File

@ -6,6 +6,8 @@ permalink: /patterns/aggregator-microservices/
categories: Architectural
tags:
- Cloud distributed
- Decoupling
- Microservices
---
## Intent

View File

@ -12,49 +12,53 @@ tags:
## Intent
Aggregate calls to microservices in a single location: the API Gateway. The user makes a single call to the API Gateway,
and the API Gateway then calls each relevant microservice.
Aggregate calls to microservices in a single location, the API Gateway. The user makes a single call
to the API Gateway, and the API Gateway then calls each relevant microservice.
## Explanation
With the Microservices pattern, a client may need data from multiple different microservices. If the client called each
microservice directly, that could contribute to longer load times, since the client would have to make a network request
for each microservice called. Moreover, having the client call each microservice directly ties the client to that
microservice - if the internal implementations of the microservices change (for example, if two microservices are
combined sometime in the future) or if the location (host and port) of a microservice changes, then every client that
With the Microservices pattern, a client may need data from multiple different microservices. If the
client called each microservice directly, that could contribute to longer load times, since the
client would have to make a network request for each microservice called. Moreover, having the
client call each microservice directly ties the client to that microservice - if the internal
implementations of the microservices change (for example, if two microservices are combined sometime
in the future) or if the location (host and port) of a microservice changes, then every client that
makes use of those microservices must be updated.
The intent of the API Gateway pattern is to alleviate some of these issues. In the API Gateway pattern, an additional
entity (the API Gateway) is placed between the client and the microservices. The job of the API Gateway is to aggregate
the calls to the microservices. Rather than the client calling each microservice individually, the client calls the
API Gateway a single time. The API Gateway then calls each of the microservices that the client needs.
The intent of the API Gateway pattern is to alleviate some of these issues. In the API Gateway
pattern, an additional entity (the API Gateway) is placed between the client and the microservices.
The job of the API Gateway is to aggregate the calls to the microservices. Rather than the client
calling each microservice individually, the client calls the API Gateway a single time. The API
Gateway then calls each of the microservices that the client needs.
Real world example
> We are implementing microservices and API Gateway pattern for an e-commerce site. In this system the API Gateway makes
calls to the Image and Price microservices.
> We are implementing microservices and API Gateway pattern for an e-commerce site. In this system
> the API Gateway makes calls to the Image and Price microservices.
In plain words
> For a system implemented using microservices architecture, API Gateway is the single entry point that aggregates the
calls to the individual microservices.
> For a system implemented using microservices architecture, API Gateway is the single entry point
> that aggregates the calls to the individual microservices.
Wikipedia says
> API Gateway is a server that acts as an API front-end, receives API requests, enforces throttling and security
policies, passes requests to the back-end service and then passes the response back to the requester. A gateway often
includes a transformation engine to orchestrate and modify the requests and responses on the fly. A gateway can also
provide functionality such as collecting analytics data and providing caching. The gateway can provide functionality to
support authentication, authorization, security, audit and regulatory compliance.
> API Gateway is a server that acts as an API front-end, receives API requests, enforces throttling
> and security policies, passes requests to the back-end service and then passes the response back
> to the requester. A gateway often includes a transformation engine to orchestrate and modify the
> requests and responses on the fly. A gateway can also provide functionality such as collecting
> analytics data and providing caching. The gateway can provide functionality to support
> authentication, authorization, security, audit and regulatory compliance.
**Programmatic Example**
This implementation shows what the API Gateway pattern could look like for an e-commerce site. The `ApiGateway` makes
calls to the Image and Price microservices using the `ImageClientImpl` and `PriceClientImpl` respectively. Customers
viewing the site on a desktop device can see both price information and an image of a product, so the `ApiGateway` calls
both of the microservices and aggregates the data in the `DesktopProduct` model. However, mobile users only see price
information; they do not see a product image. For mobile users, the `ApiGateway` only retrieves price information, which
it uses to populate the `MobileProduct`.
This implementation shows what the API Gateway pattern could look like for an e-commerce site. The
`ApiGateway` makes calls to the Image and Price microservices using the `ImageClientImpl` and
`PriceClientImpl` respectively. Customers viewing the site on a desktop device can see both price
information and an image of a product, so the `ApiGateway` calls both of the microservices and
aggregates the data in the `DesktopProduct` model. However, mobile users only see price information;
they do not see a product image. For mobile users, the `ApiGateway` only retrieves price
information, which it uses to populate the `MobileProduct`.
Here's the Image microservice implementation.
@ -64,7 +68,6 @@ public interface ImageClient {
}
public class ImageClientImpl implements ImageClient {
@Override
public String getImagePath() {
var httpClient = HttpClient.newHttpClient();
@ -114,7 +117,7 @@ public class PriceClientImpl implements PriceClient {
}
```
And here we can see how API Gateway maps the requests to the microservices.
Here we can see how API Gateway maps the requests to the microservices.
```java
public class ApiGateway {

View File

@ -23,11 +23,16 @@
package com.iluwatar.api.gateway;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
/**
@ -35,6 +40,8 @@ import org.springframework.stereotype.Component;
*/
@Component
public class ImageClientImpl implements ImageClient {
private static final Logger LOGGER = getLogger(ImageClientImpl.class);
/**
* Makes a simple HTTP Get request to the Image microservice.
*
@ -49,12 +56,26 @@ public class ImageClientImpl implements ImageClient {
.build();
try {
LOGGER.info("Sending request to fetch image path");
var httpResponse = httpClient.send(httpGet, BodyHandlers.ofString());
logResponse(httpResponse);
return httpResponse.body();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
LOGGER.error("Failure occurred while getting image path", e);
}
return null;
}
private void logResponse(HttpResponse<String> httpResponse) {
if (isSuccessResponse(httpResponse.statusCode())) {
LOGGER.info("Image path received successfully");
} else {
LOGGER.warn("Image path request failed");
}
}
private boolean isSuccessResponse(int responseCode) {
return responseCode >= 200 && responseCode <= 299;
}
}

View File

@ -23,18 +23,26 @@
package com.iluwatar.api.gateway;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
/**
* An adapter to communicate with the Price microservice.
*/
@Component
public class PriceClientImpl implements PriceClient {
private static final Logger LOGGER = getLogger(PriceClientImpl.class);
/**
* Makes a simple HTTP Get request to the Price microservice.
*
@ -49,12 +57,26 @@ public class PriceClientImpl implements PriceClient {
.build();
try {
LOGGER.info("Sending request to fetch price info");
var httpResponse = httpClient.send(httpGet, BodyHandlers.ofString());
logResponse(httpResponse);
return httpResponse.body();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
LOGGER.error("Failure occurred while getting price info", e);
}
return null;
}
private void logResponse(HttpResponse<String> httpResponse) {
if (isSuccessResponse(httpResponse.statusCode())) {
LOGGER.info("Price info received successfully");
} else {
LOGGER.warn("Price info request failed");
}
}
private boolean isSuccessResponse(int responseCode) {
return responseCode >= 200 && responseCode <= 299;
}
}

View File

@ -23,15 +23,20 @@
package com.iluwatar.image.microservice;
import static org.slf4j.LoggerFactory.getLogger;
import org.slf4j.Logger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* Exposes the Image microservice's endpoints.
*/
@RestController
public class ImageController {
private static final Logger LOGGER = getLogger(ImageController.class);
/**
* An endpoint for a user to retrieve an image path.
@ -40,6 +45,7 @@ public class ImageController {
*/
@RequestMapping(value = "/image-path", method = RequestMethod.GET)
public String getImagePath() {
LOGGER.info("Successfully found image path");
return "/product-image.png";
}
}

View File

@ -23,15 +23,20 @@
package com.iluwatar.price.microservice;
import static org.slf4j.LoggerFactory.getLogger;
import org.slf4j.Logger;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* Exposes the Price microservice's endpoints.
*/
@RestController
public class PriceController {
private static final Logger LOGGER = getLogger(PriceController.class);
/**
* An endpoint for a user to retrieve a product's price.
@ -40,6 +45,7 @@ public class PriceController {
*/
@RequestMapping(value = "/price", method = RequestMethod.GET)
public String getPrice() {
LOGGER.info("Successfully found price info");
return "20";
}
}

View File

@ -192,6 +192,7 @@
<module>leader-followers</module>
<module>strangler</module>
<module>arrange-act-assert</module>
<module>transaction-script</module>
</modules>
<repositories>

View File

@ -9,9 +9,119 @@ tags:
---
## Intent
Private Class Data design pattern seeks to reduce exposure of
attributes by limiting their visibility. It reduces the number of class
attributes by encapsulating them in single Data object.
Private Class Data design pattern seeks to reduce exposure of attributes by limiting their
visibility. It reduces the number of class attributes by encapsulating them in single Data object.
## Explanation
Real world example
> Imagine you are cooking a stew for your family for dinner. You want to prevent your family members
> from consuming the stew by tasting it while you are cooking, otherwise there will be no more stew
> for dinner later.
In plain words
> Private class data pattern prevents manipulation of data that is meant to be immutable by
> separating the data from the methods that use it into a class that maintains the data state.
Wikipedia says
> Private class data is a design pattern in computer programming used to encapsulate class
> attributes and their manipulation.
**Programmatic Example**
Taking our stew example from above. First we have a `Stew` class where its data is not protected by
private class data, making the stew's ingredient mutable to class methods.
```java
public class Stew {
private static final Logger LOGGER = LoggerFactory.getLogger(Stew.class);
private int numPotatoes;
private int numCarrots;
private int numMeat;
private int numPeppers;
public Stew(int numPotatoes, int numCarrots, int numMeat, int numPeppers) {
this.numPotatoes = numPotatoes;
this.numCarrots = numCarrots;
this.numMeat = numMeat;
this.numPeppers = numPeppers;
}
public void mix() {
LOGGER.info("Mixing the stew we find: {} potatoes, {} carrots, {} meat and {} peppers",
numPotatoes, numCarrots, numMeat, numPeppers);
}
public void taste() {
LOGGER.info("Tasting the stew");
if (numPotatoes > 0) {
numPotatoes--;
}
if (numCarrots > 0) {
numCarrots--;
}
if (numMeat > 0) {
numMeat--;
}
if (numPeppers > 0) {
numPeppers--;
}
}
}
```
Now, we have `ImmutableStew` class, where its data is protected by `StewData` class. Now, the
methods in are unable to manipulate the data of the `ImmutableStew` class.
```java
public class StewData {
private final int numPotatoes;
private final int numCarrots;
private final int numMeat;
private final int numPeppers;
public StewData(int numPotatoes, int numCarrots, int numMeat, int numPeppers) {
this.numPotatoes = numPotatoes;
this.numCarrots = numCarrots;
this.numMeat = numMeat;
this.numPeppers = numPeppers;
}
public int getNumPotatoes() {
return numPotatoes;
}
public int getNumCarrots() {
return numCarrots;
}
public int getNumMeat() {
return numMeat;
}
public int getNumPeppers() {
return numPeppers;
}
}
public class ImmutableStew {
private static final Logger LOGGER = LoggerFactory.getLogger(ImmutableStew.class);
private final StewData data;
public ImmutableStew(int numPotatoes, int numCarrots, int numMeat, int numPeppers) {
data = new StewData(numPotatoes, numCarrots, numMeat, numPeppers);
}
public void mix() {
LOGGER
.info("Mixing the immutable stew we find: {} potatoes, {} carrots, {} meat and {} peppers",
data.getNumPotatoes(), data.getNumCarrots(), data.getNumMeat(), data.getNumPeppers());
}
}
```
Let's try creating an instance of each class and call their methods:
```java
var stew = new Stew(1, 2, 3, 4);
stew.mix(); // Mixing the stew we find: 1 potatoes, 2 carrots, 3 meat and 4 peppers
stew.taste(); // Tasting the stew
stew.mix(); // Mixing the stew we find: 0 potatoes, 1 carrots, 2 meat and 3 peppers
var immutableStew = new ImmutableStew(2, 4, 3, 6);
immutableStew.mix(); // Mixing the immutable stew we find: 2 potatoes, 4 carrots, 3 meat and 6 peppers
```
## Class diagram
![alt text](./etc/private-class-data.png "Private Class Data")

View File

@ -45,7 +45,7 @@ public final class InitializingOnDemandHolderIdiom {
}
/**
* Sigleton instance.
* Singleton instance.
*
* @return Singleton instance
*/

2
transaction-script/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target/
.idea/

View File

@ -0,0 +1,119 @@
---
layout: pattern
title: Transaction Script
folder: transaction-script
permalink: /patterns/transaction-script/
categories: Behavioral
tags:
- Data access
---
## Intent
Transaction Script organizes business logic by procedures where each procedure handles a single
request from the presentation.
## Explanation
Real world example
> You need to create a hotel room booking system. Since the requirements are quite simple we intend
> to use the Transaction Script pattern here.
In plain words
> Transaction Script organizes business logic into transactions that the system needs to carry out.
Programmatic example
The `Hotel` class takes care of booking and cancelling room reservations.
```java
public class Hotel {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private final HotelDaoImpl hotelDao;
public Hotel(HotelDaoImpl hotelDao) {
this.hotelDao = hotelDao;
}
public void bookRoom(int roomNumber) throws Exception {
Optional<Room> room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
throw new Exception("Room already booked!");
} else {
Room updateRoomBooking = room.get();
updateRoomBooking.setBooked(true);
hotelDao.update(updateRoomBooking);
}
}
}
public void cancelRoomBooking(int roomNumber) throws Exception {
Optional<Room> room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
Room updateRoomBooking = room.get();
updateRoomBooking.setBooked(false);
int refundAmount = updateRoomBooking.getPrice();
hotelDao.update(updateRoomBooking);
LOGGER.info("Booking cancelled for room number: " + roomNumber);
LOGGER.info(refundAmount + " is refunded");
} else {
throw new Exception("No booking for the room exists");
}
}
}
}
```
The `Hotel` class has two methods, one for booking and cancelling a room respectively. Each one of
them handles a single transaction in the system, making `Hotel` implement the Transaction Script
pattern.
The `bookRoom` method consolidates all the needed steps like checking if the room is already booked
or not, if not booked then books the room and updates the database by using the DAO.
The `cancelRoom` method consolidates steps like checking if the room is booked or not,
if booked then calculates the refund amount and updates the database using the DAO.
## Class diagram
![alt text](./etc/transaction-script.png "Transaction script model")
## Applicability
Use the Transaction Script pattern when the application has only a small amount of logic and that
logic won't be extended in the future.
## Consequences
* As the business logic gets more complicated,
it gets progressively harder to keep the transaction script
in a well-designed state.
* Code duplication between transaction scripts can occur.
* Normally not easy to refactor transactions script to other domain logic
patterns.
## Related patterns
* Domain Model
* Table Module
* Service Layer
## Credits
* [Transaction Script Pattern](https://dzone.com/articles/transaction-script-pattern#:~:text=Transaction%20Script%20(TS)%20is%20the,need%20big%20architecture%20behind%20them.)
* [Transaction Script](https://www.informit.com/articles/article.aspx?p=1398617)
* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0321127420&linkId=18acc13ba60d66690009505577c45c04)

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,65 @@
@startuml
package com.ashishtrivedi16.transaction-script {
class App {
- H2_DB_URL : String {static}
- LOGGER : Logger {static}
- addRooms(hotelDaoImpl : HotelDaoImpl) {static}
- createDataSource() : DataSource {static}
- createSchema(dataSource : DataSource) {static}
- deleteSchema(dataSource : DataSource) {static}
- getRoomsStatus(hotelDaoImpl : HotelDaoImpl) {static}
- generateSampleRooms() : List<Room> {static}
+ main(args : String[]) {static}
}
class Room {
- id: Int
- roomType: String
- price: Int
- booked: Boolean
+ Customer(id : int, roomType : String, price: Int, booked: Boolean)
+ getId() : int
+ getRoomType() : String
+ getPrice() : Int
+ isBooked() : Boolean
+ setId(id : int)
+ setRoomType(roomType : String)
+ setPrice(price : Int)
+ setBooked(booked : boolean)
+ equals(that : Object) : boolean
+ hashCode() : int
+ toString() : String
}
interface HotelDao {
+ add(Room) : boolean {abstract}
+ delete(Room) : boolean {abstract}
+ getAll() : Stream<Room> {abstract}
+ getById(int) : Optional<Room> {abstract}
+ update(Room) : boolean {abstract}
}
class RoomSchemaSql {
+ CREATE_SCHEMA_SQL : String {static}
+ DELETE_SCHEMA_SQL : String {static}
- RoomSchemaSql()
}
class HotelDaoImpl {
- dataSource : DataSource
+ HotelDaoImpl(dataSource : DataSource)
+ add(room : Room) : boolean
- createRoom(resultSet : ResultSet) : Room
+ delete(room : Room) : boolean
+ getAll() : Stream<Room>
+ getById(id : int) : Optional<Room>
- getConnection() : Connection
- mutedClose(connection : Connection, statement : PreparedStatement, resultSet : ResultSet)
+ update(room : Room) : boolean
}
class Hotel {
- LOGGER : Logger {static}
- hotelDao: HotelDaoImpl
+ Hotel(hotelDao: HotelDaoImpl)
+ bookRoom(roomNumber: Int)
+ cancelRoomBooking(roomNumber: Int)
}
}
HotelDaoImpl ..|> HotelDao
@enduml

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
The MIT License
Copyright © 2014-2019 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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.23.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>transaction-script</artifactId>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.iluwatar.transactionscript.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,145 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import java.util.List;
import javax.sql.DataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Transaction Script (TS) is one of the simplest domain logic pattern.
* It needs less work to implement than other domain logic patterns and therefore
* its perfect fit for smaller applications that don't need big architecture behind them.
*
* <p>In this example we will use the TS pattern to implement booking and cancellation
* methods for a Hotel management App. The main method will initialise an instance of
* {@link Hotel} and add rooms to it. After that it will book and cancel a couple of rooms
* and that will be printed by the logger.</p>
*
* <p>The thing we have to note here is that all the operations related to booking or cancelling
* a room like checking the database if the room exists, checking the booking status or the
* room, calculating refund price are all clubbed inside a single transaction script method.</p>
*/
public class App {
private static final String H2_DB_URL = "jdbc:h2:~/test";
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
/**
* Program entry point.
* Initialises an instance of Hotel and adds rooms to it.
* Carries out booking and cancel booking transactions.
* @param args command line arguments
* @throws Exception if any error occurs
*/
public static void main(String[] args) throws Exception {
final var dataSource = createDataSource();
deleteSchema(dataSource);
createSchema(dataSource);
final var dao = new HotelDaoImpl(dataSource);
// Add rooms
addRooms(dao);
// Print room booking status
getRoomStatus(dao);
var hotel = new Hotel(dao);
// Book rooms
hotel.bookRoom(1);
hotel.bookRoom(2);
hotel.bookRoom(3);
hotel.bookRoom(4);
hotel.bookRoom(5);
hotel.bookRoom(6);
// Cancel booking for a few rooms
hotel.cancelRoomBooking(1);
hotel.cancelRoomBooking(3);
hotel.cancelRoomBooking(5);
getRoomStatus(dao);
deleteSchema(dataSource);
}
private static void getRoomStatus(HotelDaoImpl dao) throws Exception {
try (var customerStream = dao.getAll()) {
customerStream.forEach((customer) -> LOGGER.info(customer.toString()));
}
}
private static void deleteSchema(DataSource dataSource) throws java.sql.SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private static void createSchema(DataSource dataSource) throws Exception {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
/**
* Get database.
*
* @return h2 datasource
*/
private static DataSource createDataSource() {
var dataSource = new JdbcDataSource();
dataSource.setUrl(H2_DB_URL);
return dataSource;
}
private static void addRooms(HotelDaoImpl hotelDao) throws Exception {
for (var room : generateSampleRooms()) {
hotelDao.add(room);
}
}
/**
* Generate rooms.
*
* @return list of rooms
*/
private static List<Room> generateSampleRooms() {
final var room1 = new Room(1, "Single", 50, false);
final var room2 = new Room(2, "Double", 80, false);
final var room3 = new Room(3, "Queen", 120, false);
final var room4 = new Room(4, "King", 150, false);
final var room5 = new Room(5, "Single", 50, false);
final var room6 = new Room(6, "Double", 80, false);
return List.of(room1, room2, room3, room4, room5, room6);
}
}

View File

@ -0,0 +1,87 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Hotel {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private final HotelDaoImpl hotelDao;
public Hotel(HotelDaoImpl hotelDao) {
this.hotelDao = hotelDao;
}
/**
* Book a room.
*
* @param roomNumber room to book
* @throws Exception if any error
*/
public void bookRoom(int roomNumber) throws Exception {
var room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
throw new Exception("Room already booked!");
} else {
var updateRoomBooking = room.get();
updateRoomBooking.setBooked(true);
hotelDao.update(updateRoomBooking);
}
}
}
/**
* Cancel a room booking.
*
* @param roomNumber room to cancel booking
* @throws Exception if any error
*/
public void cancelRoomBooking(int roomNumber) throws Exception {
var room = hotelDao.getById(roomNumber);
if (room.isEmpty()) {
throw new Exception("Room number: " + roomNumber + " does not exist");
} else {
if (room.get().isBooked()) {
var updateRoomBooking = room.get();
updateRoomBooking.setBooked(false);
int refundAmount = updateRoomBooking.getPrice();
hotelDao.update(updateRoomBooking);
LOGGER.info("Booking cancelled for room number: " + roomNumber);
LOGGER.info(refundAmount + " is refunded");
} else {
throw new Exception("No booking for the room exists");
}
}
}
}

View File

@ -0,0 +1,40 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import java.util.Optional;
import java.util.stream.Stream;
public interface HotelDao {
Stream<Room> getAll() throws Exception;
Optional<Room> getById(int id) throws Exception;
Boolean add(Room room) throws Exception;
Boolean update(Room room) throws Exception;
Boolean delete(Room room) throws Exception;
}

View File

@ -0,0 +1,172 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HotelDaoImpl implements HotelDao {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
private final DataSource dataSource;
public HotelDaoImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Stream<Room> getAll() throws Exception {
try {
var connection = getConnection();
var statement = connection.prepareStatement("SELECT * FROM ROOMS");
var resultSet = statement.executeQuery(); // NOSONAR
return StreamSupport.stream(new Spliterators.AbstractSpliterator<Room>(Long.MAX_VALUE,
Spliterator.ORDERED) {
@Override
public boolean tryAdvance(Consumer<? super Room> action) {
try {
if (!resultSet.next()) {
return false;
}
action.accept(createRoom(resultSet));
return true;
} catch (Exception e) {
throw new RuntimeException(e); // NOSONAR
}
}
}, false).onClose(() -> {
try {
mutedClose(connection, statement, resultSet);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
});
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Optional<Room> getById(int id) throws Exception {
ResultSet resultSet = null;
try (var connection = getConnection();
var statement = connection.prepareStatement("SELECT * FROM ROOMS WHERE ID = ?")) {
statement.setInt(1, id);
resultSet = statement.executeQuery();
if (resultSet.next()) {
return Optional.of(createRoom(resultSet));
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
} finally {
if (resultSet != null) {
resultSet.close();
}
}
}
@Override
public Boolean add(Room room) throws Exception {
if (getById(room.getId()).isPresent()) {
return false;
}
try (var connection = getConnection();
var statement = connection.prepareStatement("INSERT INTO ROOMS VALUES (?,?,?,?)")) {
statement.setInt(1, room.getId());
statement.setString(2, room.getRoomType());
statement.setInt(3, room.getPrice());
statement.setBoolean(4, room.isBooked());
statement.execute();
return true;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Boolean update(Room room) throws Exception {
try (var connection = getConnection();
var statement =
connection
.prepareStatement("UPDATE ROOMS SET ROOM_TYPE = ?, PRICE = ?, BOOKED = ?"
+ " WHERE ID = ?")) {
statement.setString(1, room.getRoomType());
statement.setInt(2, room.getPrice());
statement.setBoolean(3, room.isBooked());
statement.setInt(4, room.getId());
return statement.executeUpdate() > 0;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
@Override
public Boolean delete(Room room) throws Exception {
try (var connection = getConnection();
var statement = connection.prepareStatement("DELETE FROM ROOMS WHERE ID = ?")) {
statement.setInt(1, room.getId());
return statement.executeUpdate() > 0;
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
private Connection getConnection() throws Exception {
return dataSource.getConnection();
}
private void mutedClose(Connection connection, PreparedStatement statement, ResultSet resultSet)
throws Exception {
try {
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
private Room createRoom(ResultSet resultSet) throws Exception {
return new Room(resultSet.getInt("ID"),
resultSet.getString("ROOM_TYPE"),
resultSet.getInt("PRICE"),
resultSet.getBoolean("BOOKED"));
}
}

View File

@ -0,0 +1,123 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
/**
* A room POJO that represents the data that will be read from the data source.
*/
public class Room {
private int id;
private String roomType;
private int price;
private boolean booked;
/**
* Create an instance of room.
* @param id room id
* @param roomType room type
* @param price room price
* @param booked room booking status
*/
public Room(int id, String roomType, int price, boolean booked) {
this.id = id;
this.roomType = roomType;
this.price = price;
this.booked = booked;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getRoomType() {
return roomType;
}
public void setRoomType(String roomType) {
this.roomType = roomType;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public boolean isBooked() {
return booked;
}
public void setBooked(boolean booked) {
this.booked = booked;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Room room = (Room) o;
if (id != room.id) {
return false;
}
if (price != room.price) {
return false;
}
if (booked != room.booked) {
return false;
}
return roomType.equals(room.roomType);
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + roomType.hashCode();
result = 31 * result + price;
result = 31 * result + (booked ? 1 : 0);
return result;
}
@Override
public String toString() {
return "Room{"
+ "id=" + id
+ ", roomType=" + roomType
+ ", price=" + price
+ ", booked=" + booked
+ '}';
}
}

View File

@ -0,0 +1,38 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
/**
* Customer Schema SQL Class.
*/
public final class RoomSchemaSql {
public static final String CREATE_SCHEMA_SQL =
"CREATE TABLE ROOMS (ID NUMBER, ROOM_TYPE VARCHAR(100), PRICE INT(100), BOOKED VARCHAR(100))";
public static final String DELETE_SCHEMA_SQL = "DROP TABLE ROOMS IF EXISTS";
private RoomSchemaSql() {
}
}

View File

@ -0,0 +1,36 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import org.junit.jupiter.api.Test;
/**
* Tests that Transaction script example runs without errors.
*/
public class AppTest {
@Test
public void test() throws Exception {
App.main(new String[]{});
}
}

View File

@ -0,0 +1,273 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.Mockito.*;
/**
* Tests {@link HotelDaoImpl}.
*/
public class HotelDaoImplTest {
private static final String DB_URL = "jdbc:h2:~/test";
private HotelDaoImpl dao;
private Room existingRoom = new Room(1, "Single", 50, false);
/**
* Creates rooms schema.
*
* @throws SQLException if there is any error while creating schema.
*/
@BeforeEach
public void createSchema() throws SQLException {
try (var connection = DriverManager.getConnection(DB_URL);
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
}
}
/**
* Represents the scenario where DB connectivity is present.
*/
@Nested
public class ConnectionSuccess {
/**
* Setup for connection success scenario.
*
* @throws Exception if any error occurs.
*/
@BeforeEach
public void setUp() throws Exception {
var dataSource = new JdbcDataSource();
dataSource.setURL(DB_URL);
dao = new HotelDaoImpl(dataSource);
var result = dao.add(existingRoom);
Assertions.assertTrue(result);
}
/**
* Represents the scenario when DAO operations are being performed on a non existing room.
*/
@Nested
public class NonExistingRoom {
@Test
public void addingShouldResultInSuccess() throws Exception {
try (var allRooms = dao.getAll()) {
assumeTrue(allRooms.count() == 1);
}
final var nonExistingRoom = new Room(2, "Double", 80, false);
var result = dao.add(nonExistingRoom);
Assertions.assertTrue(result);
assertRoomCountIs(2);
assertEquals(nonExistingRoom, dao.getById(nonExistingRoom.getId()).get());
}
@Test
public void deletionShouldBeFailureAndNotAffectExistingRooms() throws Exception {
final var nonExistingRoom = new Room(2, "Double", 80, false);
var result = dao.delete(nonExistingRoom);
Assertions.assertFalse(result);
assertRoomCountIs(1);
}
@Test
public void updationShouldBeFailureAndNotAffectExistingRooms() throws Exception {
final var nonExistingId = getNonExistingRoomId();
final var newRoomType = "Double";
final var newPrice = 80;
final var room = new Room(nonExistingId, newRoomType, newPrice, false);
var result = dao.update(room);
Assertions.assertFalse(result);
assertFalse(dao.getById(nonExistingId).isPresent());
}
@Test
public void retrieveShouldReturnNoRoom() throws Exception {
assertFalse(dao.getById(getNonExistingRoomId()).isPresent());
}
}
/**
* Represents a scenario where DAO operations are being performed on an already existing
* room.
*/
@Nested
public class ExistingRoom {
@Test
public void addingShouldResultInFailureAndNotAffectExistingRooms() throws Exception {
var existingRoom = new Room(1, "Single", 50, false);
var result = dao.add(existingRoom);
Assertions.assertFalse(result);
assertRoomCountIs(1);
assertEquals(existingRoom, dao.getById(existingRoom.getId()).get());
}
@Test
public void deletionShouldBeSuccessAndRoomShouldBeNonAccessible() throws Exception {
var result = dao.delete(existingRoom);
Assertions.assertTrue(result);
assertRoomCountIs(0);
assertFalse(dao.getById(existingRoom.getId()).isPresent());
}
@Test
public void updationShouldBeSuccessAndAccessingTheSameRoomShouldReturnUpdatedInformation() throws
Exception {
final var newRoomType = "Double";
final var newPrice = 80;
final var newBookingStatus = false;
final var Room = new Room(existingRoom.getId(), newRoomType, newPrice, newBookingStatus);
var result = dao.update(Room);
Assertions.assertTrue(result);
final var room = dao.getById(existingRoom.getId()).get();
assertEquals(newRoomType, room.getRoomType());
assertEquals(newPrice, room.getPrice());
assertEquals(newBookingStatus, room.isBooked());
}
}
}
/**
* Represents a scenario where DB connectivity is not present due to network issue, or DB service
* unavailable.
*/
@Nested
public class ConnectivityIssue {
private static final String EXCEPTION_CAUSE = "Connection not available";
/**
* setup a connection failure scenario.
*
* @throws SQLException if any error occurs.
*/
@BeforeEach
public void setUp() throws SQLException {
dao = new HotelDaoImpl(mockedDatasource());
}
private DataSource mockedDatasource() throws SQLException {
var mockedDataSource = mock(DataSource.class);
var mockedConnection = mock(Connection.class);
var exception = new SQLException(EXCEPTION_CAUSE);
doThrow(exception).when(mockedConnection).prepareStatement(Mockito.anyString());
doReturn(mockedConnection).when(mockedDataSource).getConnection();
return mockedDataSource;
}
@Test
public void addingARoomFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.add(new Room(2, "Double", 80, false));
});
}
@Test
public void deletingARoomFailsWithExceptionAsFeedbackToTheClient() {
assertThrows(Exception.class, () -> {
dao.delete(existingRoom);
});
}
@Test
public void updatingARoomFailsWithFeedbackToTheClient() {
final var newRoomType = "Double";
final var newPrice = 80;
final var newBookingStatus = false;
assertThrows(Exception.class, () -> {
dao.update(new Room(existingRoom.getId(), newRoomType, newPrice, newBookingStatus));
});
}
@Test
public void retrievingARoomByIdFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.getById(existingRoom.getId());
});
}
@Test
public void retrievingAllRoomsFailsWithExceptionAsFeedbackToClient() {
assertThrows(Exception.class, () -> {
dao.getAll();
});
}
}
/**
* Delete room schema for fresh setup per test.
*
* @throws SQLException if any error occurs.
*/
@AfterEach
public void deleteSchema() throws SQLException {
try (var connection = DriverManager.getConnection(DB_URL);
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private void assertRoomCountIs(int count) throws Exception {
try (var allRooms = dao.getAll()) {
assertEquals(count, allRooms.count());
}
}
/**
* An arbitrary number which does not correspond to an active Room id.
*
* @return an int of a room id which doesn't exist
*/
private int getNonExistingRoomId() {
return 999;
}
}

View File

@ -0,0 +1,151 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests {@link Hotel}
*/
public class HotelTest {
private static final String H2_DB_URL = "jdbc:h2:~/test";
private Hotel hotel;
private HotelDaoImpl dao;
@BeforeEach
public void setUp() throws Exception {
final var dataSource = createDataSource();
deleteSchema(dataSource);
createSchema(dataSource);
dao = new HotelDaoImpl(dataSource);
addRooms(dao);
hotel = new Hotel(dao);
}
@Test
public void bookingRoomShouldChangeBookedStatusToTrue() throws Exception {
hotel.bookRoom(1);
assertTrue(dao.getById(1).get().isBooked());
}
@Test()
public void bookingRoomWithInvalidIdShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.bookRoom(getNonExistingRoomId());
});
}
@Test()
public void bookingRoomAgainShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.bookRoom(1);
hotel.bookRoom(1);
});
}
@Test
public void NotBookingRoomShouldNotChangeBookedStatus() throws Exception {
assertFalse(dao.getById(1).get().isBooked());
}
@Test
public void cancelRoomBookingShouldChangeBookedStatus() throws Exception {
hotel.bookRoom(1);
assertTrue(dao.getById(1).get().isBooked());
hotel.cancelRoomBooking(1);
assertFalse(dao.getById(1).get().isBooked());
}
@Test
public void cancelRoomBookingWithInvalidIdShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.cancelRoomBooking(getNonExistingRoomId());
});
}
@Test
public void cancelRoomBookingForUnbookedRoomShouldRaiseException() {
assertThrows(Exception.class, () -> {
hotel.cancelRoomBooking(1);
});
}
private static void deleteSchema(DataSource dataSource) throws java.sql.SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.DELETE_SCHEMA_SQL);
}
}
private static void createSchema(DataSource dataSource) throws Exception {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(RoomSchemaSql.CREATE_SCHEMA_SQL);
} catch (Exception e) {
throw new Exception(e.getMessage(), e);
}
}
public static DataSource createDataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setUrl(H2_DB_URL);
return dataSource;
}
private static void addRooms(HotelDaoImpl hotelDao) throws Exception {
for (var room : generateSampleRooms()) {
hotelDao.add(room);
}
}
public static List<Room> generateSampleRooms() {
final var room1 = new Room(1, "Single", 50, false);
final var room2 = new Room(2, "Double", 80, false);
final var room3 = new Room(3, "Queen", 120, false);
final var room4 = new Room(4, "King", 150, false);
final var room5 = new Room(5, "Single", 50, false);
final var room6 = new Room(6, "Double", 80, false);
return List.of(room1, room2, room3, room4, room5, room6);
}
/**
* An arbitrary number which does not correspond to an active Room id.
*
* @return an int of a room id which doesn't exist
*/
private int getNonExistingRoomId() {
return 999;
}
}

View File

@ -0,0 +1,95 @@
/*
* The MIT License
* Copyright © 2014-2019 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.transactionscript;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
* Tests {@link Room}.
*/
public class RoomTest {
private Room room;
private static final int ID = 1;
private static final String ROOMTYPE = "Single";
private static final int PRICE = 50;
private static final boolean BOOKED = false;
@BeforeEach
public void setUp() {
room = new Room(ID, ROOMTYPE, PRICE, BOOKED);
}
@Test
public void getAndSetId() {
final var newId = 2;
room.setId(newId);
assertEquals(newId, room.getId());
}
@Test
public void getAndSetRoomType() {
final var newRoomType = "Double";
room.setRoomType(newRoomType);
assertEquals(newRoomType, room.getRoomType());
}
@Test
public void getAndSetLastName() {
final var newPrice = 60;
room.setPrice(newPrice);
assertEquals(newPrice, room.getPrice());
}
@Test
public void notEqualWithDifferentId() {
final var newId = 2;
final var otherRoom = new Room(newId, ROOMTYPE, PRICE, BOOKED);
assertNotEquals(room, otherRoom);
assertNotEquals(room.hashCode(), otherRoom.hashCode());
}
@Test
public void equalsWithSameObjectValues() {
final var otherRoom = new Room(ID, ROOMTYPE, PRICE, BOOKED);
assertEquals(room, otherRoom);
assertEquals(room.hashCode(), otherRoom.hashCode());
}
@Test
public void equalsWithSameObjects() {
assertEquals(room, room);
assertEquals(room.hashCode(), room.hashCode());
}
@Test
public void testToString() {
assertEquals(String.format("Room{id=%s, roomType=%s, price=%s, booked=%s}",
room.getId(), room.getRoomType(), room.getPrice(), room.isBooked()), room.toString());
}
}