UMLモデリングの本質:モデリングの実践1(酒問屋の在庫管理)での題材をJavaコードでサンプル実装する。
要約在庫とは、実在庫を任意のグループでまとめた在庫。任意の要約、および階層数で在庫を管理できるようにする。
指定日付の要約在庫(ビール系商品在庫、ワイン系商品在庫、アルコール系在庫商品など)が導出されることを確認する。
public class ScenarioTest extends UnitTest {
private StockCategory アルコール商品在庫, ビール商品在庫, ワイン商品在庫;
private Warehouse 倉庫;
private Item ドライ, 淡麗, ナパ;
private Stock ドライ在庫, 淡麗在庫, ナパ在庫;
private DateMidnight 今日, 明日;
@Before
public void before() {
Fixtures.deleteDatabase();
//要約在庫カテゴリ
アルコール商品在庫 = new StockCategory("アルコール商品在庫", null).save();
ビール商品在庫 = new StockCategory("ビール商品在庫", アルコール商品在庫).save();
ワイン商品在庫 = new StockCategory("ワイン商品在庫", アルコール商品在庫).save();
//倉庫
倉庫 = new Warehouse("倉庫").save();
//商品
ドライ = new Item("ドライ", ビール商品在庫).save();
淡麗 = new Item("淡麗", ビール商品在庫).save();
ナパ = new Item("ナパ", ワイン商品在庫).save();
//在庫
ドライ在庫 = new Stock(倉庫, ドライ, ビール商品在庫).save();
淡麗在庫 = new Stock(倉庫, 淡麗, ビール商品在庫).save();
ナパ在庫 = new Stock(倉庫, ナパ, ワイン商品在庫).save();
//期首在庫
new InitialStock(ドライ在庫, 12).save();
new InitialStock(淡麗在庫, 12).save();
new InitialStock(ナパ在庫, 12).save();
//処理日
今日 = new DateMidnight();
明日 = 今日.plusDays(1);
}
@Test
public void scenario01() throws Exception {
//現時点での各商品の在庫が1ダースずつであること
assertThat(ドライ在庫.sumQuantity(今日).value(), is(12));
assertThat(淡麗在庫.sumQuantity(今日).value(), is(12));
assertThat(ナパ在庫.sumQuantity(今日).value(), is(12));
//ドライが今日12本入荷して、明日18本出荷する
// -> 現時点で在庫が24本、明日時点で6本になること
ドライ在庫.deal(今日, +12).deal(明日, -18);
assertThat(ドライ在庫.sumQuantity(今日).value(), is(12 + 12));
assertThat(ドライ在庫.sumQuantity(明日).value(), is(12 + 12 - 18));
//淡麗が今日24本入荷して、明日10本出荷する
// -> 現時点で在庫が36本、明日時点で26本になること
淡麗在庫.deal(今日, +24).deal(明日, -10).save();
assertThat(淡麗在庫.sumQuantity(今日).value(), is(12 + 24));
assertThat(淡麗在庫.sumQuantity(明日).value(), is(12 + 24 - 10));
//ナパが今日0本入荷して、明日12本出荷する
// -> 現時点で在庫が12本、明日時点で0本になること
ナパ在庫.deal(今日, +0).deal(明日, -12).save();
assertThat(ナパ在庫.sumQuantity(今日).value(), is(12 + 0));
assertThat(ナパ在庫.sumQuantity(明日).value(), is(12 + 0 - 12));
//要約在庫確認
assertThat(ビール商品在庫.sumQuantity(今日).value(), is(24 + 36));
assertThat(ビール商品在庫.sumQuantity(明日).value(), is(24 + 36 - 18 - 10));
assertThat(ワイン商品在庫.sumQuantity(今日).value(), is(12 + 0));
assertThat(ワイン商品在庫.sumQuantity(明日).value(), is(12 - 12));
assertThat(アルコール商品在庫.sumQuantity(今日).value(), is(24 + 36 + 12));
assertThat(アルコール商品在庫.sumQuantity(明日).value(), is(24 + 36 + 12 - 18 - 10 - 12));
}
}
*以下ソースコードはEntityModelsほか、オレオレ基底クラスが存在しないと動作しません。ソースイメージとしてお読みください。
@Entity(name = "item")
public class Item extends EntityModels<Item> {
/** 品名 */
@Embedded
private final ItemName name;
/** コンストラクタ(overload) */
public Item(final String name, final StockCategory category) {
this(new ItemName(name));
}
/** コンストラクタ */
public Item(final ItemName name) {
this.name = name;
}
@Override
public boolean isSatisfied() {
new Valid(name).notNull();
return true;
}
//-----------------------------------------------------
/** 品名VO */
@Embeddable
public static class ItemName extends ValueObject<ItemName> {
@Column(name = "name", nullable = false, length = 255)
private final String value;
//コンストラクタ
public ItemName(final String value) {
this.value = value;
validate();
}
}
}
@Entity(name = "warehouse")
public class Warehouse extends EntityModels<Warehouse> {
/** 倉庫名 */
@Column
private final WarehouseName name;
/** コンストラクタ(overload) */
public Warehouse(final String name) {
this(new WarehouseName(name));
}
/** コンストラクタ */
public Warehouse(final WarehouseName name) {
this.name = name;
}
@Override
public boolean isSatisfied() {
new Valid(name).notNull();
return true;
}
//-----------------------------------------------------
/** 倉庫名VO */
@Embeddable
public static class WarehouseName extends ValueObject<WarehouseName> {
@Column(name = "name", nullable = false, length = 255)
private final String value;
//コンストラクタ
public WarehouseName(final String value) {
this.value = value;
validate();
}
}
}
@Entity(name = "stock")
public class Stock extends EntityModels<Stock> implements StockCalculable {
/** 倉庫 */
@ManyToOne(fetch = FetchType.LAZY)
private final Warehouse warehouse;
/** 商品 */
@ManyToOne(fetch = FetchType.LAZY)
private final Item item;
/** 要約在庫 */
@ManyToOne(fetch = FetchType.LAZY)
private final StockCategory category;
/** コンストラクタ */
public Stock(final Warehouse warehouse,
final Item item,
final StockCategory category) {
this.warehouse = warehouse;
this.item = item;
this.category = category;
}
@Override
public boolean isSatisfied() {
new Valid(warehouse).notNull();
new Valid(item).notNull();
new Valid(category).notNull();
return true;
}
/** 直近の期首在庫を取得 */
private InitialStock initialStock() {
return InitialStockRepo.findBy(this);
}
/** 指定日の在庫 */
@Override
public StockQuantity sumQuantity(final DateMidnight date) {
return ItemStockCalc.calc(initialStock(), date);
}
/** 取引ファクトリ */
public Stock deal(final DateMidnight date, final Integer quantity) {
new Deal(date, this, quantity).save();
return this;
}
//-----------------------------------------------------
/** 期首在庫数VO */
@Embeddable
public static class InitialQuantity extends ValueObject<InitialQuantity> {
@Column(name = "initial_quantity", nullable = false)
private final Integer value;
//コンストラクタ
public InitialQuantity(final Integer value) {
this.value = value;
validate();
}
/** 取引数との加算 */
public StockQuantity addQuantity(final DealQuantity dealQuantity) {
return new StockQuantity(value + dealQuantity.value());
}
}
/** 在庫数VO */
@Embeddable
public static class StockQuantity extends ValueObject<StockQuantity> {
@Column(name = "stock_quantity", nullable = false)
private final Integer value;
//コンストラクタ
public StockQuantity(final Integer value) {
this.value = value;
validate();
}
public Integer value() {
return value;
}
/** 加算 */
public StockQuantity add(final StockQuantity quantity) {
return new StockQuantity(quantity.value + value);
}
}
//-----------------------------------------------------
/** リポジトリクラス */
public static class StockRepo {
public static StockCollection findBy(final StockCategory category) {
final List<Stock> stocks = Stock.find("category=?", category)
.fetch();
return new StockCollection(stocks);
}
}
}
public class StockCollection extends AbstractCollection<StockCollection, Stock> {
public static final StockCollection EMPTY = new StockCollection(null);
/** コンストラクタ */
public StockCollection(final List<Stock> list) {
super(list);
}
@Override
protected AbstractCollection constructor(final List<Stock> list) {
return new StockCollection(list);
}
/** 要約在庫の算出 */
public StockQuantity sumQuantity(final DateMidnight date) {
StockQuantity quantity = new StockQuantity(0);
for (final Stock stock : list) {
quantity = quantity.add(stock.sumQuantity(date));
}
return quantity;
}
}
@Entity(name = "initial_stock")
public class InitialStock extends EntityModels<InitialStock> {
/** 在庫 */
@ManyToOne(fetch = FetchType.LAZY)
private final Stock stock;
/** 在庫数 */
@Column
private final InitialQuantity quantity;
/** コンストラクタ(overload) */
public InitialStock(final Stock stock, final Integer quantity) {
this(stock, new InitialQuantity(quantity));
}
/** コンストラクタ */
public InitialStock(final Stock stock, final InitialQuantity quantity) {
this.stock = stock;
this.quantity = quantity;
}
@Override
public boolean isSatisfied() {
new Valid(stock).notNull();
new Valid(quantity).notNull();
return true;
}
/** 期首在庫設定日以後から指定日までの取引一覧取得 */
public DealCollection deals(final DateMidnight endDate) {
//TODO ちゃんと期首在庫設定日を見ること
final DealCollection deals = DealRepo.findBy(stock);
//指定日までの取引を抽出する
return deals.filterByEndDate(endDate);
}
/** 取引数との加算 */
public StockQuantity add(final DealQuantity dealQuantity) {
return quantity.addQuantity(dealQuantity);
}
//-----------------------------------------------------
/** リポジトリクラス */
public static class InitialStockRepo {
/** 当該在庫の直近期首在庫を取得する */
public static InitialStock findBy(final Stock stock) {
return InitialStock.find("stock=? order by id desc", stock).first();
}
}
}
public class StockCalc {
/** 指定日の在庫 */
public static StockQuantity calc(final InitialStock initialStock,
final DateMidnight date) {
//期首在庫設定日以後の指定在庫の取引一覧を取得
final DealCollection deals = initialStock.deals(date);
//期首在庫設定日以後の数量合計を取得
final DealQuantity dealQuantity = deals.quantity();
//期首在庫と合算
return initialStock.add(dealQuantity);
}
}
@Entity(name = "deal")
public class Deal extends EntityModels<Deal> {
/** 取引日 */
@Column
private final DealDate date;
/** 在庫 */
@ManyToOne(fetch = FetchType.LAZY)
private final Stock stock;
/** 取引数 */
@Column
private final DealQuantity quantity;
/** コンストラクタ(overload) */
public Deal(final DateMidnight date,
final Stock stock,
final Integer quantity) {
this(new DealDate(date), stock, new DealQuantity(quantity));
}
/** コンストラクタ */
public Deal(final DealDate date,
final Stock stock,
final DealQuantity quantity) {
this.date = date;
this.stock = stock;
this.quantity = quantity;
}
@Override
public boolean isSatisfied() {
new Valid(stock).notNull();
new Valid(quantity).notNull();
new Valid(date).notNull();
return true;
}
/** 取引数 */
public DealQuantity quantity() {
return quantity;
}
/** 取引日 */
public DealDate dealDate() {
return date;
}
//-----------------------------------------------------
/** 取引日VO */
@Embeddable
public static class DealDate extends ValueObject<DealDate> {
@Column(name = "deal_date", nullable = true)
@Type(type = "org.joda.time.contrib.hibernate.PersistentDateTime")
private final DateTime value;
//コンストラクタ
public DealDate(final DateMidnight value) {
this.value = value.toDateTime();
new Valid(value).notNull();
}
/** 指定日と同じか過去か */
public boolean isEqualOrBefore(final DateMidnight date) {
return value.toDateMidnight().isEqual(date)
|| value.toDateMidnight().isBefore(date);
}
}
/** 取引数VO */
@Embeddable
public static class DealQuantity extends ValueObject<DealQuantity> {
@Column(name = "deal_quantity", nullable = false)
private final Integer value;
//コンストラクタ
public DealQuantity(final Integer value) {
this.value = value;
validate();
}
/** 値 */
public Integer value() {
return value;
}
/** 加算 */
public DealQuantity add(final DealQuantity other) {
return new DealQuantity(value + other.value);
}
}
//-----------------------------------------------------
/** リポジトリクラス */
public static class DealRepo {
public static DealCollection findBy(final Stock stock) {
final List<Deal> deals = Deal.find("stock=?", stock).fetch();
return new DealCollection(deals);
}
}
}
public class DealCollection extends AbstractCollection<DealCollection, Deal> {
public static final DealCollection EMPTY = new DealCollection(null);
/** コンストラクタ */
public DealCollection(final List<Deal> list) {
super(list);
}
@Override
protected AbstractCollection constructor(final List<Deal> list) {
return new DealCollection(list);
}
/** 取引数量の合計 */
public DealQuantity quantity() {
DealQuantity quantity = new DealQuantity(0);
for (final Deal deal : list()) {
quantity = quantity.add(deal.quantity());
}
return quantity;
}
/** 指定日までの取引を抽出 */
public DealCollection filterByEndDate(final DateMidnight endDate) {
final List<Deal> newList = new ArrayList<Deal>();
for (final Deal deal : list) {
if (deal.dealDate().isEqualOrBefore(endDate)) {
newList.add(deal);
}
}
return new DealCollection(newList);
}
}
@Entity(name = "stock_category")
public class StockCategory extends EntityModels<StockCategory> implements StockCalculable {
/** 要約在庫名 */
@Embedded
private final StockCategoryName name;
/** 親要約在庫 */
@ManyToOne(fetch = FetchType.LAZY)
private final StockCategory parentCategory;
/** コンストラクタ(overload) */
public StockCategory(final String name, final StockCategory parentCategory) {
this(new StockCategoryName(name), parentCategory);
}
/** コンストラクタ */
public StockCategory(final StockCategoryName name,
final StockCategory parentCategory) {
this.name = name;
this.parentCategory = parentCategory;
}
@Override
public boolean isSatisfied() {
new Valid(name).notNull();
//要約カテゴリルートの場合、親を取らないため new Valid(parentCategory).notNull() しない
return true;
}
/** 直下の子カテゴリ一覧 */
private StockCategoryCollection childStockCategories() {
return StockCategoryRepo.findChildStockCategories(this);
}
@Override
/** 指定日時点での当該カテゴリの在庫量(配下のカテゴリを含む) */
public StockQuantity sumQuantity(final DateMidnight date) {
final StockCategoryCollection childStockCategories = childStockCategories();
if (childStockCategories.size() != 0) {
//子カテゴリが存在する場合には、再帰処理
return childStockCategories.sumQuantity(date);
}
else {
final StockCollection stocks = StockRepo.findBy(this);
return stocks.sumQuantity(date);
//子カテゴリが存在しない場合には自身が末端
}
}
//-----------------------------------------------------
/** 要約在庫名VO */
@Embeddable
public static class StockCategoryName extends ValueObject<StockCategoryName> {
@Column(name = "name", nullable = false, length = 255)
private final String value;
//コンストラクタ
public StockCategoryName(final String value) {
this.value = value;
validate();
}
}
//-----------------------------------------------------
/** リポジトリクラス */
public static class StockCategoryRepo {
public static StockCategoryCollection findChildStockCategories(final StockCategory parentCategory) {
final List<StockCategory> categories = StockCategory.find("parentCategory=?",
parentCategory)
.fetch();
return categories != null && categories.size() != 0
? new StockCategoryCollection(categories)
: StockCategoryCollection.EMPTY;
}
}
}
/** 在庫量計算インターフェース */
public interface StockCalculable {
/** (指定日付時点での)在庫数 */
StockQuantity sumQuantity(DateMidnight date);
}
/** 要約在庫リスト */
public class StockCategoryCollection extends AbstractCollection<StockCategoryCollection, StockCategory> {
public static final StockCategoryCollection EMPTY = new StockCategoryCollection((List) null);
/** コンストラクタ(overload) */
public StockCategoryCollection(final StockCategory category) {
this(Arrays.asList(category));
}
/** コンストラクタ */
public StockCategoryCollection(final List<StockCategory> list) {
super(list);
}
@Override
protected AbstractCollection constructor(final List<StockCategory> list) {
return new StockCategoryCollection(list);
}
/** 要約在庫の算出 */
public StockQuantity sumQuantity(final DateMidnight date) {
StockQuantity quantity = new StockQuantity(0);
for (final StockCategory category : list) {
quantity = quantity.add(category.sumQuantity(date));
}
return quantity;
}
}