Лучший шаблон проектирования для рефакторинга класса, который выполняет вычисления на основе многих параметров

#c# #design-patterns

#c# #шаблоны проектирования

Вопрос:

Я рефакторинг набора классов, как показано ниже, который выполняет некоторые расчеты цен. вычисления выполняются на основе многих параметров. Код :

 public interface IParcel {
    int SourceCode { get; set; }
    int DestinationCode { get; set; }
    int weight{get;set;}
    decimal CalculatePrice();
}

public abstract class GeneralParcel : IParcel {
    //implementation of inteface properties
    //these properties set with in SourceCode amp; DestinationCode 
    //and are used in CalculatePrice() inside classes that inherit from GeneralParcel 
    protected SourceProvinceCode{get; protected set;}
    protected DestinationProvinceCode{get;protected set;}

    //private variables we need for calculations
    private static ReadOnlyDictionary<int, List<int>> _States_neighboureness;
    private static ReadOnlyCollection<City> _Citieslist;
    private static ReadOnlyCollection<Province> _Provinceslist;

    protected ReadOnlyCollection<City> Citieslist {get { return _Citieslist; }}

    protected ReadOnlyCollection<Province> ProvincesList {get { return _Provinceslist; }}

    protected ReadOnlyDictionary<int, List<int>> StatesNeighboureness {get {return _States_neighboureness; }}

    //constructor code that initializes the static variables

    //implementation is in concrete classes
    public abstract decimal CalculatePrice();
}

public ExpressParcel : GeneralParcel {
    public decimal CalculatePrice() {
        //use of those three static variables in calculations
        // plus other properties amp; parameters
        // for calculating prices 
    }
}


public SpecialParcel : GeneralParcel {
    public decimal CalculatePrice() {
        //use of those three static variables in calculations
        // plus other properties amp; parameters
        // for calculating prices 
    }
}
  

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

мой вопрос заключается в том, что эти три статических свойства на самом деле не являются частью объекта parcel, они нужны только для расчета цены, поэтому какой шаблон проектирования или обертывание (рефакторинг) предлагается?

Необходимо ли иметь другой интерфейс, как показано ниже (и затем обернуть эти статические свойства внутри него?, даже сделать статический этот класс, потому что это в основном только некоторые вычисления), тогда как подключить его к IParcel? При этом, как реализовать CalculatePrice() в SpecialParcel amp; ExpressParcel classes ?

 public interface IPriceCalculator {
    decimal CalculatePrice();
}
  

РЕДАКТИРОВАТЬ: вышесказанное было лишь общей картиной всей системы, есть и другие соображения, которые мы обсуждаем в комментариях, и я пишу их здесь снова для прояснения ситуации.

существует BulkDiscount для всех типов пакетов. массовая отправка происходит, когда клиент отправляет более 10 посылок (или любое пороговое значение), также предоставляется специальная скидка, когда один клиент отправляет более 10 посылок уникальному адресату (есть только один получатель). теперь этот тип скидок управляется в каждом типе посылки CalculatePrice() . даже есть скидки на жалюзи для посылок весом менее 7 кг.

также сейчас их 3 parceltype , я показываю здесь только 2 из них. но в будущем нам нужно добавить другой тип (поддержка TNT и DHL). у каждого типа есть много сервисов, которые клиент может выбрать и оплатить за это. например, sms service или email service amp; и так далее.

Комментарии:

1. Подумайте также, codereview.stackexchange.com .

2. да, спасибо, добавление this there тоже

Ответ №1:

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

Если вы действительно хотите пойти по этому пути, то вам понадобятся две реализации IParcelPriceCalculator (или как вы это называете) и абстрактный фабричный метод GeneralParcel для создания ExpressParcelPriceCalculator SpecialParcelPriceCalculator классов concrete или. Что лично я бы счел излишним, не в последнюю очередь потому, что этот код в любом случае будет тесно связан с каждой GeneralParcel реализацией.

Однако я бы подумал о создании статических коллекций City и Province общедоступных статических свойств City и Province соответственно. Это просто аккуратнее, и именно там я ожидал бы их найти, если бы поддерживал код. StatesNeighbourliness вероятно, следует перейти в область, или он может даже оправдать свой собственный класс.

