23

これが重複している場合は申し訳ありませんが、関連する質問でトピックに関する具体的な例を見つけることができませんでした.

「Anemic Domain Model」に関する Martin Fowler の記事を読んだ後、なぜこれがアンチパターンと見なされるのか疑問に思いました。私の知る限り、j2ee アプリケーションの 90% は「貧弱な」方法で設計されているため、大多数のエンタープライズ開発者はそれをアンチパターンと考えていますか?

誰かがこのトピックについてさらに読むことをお勧めできますか (「ドメイン駆動設計」の本以外)、またはさらに良いことに、このアンチパターンがアプリケーションの設計にどのように悪い影響を与えているかについて具体的な例を挙げてください。

ありがとう、

4

6 に答える 6

62

マーティン・ファウラーは、この業界に多くの言葉とあまり理解をもたらしません。

今日のアプリケーション (web/db) の大半は、プロパティを公開する多くのオブジェクトを必要とします。

そのような慣行に眉をひそめている(自称)権威は、模範を示し、彼の素晴らしい原則の具現化に満ちた成功した現実世界のアプリケーションを私たちに示すべきです.

もしくは黙ってろ。私たちの業界に非常に多くの熱気があるのは気分が悪くなります。これはエンジニアリングであり、演劇クラブではありません。

于 2011-06-09T15:24:24.263 に答える
59

完全な回答については、ソース コードの例を含む私のブログをご覧ください [ブログ]: https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

オブジェクト指向の観点から貧血ドメイン モデルを見ると、それは純粋な手続き型プログラミングであるため、間違いなくアンチパターンです。これがアンチパターンと呼ばれる理由は、主なオブジェクト指向の原則が貧血ドメインモデルでカバーされていないためです。

オブジェクト指向とは、オブジェクトがその状態を管理し、常に正当な状態であることを保証することを意味します。(データ隠蔽、カプセル化)

したがって、オブジェクトはデータをカプセル化し、そのアクセスと解釈を管理します。これとは対照的に、貧血モデルは、いつでも合法的な状態にあることを保証するものではありません.

注文品目を含む注文の例は、違いを示すのに役立ちます。それでは、注文の貧血モデルを見てみましょう。

貧血モデル

 public class Order {
    private BigDecimal total = BigDecimal.ZERO;
    private List<OrderItem> items = new ArrayList<OrderItem>();

    public BigDecimal getTotal() {
        return total;
    }

    public void setTotal(BigDecimal total) {
        this.total = total;
    }

    public List<OrderItem> getItems() {
        return items;
    }

    public void setItems(List<OrderItem> items) {
        this.items = items;
    }
}

public class OrderItem {

    private BigDecimal price = BigDecimal.ZERO;
    private int quantity;
    private String name;
    
    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

では、注文と注文項目を解釈して注文合計を計算するロジックはどこにあるのでしょうか。このロジックは、多くの場合、*Helper、*Util、*Manager、または単に *Service という名前のクラスに配置されます。貧血モデルの注文サービスは次のようになります。

public class OrderService {
    public void calculateTotal(Order order) {
        if (order == null) {
             throw new IllegalArgumentException("order must not be null");
        }

        BigDecimal total = BigDecimal.ZERO;
        List<OrderItem> items = order.getItems();

        for (OrderItem orderItem : items) {
            int quantity = orderItem.getQuantity();
            BigDecimal price = orderItem.getPrice();
            BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
            total = total.add(itemTotal);
        }
        order.setTotal(total);
    }
}

貧血モデルでは、メソッドを呼び出して貧血モデルに渡し、貧血モデルを有効な状態にします。したがって、貧血モデルの状態管理は貧血モデルの外に置かれ、この事実はオブジェクト指向の観点から見たアンチパターンになります。

場合によっては、貧血モデルを変更しないわずかに異なるサービス実装が表示されることがあります。代わりに、計算した値を返します。例えば

public BigDecimal calculateTotal(Order order); 

この場合、 にOrderはプロパティがありませんtotalOrderここで不変を作成する場合は、関数型プログラミングへの道を進んでいます。しかし、これは私がここで見つけることができない別のトピックです.

上記の貧血症モデルの問題点は次のとおりです。

