DDD - Изменение состояния объекта

рассмотрите следующий упрощенный пример:

public class Ticket
{
   public int Id;
   public TicketState State;

   public Ticket()
   {
      // from where do I get the "New" state entity here? with its id and name
      State = State.New;
   }

   public void Finished()
   {
      // from where do I get the "Finished" state entity here? with its id and name          
      State = State.Finished;
   }
}

public class TicketState
{
   public int Id;
   public string Name;
}

Состояние класса используется непосредственно в рамках билета объекта области. Позже в жизненный цикл билета s другие состояния могли бы быть установлены.

Билет сохраняется в таблицу Ticket, а также TicketState. Таким образом в DB билет будет иметь внешний ключ к таблице состояния билета.

Когда установка appropiate указывает в моем объекте, как я загружаю экземпляр состояния из DB? Я должен ввести репозиторий в объект? Я должен использовать платформу как замок для такого случая? Или есть ли лучшие решения, возможно, передавая состояние снаружи?

public class Ticket
{
   //...
   public ITicketStateRepository stateRep; //<-- inject

   public Ticket()
   {
      State = stateRep.GetById(NEW_STATE_ID);
   }
   //...
}

Есть ли какая-либо лучшая практика? До сих пор я не использовал платформы внедрения зависимости или чего-либо и не допустил любые вещи персистентности в мой домен..

Другой approch:

public class Ticket
{
   //...

   public Ticket(NewTicketState newTicketState)
   {
      State = newTicketState;
   }
   public void Finished(FinishedTicketState finishedTicketState)
   {
      State = finishedTicketState;
   }
   //...
}
8
задан Chris 11 February 2010 в 00:27
поделиться

3 ответа

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

Не так уж часто при написании какого-либо библиотечного кода требуется определенный базовый класс, который предполагается расширить на уровне приложений. И если вы хотите убедиться, что код библиотеки протестирован, вам нужно средства UT конкретные методы абстрактных классов.

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

Прямо из PHPUnit manual :

abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class AbstractClassTest extends PHPUnit_Framework_TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass('AbstractClass');
        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(TRUE));

        $this->assertTrue($stub->concreteMethod());
    }
}

Mock object дают вам несколько вещей:

  • от вас не требуется конкретная реализация абстрактного класса, и вы можете уйти с помощью заглушки вместо
  • вы можете вызвать конкретные методы и утверждать, что они работают правильно
  • , если конкретный метод полагается на неосуществленный (абстрактный) метод, вы можете заглушить возвращаемое значение методом will () PHPUnit
-121-2013032-

Это является серьезным нарушением правил PCI. Документы можно получить здесь: https://www.pcisecuritystandards.org/security_standards/pci_dss.shtml Было бы разумно пойти третьей стороной, как Google Checkout или что-то подобное. Стать совместимым с PCI - это большая головная боль и включает ежегодные обзоры (могут быть оценены самостоятельно), которые могут включать тестирование на проникновение и т.д. Если вы действительно его изучили, ему, вероятно, вообще не нужно иметь доступ к информации о кредитной карте, только идентификатор операции. Вы не только должны шифровать данные, но и должны иметь сложную схему защиты ключей шифрования. Это намного больше, чем то, во что хочет попасть малый бизнес. Некоторые из приведенных выше советов звучат хорошо, но они не соответствуют спецификации PCI. Прочитайте документы, и вы быстро увидите, что это большое предприятие. В настоящее время я поддерживаю собственную систему, совместимую с PCI, и мне пришлось потратить значительные усилия, чтобы довести ее до уровня стандартов. Нам также пришлось внести ряд изменений в сеть. Это будет дешевле для бизнеса конвертировать в третьей стороне.

-121--1577292-

Билет не будет иметь ссылки на репозиторий. Он будет иметь отношения «один к одному» с TicketState, и TicketRepository просто выполнит JOIN и сопоставит значения с билетом.

Когда я создаю объекты модели, я обычно не информирую их о том, являются ли они постоянными, поэтому они не вводятся в репозиторий. Репозиторий обрабатывает все операции CRUD.

Некоторые люди возражают против этого, говоря, что это приводит к модели анемического домена ; возможно, ты один из них. Если это так, вставьте репозиторий в объект Ticket, но просто попросите его сделать JOIN и вернуть билет с заполненным состоянием. При вставке или обновлении необходимо изменить две таблицы как одну единицу работы, поэтому обязательно включите проводки.

Причина, по которой мне нравится использовать операции CRUD вне объекта модели домена, заключается в том, что он обычно не является единственным объектом домена, участвующим в сценарии использования или транзакции. Например, может быть, ваш простой сценарий использования «купить билет» будет иметь объект билета, но также может быть некоторые другие объекты, которые имеют дело с бронированиями и сидений и главной книги и инвентаризации багажа и всех видов прочего. Вы действительно хотите сохранить несколько объектов модели как одну единицу работы. Только уровень обслуживания может знать, когда объект модели действует самостоятельно и когда он является частью большего, более грандиозного плана.

Обновление:

Еще одна причина, по которой мне не нравится идея введения модельного объекта с DAO, чтобы он мог справляться с обязанностями сохранения, - это удаление слоев и циклическая зависимость, которую он вводит. Если модель остается чистой от любых ссылок на классы стойкости, их можно использовать без вызова другого слоя. Это односторонняя зависимость; персистентность знает о модели, но модель не знает о персистентности.

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

4
ответ дан 5 December 2019 в 23:15
поделиться

Мы надеемся, что этот ответ следует из ответа Даффимо.

В DDD-представлении мира ваш TicketState - это объект, который является частью агрегата Ticket (где билет - это агрегированный корень).

После этого ваш TicketRepository работает как с билетами, так и с TicketStates.

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

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

Классы вашей предметной области не должны знать ничего о модели базы данных, которая заботится о состоянии, они должны знать только о представлении состояния модели предметной области. Тогда ваш репозиторий отвечает за это сопоставление.

1
ответ дан 5 December 2019 в 23:15
поделиться

На мой взгляд, простую пару "ключ-значение", представляющую состояние в базе данных (или на любом другом носителе сохраняемости), не нужно моделировать как таковую в домен. В домене я бы сделал TicketState перечислением и поручил ITicketRepository знать, как сопоставить это с требованиями схемы базы данных.

В репозитории билетов у вас может быть кэш идентификаторов состояний билетов, привязанных к TicketState, которые лениво загружаются в статическую переменную (только один подход) из базы данных. Репозиторий билетов будет сопоставлять значение Ticket.State с идентификаторами из этого кеша для вставок / обновлений.

namespace Domain {
  public class Ticket {
    public Ticket() { State = TicketStates.New; }
    public void Finish() { State = TicketStates.Finished; }
    public TicketStates State {get;set;}
  }

  public enum TicketState { New, Finished }
}

namespace Repositories {
  public class SqlTicketRepository : ITicketRepository {
    public void Save(Ticket ticket) {
      using (var tx = new TransactionScope()) { // or whatever unit of work mechanism
        int newStateId = TicketStateIds[ticket.State];
        // update Ticket table with newStateId
      }
    }
  }

  private Dictionary<TicketState, int> _ticketStateIds;
  protected Dictionary<TicketState, int> TicketStateIds{
    get {
      if (_ticketStateIds== null) 
        InitializeTicketStateIds();
      return _ticketStateIds;
    }
  }

  private void InitializeTicketStateIds() {
    // execute SQL to get all key-values pairs from TicketStateValues table
    // use hard-coded mapping from strings to enum to populate _ticketStateIds;
  }
}
0
ответ дан 5 December 2019 в 23:15
поделиться