Комментарии:

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

2. Ах, в этом случае вам может понадобиться Carrier класс, который реализует IPriceCalculator метод по-разному для каждого носителя. Так что, возможно, абстрактная фабрика, принимающая Carrier параметр, может быть правильным решением. Время поработать над некоторыми модульными тестами и посмотреть, какой из них лучше всего подходит!

3. @John итак, давайте напишем и протестируем эти три метода ( static ListOfThings , SpecialParcelPriceCalculator amp; carrier ) . 1 для which feels best!

4. Я могу представить сценарии, в которых вы можете захотеть рассчитать разные цены для разных сценариев (и не только на основе типа участка), поэтому привязка расчета цены к участку является плохим дизайнерским решением. Как насчет, например, FourthOfJulySalePriceCalculator или SpecialBulkDiscountPriceCalculator?

5. @ahmad Я думаю, вам нужен IPriceCalculator, который принимает коллекцию посылок. Вам также следует подумать, может ли вам потребоваться применить несколько правил для вычисления заданной цены. Например, я мог видеть комбинацию скидки на массовую продажу и продажи. Это еще больше изменит ваш дизайн.

Ответ №2:

Способ, которым вы вычисляете цену для данного участка, — это ответственность, которая не должна принадлежать объекту данных.

Учитывая то, что вы мне сказали, вот как я бы реализовал, чтобы попытаться учесть будущие соображения:

 public interface IParcel {
    int SourceCode { get; set; }
    int DesinationCode { get; set; }
    int Weight { get; set; }
}

public class PricingCondition {
    //some conditions that you care about for calculations, maybe the amount of bulk or the date of the sale
    //might possibly be just an enum depending on complexity
}

public static class PricingConditions {
    public static readonly PricingCondition FourthOfJulyPricingCondition = new PricingCondition();
    public static readonly PricingCondition BulkOrderPricingCondition = new PricingCondition();
    //these could alternatively come from a database depending on your requirements
}

public interface IParcelBasePriceLookupService {
    decimal GetBasePrice(IParcel parcel);
    //probably some sort of caching
}

public class ParcelPriceCalculator {
    IParcelBasePriceLookupService _basePriceLookupService;

    decimal CalculatePrice(IParcel parcel, IEnumerable<PricingCondition> pricingConditions = new List<PricingCondition>()) {
        //do some stuff
    }
    decimal CalculatePrice(IEnumerable<IParcel> parcels, IEnumerable<PricingCondition> pricingConditions = new List<PricingCondition>()) {
        //do some stuff, probably a loop calling the first method
    }
}
  

Комментарии:

1. пожалуйста, прочтите эту ссылку, наконец, я собираюсь смешать ваш код и эту ссылку

Ответ №3:

Это IPriceCalculator было бы лучшей практикой для принципа единой ответственности.

Но изменение подписи метода на decimal CalculatePrice(IParcel parcel); метод вызывает метод CalculatePrice() IParcel, чтобы получить базовую цену для каждой посылки.

Комментарии:

1. это что-то вроде шаблона декоратора? Я имею в виду, что для каждого типа участка (и сейчас в проекте их 4 типа) нам нужны интерфейсы в соответствии с каждым типом IParcel. ExpressPriceCalculator и так далее. в каждом из них или в базовом абстрактном находятся эти три статических свойства.

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

3. к сожалению, у каждого типа есть свои собственные свойства и правила вычисления! например, SpecialParcel большинство доставляет за 24 часа, но в express такого ограничения нет, поэтому формула ценообразования сильно отличается

4. тогда я бы рекомендовал обратиться к ответу Джона или Джереми.

Ответ №4:

Совет, который я бы предложил, будет в некоторой степени зависеть от того, как вы собираетесь генерировать и использовать полиморфы участков. Я имею в виду, что мы не можем видеть, какие критерии используются для определения того, является ли что-то «Экспресс» или «Особенным», и связаны ли эти критерии со свойствами самого участка или с каким-либо внешним фактором.

