Skip to content

Instantly share code, notes, and snippets.

@asufana
Last active September 12, 2016 12:41
Show Gist options
  • Save asufana/ca6cb7c13ffa9e8d6d93 to your computer and use it in GitHub Desktop.
Save asufana/ca6cb7c13ffa9e8d6d93 to your computer and use it in GitHub Desktop.
要約勘定パターンによる要約在庫管理モデル

要約勘定パターンによる要約在庫管理モデル

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;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment