Есть задача. Надо сделать что-то типа устройства сканера для супермаркета, который отмечает товары. Каждый товар имеет свое обозначение и цену за единицу. Также, на товар может быть акция - определенное количество приобретается за фиксированную цену (применяется раз). Наше устройство должно уметь назначать новую цену для продуктов, считывать продукт "на чек", и выдавать конечную стоимость на этом "чеке".
Вот моя реализация
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PricingLibrary
{
    //класс обычного продукта
    public class Product
    {
        //обозначение или название
        public string code { private set; get; }

        //цена за единицу продукта
        protected double pricePerUnit { private set; get; }

        //конструктор
        public Product (string code, double pricePerUnit)
        {
            this.code = code;
            this.pricePerUnit = pricePerUnit;
        }

        //метод для вычисления стоимости count единиц
        public virtual double CalculateCost(int count)
        {
            return pricePerUnit * count;
        }
    }

    //продукт по акции
    public class VolumeProduct : Product
    {
        //цена набора
        protected double pricePerVolume { private set; get; }

        //сколько единиц входит в набор
        protected int volumeSize { private set; get; }

        //конструктор
        public VolumeProduct(string code, double pricePerUnit, double pricePerVolume, int volumeSize):base(code, pricePerUnit)
        {
            this.pricePerVolume = pricePerVolume;
            this.volumeSize = volumeSize;
        }
      
        //метод для расчета стоимости с учетом акции
        public override double CalculateCost(int count)
        {
            if(count < volumeSize)
                   return base.CalculateCost(count);
            else
                return (count - volumeSize) * pricePerUnit + pricePerVolume;

        }
    }

    //информация о покупке определенного продукта
    class ProductCounter
    {
        //сколько единиц продукта было куплено
        private int count = 0;

        //непосредственно сам продукт
        private Product product;

        //конструктор
        public ProductCounter(Product product)
        {
            this.product = product;
        }

        //покупка нового продукта
        public void AddProductUnit()
        {
            count++;
        }

        //расчет текущей стоимости всех единиц продукта
        public double CalculatePrice()
        {
            return product.CalculateCost(count);
        }
     
        //сброс счетчика
        public void Reset()
        {
            count = 0;
        }
    }

    //класс непосредственно устройства
    public class PointOfSaleTerminal
    {
        //словарь, хранящий инфу об обозначениях и соответствующих им продуктах
        private Dictionary<string, ProductCounter> allProducts = new Dictionary<string, ProductCounter>();

        //метод для установки цены за продукты
        public void SetPricing(params Product[] productList)
        {
            foreach(Product product in productList)
            {
                allProducts.Add(product.code, new ProductCounter(product));
            }
        }

        //сканирование товара
        public void Scan(string productName)
        {
            allProducts[productName].AddProductUnit();
        }

        //расчет общей стоимости покупки
        public double CalculateTotal()
        {
            double value = 0;

            ProductCounter[] productList = allProducts.Select(item => item.Value).ToArray();
            foreach(ProductCounter product in productList)
            {
                value += product.CalculatePrice();
            }

            return value;
        }

        //сброс, для возможности обработки новой покупки
        public void Reset()
        {
            ProductCounter[] productList = allProducts.Select(item => item.Value).ToArray();
            foreach (ProductCounter product in productList)
            {
                product.Reset();
            }
        }
    }
}
Вопрос следующий - что надо сделать с этим кодом, чтобы он соответствовал стандартам SOLID? OCP и LSP, вроде как, соблюдены, да и то не факт. Мне нужны хотя бы наводки.

Принятый ответ

