Архитектура на C# приложение - EF5 и разбиране на слоя на услугата

По време на работа бях хвърлен в разработването на наследено корпоративно приложение, което все още е в процес на производство и е спряно през последните няколко месеца поради лош дизайн и нестабилност.

Така че започнахме да използваме EF5 и да прилагаме някои дизайнерски модели / слоеве към нашето приложение.

Това, което се мъча да разбера, е: какво точно трябва да прави Сервизният слой в нашия случай? Ще бъде ли свръхархитектуриране или ще предостави някои предимства, без да добавя ненужна сложност?

Нека ви покажем какво имаме досега:

  • въведохме EF (Code First with POCOs), за да картографираме нашата наследена база данни (работи сравнително добре)
  • ние създадохме хранилища за повечето неща, от които се нуждаем в нашия нов слой данни (специфични реализации, не виждам никаква полза по отношение на разделянето на загриженост, използвайки общи репозитории..)

Сега в конкретния случай става въпрос за изчисляване на цени за артикул - или чрез получаване на цена от дадена статия директно, или от групата, в която е артикулът (ако няма посочена цена). Става много по-сложно, защото има и различни ценови листи (в зависимост от пълната стойност на поръчката) и в зависимост от клиента, който също може да има специални цени и т.н.

Така че основният ми въпрос е: кой е отговорен за получаването на правилната цена?

Моите мисли са: Поръчката трябва да знае за елементите, от които се състои. Тези артикули от друга страна трябва да знаят каква е цената им, но поръчката не трябва да знае как да изчисли цената на артикула, а просто трябва да обобщи разходите им.

Извадка от моя код в момента:

ArticlePrice (POCO, картографирането скоро ще бъде заменено от Fluid API)

[Table("artikeldaten_preise")]
public class ArticlePrice : BaseEntity
{
    [Key]
    [Column("id")]
    public int Id { get; set; }

    [Column("einheit")]
    public int UnitId { get; set; }

    [ForeignKey("UnitId")]
    public virtual Unit Unit { get; set; }

    [Column("preisliste")]
    public int PricelistId { get; set; }

    [ForeignKey("PricelistId")]
    public virtual Pricelist Pricelist { get; set; }

    [Column("artikel")]
    public int ArticleId { get; set; }

    [ForeignKey("ArticleId")]
    public virtual Article Article { get; set; }

    public PriceInfo PriceInfo { get; set; }

}

Хранилище за цени на артикули:

   public class ArticlePriceRepository : CarpetFiveRepository
{
    public ArticlePriceRepository(CarpetFiveContext context) : base(context) {}

    public IEnumerable<ArticlePrice> FindByCriteria(ArticlePriceCriteria criteria)
    {
        var prices = from price in DbContext.ArticlePrices
                     where
                         price.PricelistId == criteria.Pricelist.Id
                         && price.ArticleId == criteria.Article.Id
                         && price.UnitId == criteria.Unit.Id
                         && price.Deleted == false
                     select price;

        return prices.ToList();
    }
}

public class ArticlePriceCriteria
{
    public Pricelist Pricelist { get; set; }
    public Article Article { get; set; }
    public Unit Unit { get; set; }

    public ArticlePriceCriteria(Pricelist pricelist, Article article, Unit unit)
    {
        Pricelist = pricelist;
        Article = article;
        Unit = unit;
    }
}

PriceService (има ужасяваща миризма на код...)

public class PriceService
{
    private PricelistRepository _pricelistRepository;
    private ArticlePriceRepository _articlePriceRepository;
    private PriceGroupRepository _priceGroupRepository;

    public PriceService(PricelistRepository pricelistRepository, ArticlePriceRepository articlePriceRepository, PriceGroupRepository priceGroupRepository)
    {
        _pricelistRepository = pricelistRepository;
        _articlePriceRepository = articlePriceRepository;
        _priceGroupRepository = priceGroupRepository;
    }

    public double GetByArticle(Article article, Unit unit, double amount = 1, double orderValue = 0, DateTime dateTime = new DateTime())
    {
        var pricelists = _pricelistRepository.FindByDate(dateTime, orderValue);

        var articlePrices = new List<ArticlePrice>();

        foreach (var list in pricelists)
            articlePrices.AddRange(_articlePriceRepository.FindByCriteria(new ArticlePriceCriteria(list, article, unit)));

        double price = 0;
        double priceDiff = 0;

        foreach (var articlePrice in articlePrices)
        {
            switch (articlePrice.PriceInfo.Type)
            {
                    case PriceTypes.Absolute:
                        price = articlePrice.PriceInfo.Price;
                        break;
                    case PriceTypes.Difference:
                        priceDiff = priceDiff + articlePrice.PriceInfo.Price;
                    break;
            }
        }

        return (price + priceDiff) * amount;
    }