Сказав это, я думаю, что ваша интуиция хороша в том, чтобы отделить расчет цены от самого объекта посылки. Как указал kmkemp, посылка не должна выяснять, как рассчитать цену посылки в зависимости от типа посылки. Посылка — это объект типа передачи данных / POCO, по крайней мере, как указано в вашем указании веса, источника и т. Д.

Однако я не совсем понимаю, почему вы используете эти интерфейсы. Не поймите меня неправильно — интерфейс хорош для развязки и тестируемости, но зачем нужен интерфейс parcel в дополнение к абстрактному базовому классу с абстрактным методом. Лично, исходя только из имеющейся у меня информации, я бы сделал это:

 public class Parcel
{
    int SourceCode { get; set; }
    int DestinationCode { get; set; }
    int weight { get; set; }
}

public abstract class GeneralCalculator
{
    //Statics go here, or you can inject them as instance variables
    //and they make sense here, since this is presumably data for price calculation
    protected static ReadOnlyDictionary<int, List<int>> _States_neighboureness;
    protected static ReadOnlyCollection<City> _Citieslist;
    protected static ReadOnlyCollection<Province> _Provinceslist;
    //.... etc

    public abstract Decimal CalculatePrice(Parcel parcel);
}

public class ExpressCalculator : GeneralCalculator
{
    public override decimal CalculatePrice(Parcel parcel)
    {
        return 0.0M;
    }
}

public class SpecialCalculator : GeneralCalculator
{
    public override decimal CalculatePrice(Parcel parcel)
    {
        return 0.0M;
    }
}
  

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

Но, как бы вы его ни модифицировали, я бы определенно проголосовал за вашу мысль о том, чтобы отделить определение участка от схемы расчета его цены. Хранение и обработка данных — это отдельные проблемы. Я бы также не стал голосовать за статический класс где-нибудь, содержащий глобальные настройки, но это мой личный вкус — такие вещи слишком легко приобретают сеттер и становятся глобальной переменной в будущем.

Комментарии:

1. на самом деле именно клиент говорит, какой тип посылки он / она хочет использовать. Кстати, у каждого типа посылки есть еще несколько свойств и сервисов, о которых для простоты я ничего не говорю и не пишу. например, у SpecialParcel есть опция, которая, если клиент хочет, может отправлять sms при доставке (даже при доставке каждого узла обмена) или получать электронное письмо и многое другое, что зависит от каждого типа. тогда interface IParcel правило заключается в том, чтобы очистить, что между этими типами есть только некоторые базовые свойства.

2. Что ж, если у пакета есть веская причина быть полиморфным, альтернативной концепцией дизайна было бы иметь один калькулятор цен со статическим материалом и выставлять все, что используется калькулятором цен, в абстрактном базовом классе parcel. Затем переопределите вещи, которые отличаются между участками в классах DTO. Например, увеличьте свойство SMS до базового класса и верните ему значение false , а затем переопределите в special и верните значение true . Тогда калькулятор ценообразования будет использовать это свойство (если применимо) и не будет заботиться о том, какой тип посылки его предоставлял.

Ответ №5:

Как вы говорите, эти статические свойства на самом деле не являются частью GeneralParcel класса. Переместите их в статический класс «ListsOfThings».

Затем вы можете использовать код, который ссылается на ListsOfThings.ProvincesList и т.д.

Комментарии:

1. Я думаю об этом, но как насчет обслуживания кода? о чем IPriceCalculator ?

2. Почему может возникнуть проблема с обслуживанием? У вас есть списки вещей, хранящиеся в классе с именем «ListsOfThings». А статические свойства или что-либо еще статическое не имеет смысла в интерфейсе.

3. @woniв этом проекте много изменений, поэтому кажется (и только кажется), что это не очень хорошая практика. я чувствую, что это не лучшая практика.

4. Я не думаю, что вы меня понимаете. Список городов не будет меняться в зависимости от типа посылки, перевозчика или чего-либо еще. Это должно быть либо статическое свойство City класса, либо оно должно быть статическим свойством статического ListsOfUnrelatedThings класса.