Как спроектировать зависимость от класса, пытаясь избежать Закона Деметры

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

TWorkShift = class
   Date: TDateTime;
   fTotalSold: Currency;
   fSales: TList<TSale>; 
public
   property TotalSold: Currency read fTotalSold write fTotalSold;
   property Sales: Currency read fSales write fSales;
end;

TSale = class
    fAmount: Currency;
    fWorkShift: TWorkShift;
public
    property Amount: Currency read fAmount write fAmount; 
    procedure Save;  
end;

Теперь проблема, с которой я столкнулся, состоит в том, чтобы попытаться прийти к лучшей идее, не нарушая Закон Деметры. Я пытаюсь выполнить следующее:

  1. Каждый раз, когда сохраняется новый TSale, я хочу добавить его в список продаж TWorkShift текущего пользователя, а также я хочу суммировать сумму продажи с «TotalSold» TWorkShift.

Я пробовал два разных подхода:

Подход А:

// Допустим, у нас есть рабочая смена с идентификатором 1 и она загружается из базы данных с помощью: CurrentShift := TWorkShift.Create(1);

NewSale := TSale.Create;
NewSale.Amount:=100;
NewSale.Save;

CurrentShift.Sales.Add(NewSale);
CurrentShift.TotalSold := CurrentShift.TotalSold + NewSale.Amount;

Проблема с этим подходом в том, что его сложно протестировать, потому что я хочу инкапсулировать логику суммы в некоторых классах или где-то еще (может быть, в новом классе?).

Подход Б:

Мой другой подход заключается в том, чтобы включить этот код в сам класс TSale:

procedure TSale.Save;
begin
    SaveToDataBase; 

    fWorkShift.Sales.Add(Self);
    fWorkShift.TotalSold := fWorkShift.TotalSold + Self.Amount;
end;

Я думаю, что этот подход нарушает Закон Деметры и не кажется мне правильным.

Я хочу найти «правильный способ» сделать это с максимальной простотой кода и легкостью обслуживания в будущем. Поэтому любые предложения будут оценены.

Спасибо


person Luis Carrasco    schedule 26.04.2012    source источник


Ответы (2)


Если вы хотите добавить распродажу в TWorkShift, то у вас должен быть

TWorkShift.AddSale(aSale: TSale);
begin
  Sales.Add(aSale);
end;

Другими словами, TWorkShift должен «просить» то, что ему нужно.

Кроме того, я не вижу причин, по которым в TSale может быть поле TWorkShift. У рабочей смены много продаж, но зачем распродаже рабочую смену?

person Nick Hodges    schedule 26.04.2012
comment
Спасибо, Ник. Что ж, в этом случае я использую структуру Aurelius ORM, поэтому у меня есть ассоциация для получения такой информации, как: Sale := Manager.Find<TSale>(1); ShowMessage('The sale was sold in the work shift with ID:' + IntToStr(Sale.Shift.ID)); Это необходимо, потому что иногда мне нужно показать всю информацию о продаже, например, смену, в которой она было продано, дата, касса и т.д. - person Luis Carrasco; 27.04.2012
comment
Луис, это проблема уровня представления, а не BL. Уровень представления должен собирать всю необходимую информацию. Таким образом, в вашем случае вы можете получить объект продажи из объекта рабочей смены и получить всю информацию. - person whosrdaddy; 27.04.2012
comment
Луис - если ORM вынуждает вас это делать, то вам, вероятно, следует подумать об использовании другого ORM. Это плохой замысел — продавец ничего не должен знать о рабочей смене, где это произошло. Что, если вы вообще хотите продавать вещи вне Workshift? - person Nick Hodges; 28.04.2012
comment
Я должен добавить, что whosrdaddy прав: рабочая смена должна производить все свои продажи или какую-то конкретную продажу. - person Nick Hodges; 28.04.2012
comment
Спасибо, Ник, whorsdaddy. На самом деле я все еще тестирую Aurelius ORM, также я читал ваши статьи о внедрении зависимостей и Spring Framework, Ник, и я впечатлен. Пытался смешать его с Aurelius, но когда я меняю все, чтобы использовать интерфейсы, ORM ломается. Я пытаюсь добиться максимальной простоты обслуживания и стремлюсь создавать простой код. Мне понравился ваш простой код в отношении того, что WorkShift должен запрашивать только то, что ему нужно. - person Luis Carrasco; 28.04.2012

Вы что-то делаете, когда добавляете элементы в TList, чтобы вы могли использовать функцию OnNotify. Я не знаю, использует ли Aurelius это событие, поэтому я добавил код для этого. Вам нужно только посмотреть, может ли назначение OnNotify произойти внутри фреймворка после того, как список будет назначен вашему объекту TWorkShift, потому что тогда он может перезаписать обработчик событий NotifySales.

type
  TWorkShift = class
  private
    Date: TDateTime;
    fTotalSold: Currency;
    fSales: TList<TSale>;
    fNotifySales: TCollectionNotifyEvent<TSale>;
    procedure NotifySales(Sender: TObject; const Item: TSale;
      Action: TCollectionNotification);
    procedure SetSales(const Value: TList<TSale>);
  public
    property TotalSold: Currency read fTotalSold write fTotalSold;
    property Sales: TList<TSale> read fSales write SetSales;
  end;

procedure TWorkShift.NotifySales(Sender: TObject; const Item: TSale;
  Action: TCollectionNotification);
begin
  if Assigned(fNotifySales) then
    fNotifySales(Sender, Item, Action);

  case Action of
    cnAdded: fTotalSold := fTotalSold + Item.Amount;
    cnRemoved: fTotalSold := fTotalSold - Item.Amount;
  end;
end;

procedure TWorkShift.SetSales(const Value: TList<TSale>);
begin
  if Assigned(fSales) then
  begin
    fSales.OnNotify := fNotifySales;
    fNotifySales := nil;
  end;

  fSales := Value;

  if Assigned(fSales) then
  begin
    fNotifySales := fSales.OnNotify;
    fSales.OnNotify := NotifySales;
  end;
end;
person Stefan Glienke    schedule 27.04.2012
comment
Спасибо Стефан. Я попробовал ваш подход после того, как опубликовал этот вопрос, и, хотя он работает так, как задумано, я думаю, что он усложняет ситуацию и считает подход Ника более простым и понятным. Спасибо за ваш ответ, я был очень ценен для моих экспериментов. - person Luis Carrasco; 28.04.2012