feature: Added Domain Model pattern (#1795)

* domain-model pattern

* fixed optional get before check isPresent

* readme minor changes

* change currency representation with joda money

* changed names of test methods

* fixed code smells

* Update domain-model/README.md

Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>

* Update domain-model/README.md

Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>

* updated readme and diagrams

Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
This commit is contained in:
AndriyPyzh
2021-07-22 16:58:21 +03:00
committed by GitHub
parent eaeb6e717c
commit eac85678f0
20 changed files with 1765 additions and 8 deletions

View File

@@ -0,0 +1,173 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import static org.joda.money.CurrencyUnit.USD;
import java.sql.SQLException;
import java.time.LocalDate;
import javax.sql.DataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.joda.money.Money;
/**
* Domain Model pattern is a more complex solution for organizing domain logic than Transaction
* Script and Table Module. It provides an object-oriented way of dealing with complicated logic.
* Instead of having one procedure that handles all business logic for a user action like
* Transaction Script there are multiple objects and each of them handles a slice of domain logic
* that is relevant to it. The significant difference between Domain Model and Table Module pattern
* is that in Table Module a single class encapsulates all the domain logic for all records stored
* in table when in Domain Model every single class represents only one record in underlying table.
*
* <p>In this example, we will use the Domain Model pattern to implement buying of products
* by customers in a Shop. The main method will create a customer and a few products.
* Customer will do a few purchases, try to buy product which are too expensive for him,
* return product which he bought to return money.</p>
*/
public class App {
public static final String H2_DB_URL = "jdbc:h2:~/test";
public static final String CREATE_SCHEMA_SQL =
"CREATE TABLE CUSTOMERS (name varchar primary key, money decimal);"
+ "CREATE TABLE PRODUCTS (name varchar primary key, price decimal, expiration_date date);"
+ "CREATE TABLE PURCHASES ("
+ "product_name varchar references PRODUCTS(name),"
+ "customer_name varchar references CUSTOMERS(name));";
public static final String DELETE_SCHEMA_SQL =
"DROP TABLE CUSTOMERS IF EXISTS;"
+ "DROP TABLE PURCHASES IF EXISTS;"
+ "DROP TABLE PRODUCTS IF EXISTS;";
/**
* Program entry point.
*
* @param args command line arguments
* @throws Exception if any error occurs
*/
public static void main(String[] args) throws Exception {
// Create data source and create the customers, products and purchases tables
final var dataSource = createDataSource();
deleteSchema(dataSource);
createSchema(dataSource);
// create customer
var customerDao = new CustomerDaoImpl(dataSource);
var tom =
Customer.builder()
.name("Tom")
.money(Money.of(USD, 30))
.customerDao(customerDao)
.build();
tom.save();
// create products
var productDao = new ProductDaoImpl(dataSource);
var eggs =
Product.builder()
.name("Eggs")
.price(Money.of(USD, 10.0))
.expirationDate(LocalDate.now().plusDays(7))
.productDao(productDao)
.build();
var butter =
Product.builder()
.name("Butter")
.price(Money.of(USD, 20.00))
.expirationDate(LocalDate.now().plusDays(9))
.productDao(productDao)
.build();
var cheese =
Product.builder()
.name("Cheese")
.price(Money.of(USD, 25.0))
.expirationDate(LocalDate.now().plusDays(2))
.productDao(productDao)
.build();
eggs.save();
butter.save();
cheese.save();
// show money balance of customer after each purchase
tom.showBalance();
tom.showPurchases();
// buy eggs
tom.buyProduct(eggs);
tom.showBalance();
// buy butter
tom.buyProduct(butter);
tom.showBalance();
// trying to buy cheese, but receive a refusal
// because he didn't have enough money
tom.buyProduct(cheese);
tom.showBalance();
// return butter and get money back
tom.returnProduct(butter);
tom.showBalance();
// Tom can buy cheese now because he has enough money
// and there is a discount on cheese because it expires in 2 days
tom.buyProduct(cheese);
tom.save();
// show money balance and purchases after shopping
tom.showBalance();
tom.showPurchases();
}
private static DataSource createDataSource() {
var dataSource = new JdbcDataSource();
dataSource.setUrl(H2_DB_URL);
return dataSource;
}
private static void deleteSchema(DataSource dataSource) throws SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(DELETE_SCHEMA_SQL);
}
}
private static void createSchema(DataSource dataSource) throws SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute(CREATE_SCHEMA_SQL);
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
/**
* This class organizes domain logic of customer.
* A single instance of this class
* contains both the data and behavior of a single customer.
*/
@Slf4j
@Getter
@Setter
@Builder
public class Customer {
@NonNull private final CustomerDao customerDao;
@Builder.Default private List<Product> purchases = new ArrayList<>();
@NonNull private String name;
@NonNull private Money money;
/**
* Save customer or update if customer already exist.
*/
public void save() {
try {
Optional<Customer> customer = customerDao.findByName(name);
if (customer.isPresent()) {
customerDao.update(this);
} else {
customerDao.save(this);
}
} catch (SQLException ex) {
LOGGER.error(ex.getMessage());
}
}
/**
* Add product to purchases, save to db and withdraw money.
*
* @param product to buy.
*/
public void buyProduct(Product product) {
LOGGER.info(
String.format(
"%s want to buy %s($%.2f)...",
name, product.getName(), product.getSalePrice().getAmount()));
try {
withdraw(product.getSalePrice());
} catch (IllegalArgumentException ex) {
LOGGER.error(ex.getMessage());
return;
}
try {
customerDao.addProduct(product, this);
purchases.add(product);
LOGGER.info(String.format("%s bought %s!", name, product.getName()));
} catch (SQLException exception) {
receiveMoney(product.getSalePrice());
LOGGER.error(exception.getMessage());
}
}
/**
* Remove product from purchases, delete from db and return money.
*
* @param product to return.
*/
public void returnProduct(Product product) {
LOGGER.info(
String.format(
"%s want to return %s($%.2f)...",
name, product.getName(), product.getSalePrice().getAmount()));
if (purchases.contains(product)) {
try {
customerDao.deleteProduct(product, this);
purchases.remove(product);
receiveMoney(product.getSalePrice());
LOGGER.info(String.format("%s returned %s!", name, product.getName()));
} catch (SQLException ex) {
LOGGER.error(ex.getMessage());
}
} else {
LOGGER.error(String.format("%s didn't buy %s...", name, product.getName()));
}
}
/**
* Print customer's purchases.
*/
public void showPurchases() {
Optional<String> purchasesToShow =
purchases.stream()
.map(p -> p.getName() + " - $" + p.getSalePrice().getAmount())
.reduce((p1, p2) -> p1 + ", " + p2);
if (purchasesToShow.isPresent()) {
LOGGER.info(name + " bought: " + purchasesToShow.get());
} else {
LOGGER.info(name + " didn't bought anything");
}
}
/**
* Print customer's money balance.
*/
public void showBalance() {
LOGGER.info(name + " balance: " + money);
}
private void withdraw(Money amount) throws IllegalArgumentException {
if (money.compareTo(amount) < 0) {
throw new IllegalArgumentException("Not enough money!");
}
money = money.minus(amount);
}
private void receiveMoney(Money amount) {
money = money.plus(amount);
}
}