    public double GetByPriceGroup(PriceGroup priceGroup, Unit unit)
    {
        throw new NotImplementedException("not implemented yet");
    }

    //etc. you'll get the point that this approach might be completely WRONG

}

Последните ми въпроси са: Как правилно да моделирам проблема си? Правилно ли е, че съм на път да надархитектурирам кода си? Как би изглеждал правилно моят сервизен слой? Бих ли предпочел да имам ArticlePriceService, ArticleGroupPriceService и т.н.? Но кой ще свърже тези части и ще изчисли правилната цена? Това би ли напр. да бъде отговорност на OrderItemService, която има метод "GetPrice"? Но отново orderItemService ще трябва да знае за другите услуги..

Моля, опитайте се да ми предоставите възможни решения по отношение на архитектурата и кой обект/слой какво прави.

Чувствайте се свободни да ми задавате допълнителни въпроси, ако имате нужда от повече информация!


person bberger    schedule 13.06.2013    source източник
comment
Ако можехте да намалите размера на вашия въпрос до половината или дори една трета от първоначалния ви въпрос, щяхте повече хора да прочетат това...   -  person oleksii    schedule 13.06.2013
comment
Отворен съм за предложения, но наистина не виждам как мога да го намаля, без да отнема важна информация относно въпросите ми :/   -  person bberger    schedule 13.06.2013


Отговори (2)


Вие представихте прост сценарий, за който самото хранилище може да е достатъчно.
Имате ли повече хранилища?
Очаквате ли вашето приложение да се развива и да използвате повече хранилища?

Наличието на сервизен слой, който абстрахира слоя с данни, се препоръчва и се използва от повечето приложения/примери, които съм виждал, и режийните разходи не са толкова значителни.

Една от причините за използване на услуги може да изскочи, когато искате да извлечете данни от няколко различни хранилища и след това да извършите някакъв вид агрегиране/манипулиране на данните.
Слоят на услугата тогава ще осигури логиката за манипулиране, докато потребителят на услугата няма да трябва да се занимава с няколко различни хранилища.
Трябва също да помислите за ситуации, в които може да искате да промените повече от един обект в една транзакция (Което означава - повече от едно хранилище) и запазване на промените в DB само когато всички действия за актуализиране са успешни.
Тази ситуация трябва да предполага използването на Модел на работна единица и вероятно ще приключи използването на слой на услугата, за да даде възможност за правилно тестване на единици.

person Liel    schedule 13.06.2013
comment
Е, в моя сценарий самата цена се изчислява от различни източници: тя трябва да вземе предвид ArticlePrices, ArticleGroupPrices и CustomerPrices - които идват от 3 различни хранилища. Той също така трябва да вземе предвид от коя ценова листа идват всички те и трябва да знае коя ценова листа има предимство пред другата. Част от моите притеснения е: моята PriceService ще трябва да знае за всички тези фактори и как те влияят на всеки друго.. Също така: тогава ще имам ли OrderPriceService, която знае за ItemPriceService, която знае за (Article)PriceService? - person bberger; 13.06.2013
comment
Трудно е да ви кажа как да проектирате вашата система. Много обичайно е хранилището САМО да извлича данни и сервизен слой, който изпълнява цялата логика. Освен това, както писах, трябва да помислите за възможни сценарии за АКТУАЛИЗИРАНЕ, а не само за сценарии за извличане на данни. - person Liel; 13.06.2013
comment
Не виждам нищо лошо в кода ви на PriceService. Защо смятате, че е грешно? - person Liel; 13.06.2013
comment
Нещото при PriceService, което не харесвам, е, че трябва да знае за изчисляването на цената за множество цели. В крайна сметка бих имал методи за неща като ..ByArticle, ByOrder, ByGroup и т.н.. Предпочитам да имам някакъв метод като Order.GetPrice(), който прави нещо като Sum(Articles.GetPrice(), DeliveryOptions.GetPrice()) и т.н. - person bberger; 20.06.2013

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

За мен изглежда вашата услуга трябва да се нарича "ShopService" (или нещо еквивалентно). Тогава вашият метод GetByArticle трябва да се казва GetPriceByArticle.

Идеята за промяна на името на услугата за нещо по-голямо от цената би била по-смислена и би адресирала и други проблеми (като вашата OrderPriceService, за която се чудите).

Може би можете да се запитате "Какво е името на моята страница или прозорец, който взаимодейства с тази услуга?" Само един ли е или повече? Ако са повече, какво е общото между тях? Това може да ви помогне да намерите добро име за вашата услуга и следователно различни методи за придобиване на това, от което всеки има нужда.

Кажи ми повече. Ще се моля да помогна.

person fradiv    schedule 03.02.2015