  • 誰かが OrderItem を Order に追加した場合Order.getTotal()、OrderService によって再計算されていない限り、値は正しくありません。実際のアプリケーションでは、誰が注文アイテムを追加したのか、なぜ OrderService が呼び出されなかったのかを調べるのは面倒です。既にお気づきかもしれませんが、Order は注文項目リストのカプセル化も破ります。誰かが電話order.getItems().add(orderItem)して注文項目を追加できます。これにより、アイテムを実際に追加するコードを見つけるのが難しくorder.getItems()なる可能性があります (参照はアプリケーション全体に渡されます)。
  • のメソッドは、すべての Order オブジェクトOrderServicecalculateTotal 合計を計算します。したがって、ステートレスでなければなりません。ただし、ステートレスとは、合計値をキャッシュできず、Order オブジェクトが変更された場合にのみ再計算できることも意味します。したがって、calculateTotal メソッドに長い時間がかかる場合は、パフォーマンスの問題もあります。それでも、注文が合法的な状態にあるかどうかをクライアントが認識できないため、パフォーマンスの問題が発生する可能性がcalculateTotal(..)あります。

また、サービスが貧血モデルを更新せず、代わりに結果を返すだけである場合もあります。例えば

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        if (order == null) {
             throw new IllegalArgumentException("order must not be null");
        }

        BigDecimal total = BigDecimal.ZERO;
        List<OrderItem> items = order.getItems();

        for (OrderItem orderItem : items) {
            int quantity = orderItem.getQuantity();
            BigDecimal price = orderItem.getPrice();
            BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
            total = total.add(itemTotal);
        }
       return total;
    }
}

この場合、サービスはある時点で貧血モデルの状態を解釈し、その結果で貧血モデルを更新しません。このアプローチの唯一の利点は、貧血モデルにはプロパティtotalがないため、無効な状態を含めることができないことです。totalただし、これは、total必要になるたびに を計算する必要があることも意味します。プロパティを削除することで、total開発者は のプロパティの状態に依存せずにサービスを使用するようになりますtotal。ただし、これは開発者が何らかの方法で値をキャッシュすることを保証するものではなく、total古い値も使用する可能性があります。サービスを実装するこの方法は、プロパティが別のプロパティから派生するときはいつでも実行できます。つまり、基本的なデータを解釈するとき。例int getAge(Date birthday)

次に、リッチ ドメイン モデルを見て違いを確認します。

リッチ ドメイン アプローチ

public class Order {

    private BigDecimal total;
    private List<OrderItem> items = new ArrayList<OrderItem>();

    /**
      * The total is defined as the sum of all {@link OrderItem#getTotal()}.
      *
      * @return the total of this {@link Order}.
      */
    public BigDecimal getTotal() {
        if (total == null) {
           /*
            * we have to calculate the total and remember the result
            */
           BigDecimal orderItemTotal = BigDecimal.ZERO;
           List<OrderItem> items = getItems();

           for (OrderItem orderItem : items) {
               BigDecimal itemTotal = orderItem.getTotal();
               /*
                * add the total of an OrderItem to our total.
                */
               orderItemTotal = orderItemTotal.add(itemTotal);
           }

           this.total = orderItemTotal;
           }
        return total;
        }

   /**
    * Adds the {@link OrderItem} to this {@link Order}.
    *
    * @param orderItem
    *            the {@link OrderItem} to add. Must not be null.
    */
    public void addItem(OrderItem orderItem) {
        if (orderItem == null) {
            throw new IllegalArgumentException("orderItem must not be null");
        }
        if (this.items.add(orderItem)) {
           /*
            * the list of order items changed so we reset the total field to
            * let getTotal re-calculate the total.
            */ 
            this.total = null;
        }
    }

    /**
      *
      * @return the {@link OrderItem} that belong to this {@link Order}. Clients
      *         may not modify the returned {@link List}. Use
      *         {@link #addItem(OrderItem)} instead.
      */
    public List<OrderItem> getItems() {
       /*
        * we wrap our items to prevent clients from manipulating our internal
        * state.
        */
        return Collections.unmodifiableList(items);
    }

}

