Подтвердить что ты не робот

Конкретные примеры того, почему "модель анемичного домена" считается анти-шаблоном

Прошу прощения, если это дубликат, но я не нашел конкретных примеров по теме в связанных вопросах.

После прочтения статьи Мартина Фаулера "Модель анемичного домена" , я остался блуждающим относительно того, почему это считается анти-шаблоном. Даже большинство разработчиков предприятий считают это анти-шаблоном, поскольку AFAIK, вероятно, 90% приложений j2ee спроектированы "анемичным" способом?

Может кто-то порекомендовать дальнейшее чтение по этой теме (кроме книги "Dr Driven Design" ) или даже лучше дать конкретные примеры того, как этот анти-шаблон плохо влияет на дизайн приложения.

Спасибо,

4b9b3361

Ответ 1

Учитывая следующие два класса:

class CalculatorBean  
{  
    //getters and setters  
}  

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

Если я правильно понимаю, Фаулер заявляет, что, поскольку ваш CalculatorBean - это всего лишь куча геттеров/сеттеров, вы не получаете от него никакой реальной ценности, и если вы переносите этот объект в другую систему, он ничего не сделает. Проблема в том, что ваш CalculatorBeanService содержит все, за что должен отвечать CalculatorBean. Это не так хорошо, как теперь CalculatorBean делегирует всю свою ответственность перед CalculatorBeanService

Ответ 2

Для получения полного ответа просмотрите мой блог, который также содержит примеры исходного кода [blog]: 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);
    }
}

В анемической модели вы вызываете метод и передаете ему анемичную модель, чтобы привести анемичную модель в законное состояние. Поэтому управление состояниями анемической модели размещается вне анемичной модели, и этот факт делает ее анти-шаблон в объектно-ориентированной перспективе.

Проблемы с моделью анемического порядка выше:

  • Если кто-то добавляет OrderItem в Order, значение Order.getTotal() неверно, если оно не было пересчитано с помощью OrderService. В реальном мире приложение может быть громоздким, чтобы узнать, кто добавил элемент заказа и почему OrderService не был вызван. Как вы уже узнали, заказ также нарушает инкапсуляцию списка элементов заказа. Кто-то может вызвать order.getItems().add(orderItem), чтобы добавить элемент заказа. Это может затруднить поиск кода, который действительно добавляет элемент (ссылка order.getItems() может быть передана через все приложение).
  • Метод OrderService calculateTotal отвечает за вычисление общего для всех объектов Order. Поэтому он должен быть без гражданства. Но апатрид также означает, что он не может кэшировать общее значение и только пересчитывать его, если объект 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;
    }
}

Модель с богатым доменом уважает объектно-ориентированные принципы и гарантии, что она находится в законном состоянии в любое время.

Ссылки

Ответ 3

Мартин Фаулер привносит в эту отрасль много слов и меньше понимания.

Сегодня большинство приложений (web/db) нуждаются во многих объектах, которые раскрывают свои свойства.

Любой авторитет (сам утверждающий), нахмурившийся такой практикой, должен привести пример и показать нам успешное приложение в реальном мире, полное воплощений его чудесных принципов.

Или заткнись. Отвратительно, что в нашей отрасли так много горячего воздуха. Это инженерное дело, а не драматический клуб.

Ответ 4

Ну. Вы правы, что почти весь код Java написан таким образом. Причина, по которой это анти-шаблон, заключается в том, что одним из основных принципов объектно-ориентированного проектирования является объединение данных и функций, которые работают с ним в один объект. Например, когда я писал код старой школы 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 в качестве первого параметра.

Когда С++ появился, структура стала классом, и она позволяет помещать функции в struct (класс). Затем struct неявно передается как этот указатель, поэтому вместо создания структуры и передачи ее в функции вы создаете против нее класс и методы вызова. Код более понятен и легче понять этот способ.

Затем я перешел в мир java, и все отделяют модель от службы, то есть модель - это прославленная структура, а служба, являющаяся апатридом как таковой, становится набором функций, которые работают на модель. Который для меня звучит подозрительно, как идиома языка. Это довольно забавно, потому что в c это было сделано, потому что язык не предлагал ничего лучшего, и в java это было сделано, потому что программисты не знают ничего лучше.

Ответ 5

Как и большинство вещей в мире разработки программного обеспечения, нет черных и белых. Бывают случаи, когда модель анемичной области является идеальной подгонкой.

НО есть много случаев, когда разработчики пытаются создать модель домена, также как и DDD, и в конечном итоге используют режим анонимного домена. Я думаю, что в этом случае модель анемической области считается антипатерной.

Просто убедитесь, что вы используете лучший инструмент для задания, и если он работает для вас, не беспокойтесь, изменяя его.

Ответ 6

Он просто нарушает принцип "Tell, Dont Ask" , который гласит, что объекты должны сообщать клиенту, что они могут или не могут сделать, а не подвергать свойства и оставлять его клиенту, чтобы определить, объект находится в определенном состоянии для данного действия.