Советы по дизайну для избежания изменений в нескольких классах - PullRequest
2 голосов
/ 27 апреля 2010

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

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

У меня есть книга Head First Design Patterns, и я попытался найти там идеи, но ближе всего я получил образец декоратора, в котором есть пример, в котором starbuzz устанавливает цены на кофе сначала в зависимости от добавленных приправ, а затем в упражнение, добавив параметр размера (Tall, Grande, Venti). Но это, похоже, не помогло, потому что добавление этого параметра все же добавляло сложность условия if во многих местах (и это упражнение, которое они не объяснили далее).

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

Здесь ниже код. В основном он рассчитывает цену для проекта, который имеет задачи «Написание» и «Анализ» с параметром размера и различными моделями ценообразования. В дальнейшем будут и другие параметры, такие как «Насколько новый продукт?» (Новый, 1-5 лет, 6-10 лет) и т. Д. Любой совет о лучшем дизайне будет высоко оценен, будь то «шаблон дизайна» или просто хорошие объектно-ориентированные принципы, которые сделают его устойчивым к изменениям (например, добавление другого размера или изменение одного из значений размера, которые нужно изменить только в одном месте, а не в нескольких предложениях if):

public class Project
{
    private readonly int _numberOfProducts;
    protected Size _size;
    public Task Analysis { get; set; }
    public Task Writing { get; set; }

    public Project(int numberOfProducts)
    {
        _numberOfProducts = numberOfProducts;
        _size = GetSize();
        Analysis = new AnalysisTask(numberOfProducts, _size);
        Writing = new WritingTask(numberOfProducts, _size);

    }

    private Size GetSize()
    {
        if (_numberOfProducts <= 2)
            return Size.small;
        if (_numberOfProducts <= 8)
            return Size.medium;
        return Size.large;
    }
    public double GetPrice()
    {
        return Analysis.GetPrice() + Writing.GetPrice();
    }
}

public abstract class Task
{
    protected readonly int _numberOfProducts;
    protected Size _size;
    protected double _pricePerHour;
    protected Dictionary<Size, int> _hours;
    public abstract int TotalHours { get; }

    public double Price { get; set; }

    protected Task(int numberOfProducts, Size size)
    {
        _numberOfProducts = numberOfProducts;
        _size = size;
    }

    public double GetPrice()
    {
        return _pricePerHour * TotalHours;
    }
}

public class AnalysisTask : Task
{
    public AnalysisTask(int numberOfProducts, Size size)
        : base(numberOfProducts, size)
    {
        _pricePerHour = 850;
        _hours = new Dictionary<Size, int>() { { Size.small, 56 }, { Size.medium, 104 }, { Size.large, 200 } };
    }

    public override int TotalHours
    {
        get { return _hours[_size]; }
    }
}

public class WritingTask : Task
{
    public WritingTask(int numberOfProducts, Size size)
        : base(numberOfProducts, size)
    {
        _pricePerHour = 650;
        _hours = new Dictionary<Size, int>() { { Size.small, 125 }, { Size.medium, 100 }, { Size.large, 60 } };
    }

    public override int TotalHours
    {
        get
        {
            if (_size == Size.small)
                return _hours[_size] * _numberOfProducts;
            if (_size == Size.medium)
                return (_hours[Size.small] * 2) + (_hours[Size.medium] * (_numberOfProducts - 2));
            return (_hours[Size.small] * 2) + (_hours[Size.medium] * (8 - 2)) + (_hours[Size.large] * (_numberOfProducts - 8));
        }
    }
}

public enum Size
{
    small, medium, large
}

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        List<int> quantities = new List<int>();

        for (int i = 0; i < 100; i++)
        {
            quantities.Add(i);
        }
        comboBoxNumberOfProducts.DataSource = quantities;
    }

    private void comboBoxNumberOfProducts_SelectedIndexChanged(object sender, EventArgs e)
    {
        Project project = new Project((int)comboBoxNumberOfProducts.SelectedItem);
        labelPrice.Text = project.GetPrice().ToString();
        labelWriterHours.Text = project.Writing.TotalHours.ToString();
        labelAnalysisHours.Text = project.Analysis.TotalHours.ToString();
    }
}