lentinant, мое понимание SOLID:
S - один класс на одну задачу, сложные задачи разбиваются до подзадач и, соответственно, получаем один класс на большую задачу и по классу на подзадачу. Условно можно выразить так "если класс занимается выпеканием хлеба, то он не должен заниматься его доставкой". Важно не увлекаться дроблением сверх меры на этапе проектирования - если видишь что класс начинает разбухать и обростать группами не связанных методов, то самое время использовать этот принцип.
O - избегать изменения контрактов уже стабилизировавшегося кода. Применяется когда код уже может где-то использоваться как зависимость. По сути это требование обратной совместимости - расширять функционал можно и нужно, но старый код, зависящий от твоего, должен работать даже после превращения калькулятора в подводную лодку с вертикальным взлетом.
L - требование, согласно которому экземпляры родительских объектов должно быть можно заменить экземплярами дочерних объектов, не нарушая целостности программы. По сути это критерий, по которому можно определить и устранить избыточное или неправильное наследование, заменив его наследованием обоих объектов от общего родителя или агрегацией или хоть чертом лысым - сам принцип ничего не говорит о том, как именно его надо выпонять. Есть небольшой нюанс - применяется этот принцип только к экземплярам классов, а не к ссылкам или самому дереву наследования - если, например, где-то есть ссылка на A, который родительский класс для B, но по факту там используются только экземпляры классов B, C и D, то "nothing to do here".
I - аналог S для интерфейсов. Его главная идея в том, что не стоит перегружать класс, реализующий интерфейс, лишними методами.
D - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
`
ОЖИДАНИЕ РЕКЛАМЫ...
0
26
9 лет назад
Отредактирован lentinant
0
Чтобы упростить задачу, перегружу немного дополню вопрос своими соображениями (как я понял аспекты SOLID)
Насколько я понял, L - просто концепция, при которой в программе экземпляр класса можно заменить экземпляром его дочернего класса, без ущерба программе; O - по сути, не должно быть классов, выполняющих "избыточную" работу, то есть, если мне надо выполнять некоторые базовые действия, и в некоторых случаях, кроме базовых выполнять еще дополнительные, то надо создать базовый класс с базовыми действиями, а дополнительные прописывать уже в классе, наследующем базовый; I - если методы класса можно разделить на тематические группы, то лучше всего каждую группу делать отдельным интерфейсом.
Немного непонятно, что требуется в D. Если у нас класс A включает в себя экземпляр класса B, то соответственно этому принципу, лучше взять какой-то интерфейс I, наследовать класс B от этого интерфейса, и переменную в классе A тоже стоит сделать в виде интерфейса I?
Принцип S говорит "не надо нагружать класс методами, выноси функционал в отдельные классы", однако, с этим можно дойти до того, что на каждый метод надо будет делать отдельный класс. Или тут надо выделять определенные категории, как в принципе I?
Если не говорить о конкретной задаче выше, вы можете подтвердить или опровергнуть мои соображения?
0
24
9 лет назад
Отредактирован prog
0
lentinant, мое понимание SOLID:
S - один класс на одну задачу, сложные задачи разбиваются до подзадач и, соответственно, получаем один класс на большую задачу и по классу на подзадачу. Условно можно выразить так "если класс занимается выпеканием хлеба, то он не должен заниматься его доставкой". Важно не увлекаться дроблением сверх меры на этапе проектирования - если видишь что класс начинает разбухать и обростать группами не связанных методов, то самое время использовать этот принцип.
O - избегать изменения контрактов уже стабилизировавшегося кода. Применяется когда код уже может где-то использоваться как зависимость. По сути это требование обратной совместимости - расширять функционал можно и нужно, но старый код, зависящий от твоего, должен работать даже после превращения калькулятора в подводную лодку с вертикальным взлетом.
L - требование, согласно которому экземпляры родительских объектов должно быть можно заменить экземплярами дочерних объектов, не нарушая целостности программы. По сути это критерий, по которому можно определить и устранить избыточное или неправильное наследование, заменив его наследованием обоих объектов от общего родителя или агрегацией или хоть чертом лысым - сам принцип ничего не говорит о том, как именно его надо выпонять. Есть небольшой нюанс - применяется этот принцип только к экземплярам классов, а не к ссылкам или самому дереву наследования - если, например, где-то есть ссылка на A, который родительский класс для B, но по факту там используются только экземпляры классов B, C и D, то "nothing to do here".
I - аналог S для интерфейсов. Его главная идея в том, что не стоит перегружать класс, реализующий интерфейс, лишними методами.
D - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
Принятый ответ
0
30
9 лет назад
0
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
Вот самое важное замечание.
0
26
9 лет назад
Отредактирован lentinant
0
Вот самое важное замечание.
Ну, куратор на работе сказал, что задание надо делать с учетом этих принципов, ибо это необходимый опыт для командной разработки. Вот я и пытаюсь разобраться.
S - один класс на одну задачу, сложные задачи разбиваются до подзадач и, соответственно, получаем один класс на большую задачу и по классу на подзадачу. Условно можно выразить так "если класс занимается выпеканием хлеба, то он не должен заниматься его доставкой". Важно не увлекаться дроблением сверх меры на этапе проектирования - если видишь что класс начинает разбухать и обрастать группами не связанных методов, то самое время использовать этот принцип.
Так суть принципа - уменьшить количество методов в классе, или инкапсулировать различные обязанности? Если в пекарне сделали отдельно отдел доставки и отдел выпечки, всё равно придется обращаться и к тому, и к тому (если я в примере в классе PointOfSaleTerminal вынесу в отдельный класс методы Scan и Reset, так как они отвечают за менеджмент списка покупок, всё равно надо будет оставить методы, которые будут вызывать Scan и Reset с нашего подкласса).
prog:
D - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Грубо говоря, использовать в классах не прямые типы в ссылочных переменных, а интерфейсы? Чтобы туда можно было всунуть экземпляр любого класса, реализующего интерфейс?

Ладно, куратор потом всё равно сказал, что не страшно, если не всё будет подчиняться этим принципам. Правда, теперь непонятно, зачем я два дня сидел практически без дела.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.