View File

@@ -0,0 +1,40 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import java.sql.SQLException;
import java.util.Optional;
public interface CustomerDao {
Optional<Customer> findByName(String name) throws SQLException;
void update(Customer customer) throws SQLException;
void save(Customer customer) throws SQLException;
void addProduct(Product product, Customer customer) throws SQLException;
void deleteProduct(Product product, Customer customer) throws SQLException;
}

View File

@@ -0,0 +1,109 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import static org.joda.money.CurrencyUnit.USD;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import javax.sql.DataSource;
import org.joda.money.Money;
public class CustomerDaoImpl implements CustomerDao {
private final DataSource dataSource;
public CustomerDaoImpl(final DataSource userDataSource) {
this.dataSource = userDataSource;
}
@Override
public Optional<Customer> findByName(String name) throws SQLException {
var sql = "select * from CUSTOMERS where name = ?;";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, name);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next()) {
return Optional.of(
Customer.builder()
.name(rs.getString("name"))
.money(Money.of(USD, rs.getBigDecimal("money")))
.customerDao(this)
.build());
} else {
return Optional.empty();
}
}
}
@Override
public void update(Customer customer) throws SQLException {
var sql = "update CUSTOMERS set money = ? where name = ?;";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setBigDecimal(1, customer.getMoney().getAmount());
preparedStatement.setString(2, customer.getName());
preparedStatement.executeUpdate();
}
}
@Override
public void save(Customer customer) throws SQLException {
var sql = "insert into CUSTOMERS (name, money) values (?, ?)";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, customer.getName());
preparedStatement.setBigDecimal(2, customer.getMoney().getAmount());
preparedStatement.executeUpdate();
}
}
@Override
public void addProduct(Product product, Customer customer) throws SQLException {
var sql = "insert into PURCHASES (product_name, customer_name) values (?,?)";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, product.getName());
preparedStatement.setString(2, customer.getName());
preparedStatement.executeUpdate();
}
}
@Override
public void deleteProduct(Product product, Customer customer) throws SQLException {
var sql = "delete from PURCHASES where product_name = ? and customer_name = ?";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, product.getName());
preparedStatement.setString(2, customer.getName());
preparedStatement.executeUpdate();
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import static org.joda.money.CurrencyUnit.USD;
import java.math.RoundingMode;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.Money;
/**
* This class organizes domain logic of product.
* A single instance of this class
* contains both the data and behavior of a single product.
*/
@Slf4j
@Getter
@Setter
@Builder
@AllArgsConstructor
public class Product {
private static final int DAYS_UNTIL_EXPIRATION_WHEN_DISCOUNT_ACTIVE = 4;
private static final double DISCOUNT_RATE = 0.2;
@NonNull private final ProductDao productDao;
@NonNull private String name;
@NonNull private Money price;
@NonNull private LocalDate expirationDate;
/**
* Save product or update if product already exist.
*/
public void save() {
try {
Optional<Product> product = productDao.findByName(name);
if (product.isPresent()) {
productDao.update(this);
} else {
productDao.save(this);
}
} catch (SQLException ex) {
LOGGER.error(ex.getMessage());
}
}
/**
* Calculate sale price of product with discount.
*/
public Money getSalePrice() {
return price.minus(calculateDiscount());
}
private Money calculateDiscount() {
if (ChronoUnit.DAYS.between(LocalDate.now(), expirationDate)
< DAYS_UNTIL_EXPIRATION_WHEN_DISCOUNT_ACTIVE) {
return price.multipliedBy(DISCOUNT_RATE, RoundingMode.DOWN);
}
return Money.zero(USD);
}
}