В конце - простой текущий код вызова в событии изменения для выпадающего списка, который устанавливает размер ... (Кстати, мне не нравится тот факт, что мне нужно использовать несколько точек, чтобы добраться до TotalHours в конце и здесь, насколько я могу вспомнить, это нарушает «принцип наименьшего знания» или «закон деметры», так что вклад в это также будет оценен, но это не главный вопрос)

С уважением,

Андерс

Ответы [ 4 ]

3 голосов
/ 27 апреля 2010

Прежде всего, вы должны, на мой взгляд, переосмыслить свой дизайн. Проекты не выглядят так, и, как я посмотрел в вашем коде, у вас нет возможности добавить больше задач в проект. Также рассмотрите возможность разделения Проекта и способа подсчета приза. Что делать, если у вас есть разные методы расчета? Это также касается ответственности, скоро ваш проект может вырасти, и вам будет сложно разделить способ расчета цены и структуры проекта. Как правило, избегая «если» делается с помощью полиморфизма - может быть, вы хотели бы иметь разные типы проектов в зависимости от их параметров. Это может быть достигнуто с помощью метода Factory, который будет принимать аргументы, один раз выполнить «если» и затем создать некоторый подтип Project, который будет знать, как правильно рассчитать свой выигрыш. Если вы разделяете проект и расчеты, вместо этого рассмотрите использование шаблона стратегии для расчета выигрыша. Забота о законе Деметры здесь адекватна, потому что вы выставляете задачи. Попробуйте вместо этого использовать метод, который вернет итоговую цену и делегирует. Причина в том, что класс, в котором будет использоваться этот метод (проект или стратегия расчета), может решать, как его вычислять, он также может получать информацию из других задач. Вам придется изменить метод, если вы планируете добавить больше задач, возможно, использовать один метод с параметром string или enum, чтобы выбрать конкретную задачу для расчета приза. КСТАТИ. почему вы так часто используете подчеркивания?

1 голос
/ 29 апреля 2010

Если у вас есть такой оператор if ... else, основанный на свойствах класса, попробуйте изменить его, используя шаблон стратегии. Вы можете попробовать книгу под названием «Refactor to Patterns», которая является хорошей книгой по рефаторингу.

0 голосов
/ 27 апреля 2010

Он использует подчеркивания для переменных членов. Вы могли бы использовать «я». или это." вместо этого, но это так же ясно. Я полагаю, что это происходит от какого-то старого стандарта стиля Java? лично мне это очень нравится.

0 голосов
/ 27 апреля 2010

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

Предполагается один набор данных об использовании.

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

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

Редактировать: я хотел прокомментировать это ниже, но не хватило места:

расширяет глубину вашего xml, чтобы осмысленно представлять большие структуры данных (WritingTask и AnalysisTask), а также их составные части (свойства и методы). Методы часто могут быть определены набором правил. Вы можете маркировать свойства и правила, чтобы с ними можно было взаимодействовать независимо. Пример:

<task name="WritingTask">
<property name="numberofproducts" type="int"/>
<property name="Size" type="size">
    <property name="Price" type="decimal">
    <param name="priceperhour" value="650">
    </property>
<property name="hours" type="Dictionary">
    <param name="Size.small" value="125"/> 
    <param name="Size.medium" value="100"/>     
        <param name="Size.large" value="60"/>   
    </property>     
    <method name="TotalHours">
        <rule condition="_size == Size.Small">
    <return value="_hours[_size] * _numberofproducts"/>
        </rule>
        <rule condition="_size == Size.medium">
        <return value="(_hours[Size.small] * 2) + (_hours[Size.medium] * _numberOfProducts - 2))"/>
    </rule>
    <return value="(_hours[Size.small] * 2) + (_hours[Size.medium] * (8 - 2)) + (_hours[Size.large] * (_numberOfProducts - 8))"/> 
    </method> 
</task>

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...