public class OrderItem {

    private BigDecimal price;

    private int quantity;

    private String name = "no name";

    public OrderItem(BigDecimal price, int quantity, String name) {
     if (price == null) {
      throw new IllegalArgumentException("price must not be null");
     }
     if (name == null) {
      throw new IllegalArgumentException("name must not be null");
     }
     if (price.compareTo(BigDecimal.ZERO) < 0) {
      throw new IllegalArgumentException(
        "price must be a positive big decimal");
     }
     if (quantity < 1) {
      throw new IllegalArgumentException("quantity must be 1 or greater");
     }
     this.price = price;
     this.quantity = quantity;
     this.name = name;
    }

    public BigDecimal getPrice() {
     return price;
    }

    public int getQuantity() {
     return quantity;
    }

    public String getName() {
     return name;
    }

    /**
      * The total is defined as the {@link #getPrice()} multiplied with the
      * {@link #getQuantity()}.
      *
      * @return
      */
    public BigDecimal getTotal() {
     int quantity = getQuantity();
      BigDecimal price = getPrice();
      BigDecimal total = price.multiply(new BigDecimal(quantity));
     return total;
    }
}

リッチ ドメイン モデルは、オブジェクト指向の原則を尊重し、常に合法的な状態にあることを保証します。

参考文献

于 2013-02-22T09:11:45.270 に答える
17

良い。ほとんどすべての Java コードがこのように記述されていることは間違いありません。これがアンチ パターンである理由は、オブジェクト指向設計の主な原則の 1 つは、データとそれを操作する関数を 1 つのオブジェクトに結合することだからです。たとえば、古い学校の C コードを書いていたときは、次のようなオブジェクト指向設計を模倣していました。

struct SomeStruct {
    int x;
    float y;
};

void some_op_i(SomeStruct* s, int x) {
    // do something
}
void some_op_f(SomeStruct* s, float y) {
    // something else
}

つまり、この言語では関数を組み合わせて構造体の内部で SomeStruct を操作することができなかったため、慣例により SomeStruct を最初のパラメーターとして使用する自由な関数のグループを作成しました。

C++ が登場すると、構造体はクラスになり、関数を構造体 (クラス) に入れることができます。次に、構造体が this ポインターとして暗黙的に渡されるため、構造体を作成して関数に渡す代わりに、クラスを作成してそれに対してメソッドを呼び出します。この方法により、コードがより明確になり、理解しやすくなります。

それから私は Java の世界に移り、誰もがモデルをサービスから分離しました。つまり、モデルは美化された構造体であり、サービスはそのままではステートレスであり、モデル上で動作する関数の集まりになります。私には、ac 言語のイディオムのように疑わしく聞こえます。c では言語が優れたものを提供しなかったために行われ、Java ではプログラマーがそれ以上のことを知らないために行われたため、これはかなり面白いことです。

于 2011-06-09T14:22:36.040 に答える
12

次の 2 つのクラスがあるとします。

class CalculatorBean  
{  
    //getters and setters  
}  

class CalculatorBeanService  
{  
   Number calculate(Number first, Number second);  
    {  
       //do calculation  
    }  
} 

私の理解が正しければ、ファウラーは、あなたCalculatorBeanはゲッター/セッターの集まりにすぎないので、そこから実際の価値を得ることができず、そのオブジェクトを別のシステムに移植しても何もしないと述べています。問題には、が責任を負うべきCalculatorBeanServiceものがすべて含まれているようです。CalculatorBean現在、CalculatorBeanすべての責任をCalculatorBeanService

于 2011-06-09T14:10:04.173 に答える
5

ソフトウェア開発の世界のほとんどのものと同様に、白黒はありません。貧血ドメインモデルが完全に適合する場合があります。

しかし、開発者がドメインモデルを構築しようとし、別名DDDを実行しようとして、代わりに貧血のドメインモードになってしまう場合がたくさんあります。この場合、貧血ドメインモデルは反パターンと見なされると思います。

仕事に最適なツールを使用していることを確認してください。それが機能する場合は、わざわざ変更しないでください。

于 2011-06-10T07:51:23.273 に答える