View File

@@ -0,0 +1,36 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import java.sql.SQLException;
import java.util.Optional;
public interface ProductDao {
Optional<Product> findByName(String name) throws SQLException;
void save(Product product) throws SQLException;
void update(Product product) throws SQLException;
}

View File

@@ -0,0 +1,92 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import static org.joda.money.CurrencyUnit.USD;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import javax.sql.DataSource;
import org.joda.money.Money;
public class ProductDaoImpl implements ProductDao {
private final DataSource dataSource;
public ProductDaoImpl(final DataSource userDataSource) {
this.dataSource = userDataSource;
}
@Override
public Optional<Product> findByName(String name) throws SQLException {
var sql = "select * from PRODUCTS where name = ?;";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, name);
ResultSet rs = preparedStatement.executeQuery();
if (rs.next()) {
return Optional.of(
Product.builder()
.name(rs.getString("name"))
.price(Money.of(USD, rs.getBigDecimal("price")))
.expirationDate(rs.getDate("expiration_date").toLocalDate())
.productDao(this)
.build());
} else {
return Optional.empty();
}
}
}
@Override
public void save(Product product) throws SQLException {
var sql = "insert into PRODUCTS (name, price, expiration_date) values (?, ?, ?)";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, product.getName());
preparedStatement.setBigDecimal(2, product.getPrice().getAmount());
preparedStatement.setDate(3, Date.valueOf(product.getExpirationDate()));
preparedStatement.executeUpdate();
}
}
@Override
public void update(Product product) throws SQLException {
var sql = "update PRODUCTS set price = ?, expiration_date = ? where name = ?;";
try (var connection = dataSource.getConnection();
var preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setBigDecimal(1, product.getPrice().getAmount());
preparedStatement.setDate(2, Date.valueOf(product.getExpirationDate()));
preparedStatement.setString(3, product.getName());
preparedStatement.executeUpdate();
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/** Tests that Domain Model example runs without errors. */
final class AppTest {
@Test
void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[] {}));
}
}

View File

@@ -0,0 +1,164 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.*;
class CustomerDaoImplTest {
public static final String INSERT_CUSTOMER_SQL = "insert into CUSTOMERS values('customer', 100)";
public static final String SELECT_CUSTOMERS_SQL = "select name, money from CUSTOMERS";
public static final String INSERT_PURCHASES_SQL =
"insert into PURCHASES values('product', 'customer')";
public static final String SELECT_PURCHASES_SQL =
"select product_name, customer_name from PURCHASES";
private DataSource dataSource;
private Product product;
private Customer customer;
private CustomerDao customerDao;
@BeforeEach
void setUp() throws SQLException {
// create db schema
dataSource = TestUtils.createDataSource();
TestUtils.deleteSchema(dataSource);
TestUtils.createSchema(dataSource);
// setup objects
customerDao = new CustomerDaoImpl(dataSource);
customer = Customer.builder().name("customer").money(Money.of(CurrencyUnit.USD,100.0)).customerDao(customerDao).build();
product =
Product.builder()
.name("product")
.price(Money.of(USD, 100.0))
.expirationDate(LocalDate.parse("2021-06-27"))
.productDao(new ProductDaoImpl(dataSource))
.build();
}
@AfterEach
void tearDown() throws SQLException {
TestUtils.deleteSchema(dataSource);
}
@Test
void shouldFindCustomerByName() throws SQLException {
var customer = customerDao.findByName("customer");
assertTrue(customer.isEmpty());
TestUtils.executeSQL(INSERT_CUSTOMER_SQL, dataSource);
customer = customerDao.findByName("customer");
assertTrue(customer.isPresent());
assertEquals("customer", customer.get().getName());
assertEquals(Money.of(USD, 100), customer.get().getMoney());
}
@Test
void shouldSaveCustomer() throws SQLException {
customerDao.save(customer);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_CUSTOMERS_SQL)) {
assertTrue(rs.next());
assertEquals(customer.getName(), rs.getString("name"));
assertEquals(customer.getMoney(), Money.of(USD, rs.getBigDecimal("money")));
}
assertThrows(SQLException.class, () -> customerDao.save(customer));
}
@Test
void shouldUpdateCustomer() throws SQLException {
TestUtils.executeSQL(INSERT_CUSTOMER_SQL, dataSource);
customer.setMoney(Money.of(CurrencyUnit.USD, 99));
customerDao.update(customer);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_CUSTOMERS_SQL)) {
assertTrue(rs.next());
assertEquals(customer.getName(), rs.getString("name"));
assertEquals(customer.getMoney(), Money.of(USD, rs.getBigDecimal("money")));
assertFalse(rs.next());
}
}
@Test
void shouldAddProductToPurchases() throws SQLException {
TestUtils.executeSQL(INSERT_CUSTOMER_SQL, dataSource);
TestUtils.executeSQL(ProductDaoImplTest.INSERT_PRODUCT_SQL, dataSource);
customerDao.addProduct(product, customer);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_PURCHASES_SQL)) {
assertTrue(rs.next());
assertEquals(product.getName(), rs.getString("product_name"));
assertEquals(customer.getName(), rs.getString("customer_name"));
assertFalse(rs.next());
}
}
@Test
void shouldDeleteProductFromPurchases() throws SQLException {
TestUtils.executeSQL(INSERT_CUSTOMER_SQL, dataSource);
TestUtils.executeSQL(ProductDaoImplTest.INSERT_PRODUCT_SQL, dataSource);
TestUtils.executeSQL(INSERT_PURCHASES_SQL, dataSource);
customerDao.deleteProduct(product, customer);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_PURCHASES_SQL)) {
assertFalse(rs.next());
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Optional;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
class CustomerTest {
private CustomerDao customerDao;
private Customer customer;
private Product product;
@BeforeEach
void setUp() {
customerDao = mock(CustomerDao.class);
customer = Customer.builder()
.name("customer")
.money(Money.of(CurrencyUnit.USD, 100.0))
.customerDao(customerDao)
.build();
product = Product.builder()
.name("product")
.price(Money.of(USD, 100.0))
.expirationDate(LocalDate.now().plusDays(10))
.productDao(mock(ProductDao.class))
.build();
}
@Test
void shouldSaveCustomer() throws SQLException {
when(customerDao.findByName("customer")).thenReturn(Optional.empty());
customer.save();
verify(customerDao, times(1)).save(customer);
when(customerDao.findByName("customer")).thenReturn(Optional.of(customer));
customer.save();
verify(customerDao, times(1)).update(customer);
}
@Test
void shouldAddProductToPurchases() {
product.setPrice(Money.of(USD, 200.0));
customer.buyProduct(product);
assertEquals(customer.getPurchases(), new ArrayList<>());
assertEquals(customer.getMoney(), Money.of(USD,100));
product.setPrice(Money.of(USD, 100.0));
customer.buyProduct(product);
assertEquals(new ArrayList<>(Arrays.asList(product)), customer.getPurchases());
assertEquals(Money.zero(USD), customer.getMoney());
}
@Test
void shouldRemoveProductFromPurchases() {
customer.setPurchases(new ArrayList<>(Arrays.asList(product)));
customer.returnProduct(product);
assertEquals(new ArrayList<>(), customer.getPurchases());
assertEquals(Money.of(USD, 200), customer.getMoney());
customer.returnProduct(product);
assertEquals(new ArrayList<>(), customer.getPurchases());
assertEquals(Money.of(USD, 200), customer.getMoney());
}
}

View File

@@ -0,0 +1,127 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.joda.money.Money;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.*;
class ProductDaoImplTest {
public static final String INSERT_PRODUCT_SQL =
"insert into PRODUCTS values('product', 100, DATE '2021-06-27')";
public static final String SELECT_PRODUCTS_SQL =
"select name, price, expiration_date from PRODUCTS";
private DataSource dataSource;
private ProductDao productDao;
private Product product;
@BeforeEach
void setUp() throws SQLException {
// create schema
dataSource = TestUtils.createDataSource();
TestUtils.deleteSchema(dataSource);
TestUtils.createSchema(dataSource);
// setup objects
productDao = new ProductDaoImpl(dataSource);
product =
Product.builder()
.name("product")
.price(Money.of(USD, 100.0))
.expirationDate(LocalDate.parse("2021-06-27"))
.productDao(productDao)
.build();
}
@AfterEach
void tearDown() throws SQLException {
TestUtils.deleteSchema(dataSource);
}
@Test
void shouldFindProductByName() throws SQLException {
var product = productDao.findByName("product");
assertTrue(product.isEmpty());
TestUtils.executeSQL(INSERT_PRODUCT_SQL, dataSource);
product = productDao.findByName("product");
assertTrue(product.isPresent());
assertEquals("product", product.get().getName());
assertEquals(Money.of(USD, 100), product.get().getPrice());
assertEquals(LocalDate.parse("2021-06-27"), product.get().getExpirationDate());
}
@Test
void shouldSaveProduct() throws SQLException {
productDao.save(product);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_PRODUCTS_SQL)) {
assertTrue(rs.next());
assertEquals(product.getName(), rs.getString("name"));
assertEquals(product.getPrice(), Money.of(USD, rs.getBigDecimal("price")));
assertEquals(product.getExpirationDate(), rs.getDate("expiration_date").toLocalDate());
}
assertThrows(SQLException.class, () -> productDao.save(product));
}
@Test
void shouldUpdateProduct() throws SQLException {
TestUtils.executeSQL(INSERT_PRODUCT_SQL, dataSource);
product.setPrice(Money.of(USD, 99.0));
productDao.update(product);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement();
ResultSet rs = statement.executeQuery(SELECT_PRODUCTS_SQL)) {
assertTrue(rs.next());
assertEquals(product.getName(), rs.getString("name"));
assertEquals(product.getPrice(), Money.of(USD, rs.getBigDecimal("price")));
assertEquals(product.getExpirationDate(), rs.getDate("expiration_date").toLocalDate());
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.joda.money.Money;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.Optional;
import static org.joda.money.CurrencyUnit.USD;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;
class ProductTest {
private ProductDao productDao;
private Product product;
@BeforeEach
void setUp() {
productDao = mock(ProductDaoImpl.class);
product = Product.builder()
.name("product")
.price(Money.of(USD, 100.0))
.expirationDate(LocalDate.now().plusDays(10))
.productDao(productDao)
.build();
}
@Test
void shouldSaveProduct() throws SQLException {
when(productDao.findByName("product")).thenReturn(Optional.empty());
product.save();
verify(productDao, times(1)).save(product);
when(productDao.findByName("product")).thenReturn(Optional.of(product));
product.save();
verify(productDao, times(1)).update(product);
}
@Test
void shouldGetSalePriceOfProduct() {
assertEquals(Money.of(USD, 100), product.getSalePrice());
product.setExpirationDate(LocalDate.now().plusDays(2));
assertEquals(Money.of(USD, 80), product.getSalePrice());
}
}

View File

@@ -0,0 +1,53 @@
/*
* The MIT License
* Copyright © 2014-2021 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.domainmodel;
import org.h2.jdbcx.JdbcDataSource;
import javax.sql.DataSource;
import java.sql.SQLException;
public class TestUtils {
public static void executeSQL( String sql, DataSource dataSource) throws SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.executeUpdate(sql);
}
}
public static void createSchema(DataSource dataSource) throws SQLException {
TestUtils.executeSQL(App.CREATE_SCHEMA_SQL, dataSource);
}
public static void deleteSchema(DataSource dataSource) throws SQLException {
TestUtils.executeSQL(App.DELETE_SCHEMA_SQL, dataSource);
}
public static DataSource createDataSource() {
var dataSource = new JdbcDataSource();
dataSource.setURL(App.H2_DB_URL);
return dataSource;
}
}