Принципы SOLID
Есть задача. Надо сделать что-то типа устройства сканера для супермаркета, который отмечает товары. Каждый товар имеет свое обозначение и цену за единицу. Также, на товар может быть акция - определенное количество приобретается за фиксированную цену (применяется раз). Наше устройство должно уметь назначать новую цену для продуктов, считывать продукт "на чек", и выдавать конечную стоимость на этом "чеке".
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 - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
Смотрите также:
Комментарии
Чтобы упростить задачу, перегружу немного дополню вопрос своими соображениями (как я понял аспекты SOLID)
Насколько я понял, L - просто концепция, при которой в программе экземпляр класса можно заменить экземпляром его дочернего класса, без ущерба программе; O - по сути, не должно быть классов, выполняющих "избыточную" работу, то есть, если мне надо выполнять некоторые базовые действия, и в некоторых случаях, кроме базовых выполнять еще дополнительные, то надо создать базовый класс с базовыми действиями, а дополнительные прописывать уже в классе, наследующем базовый; I - если методы класса можно разделить на тематические группы, то лучше всего каждую группу делать отдельным интерфейсом.
Немного непонятно, что требуется в D. Если у нас класс A включает в себя экземпляр класса B, то соответственно этому принципу, лучше взять какой-то интерфейс I, наследовать класс B от этого интерфейса, и переменную в классе A тоже стоит сделать в виде интерфейса I?
Принцип S говорит "не надо нагружать класс методами, выноси функционал в отдельные классы", однако, с этим можно дойти до того, что на каждый метод надо будет делать отдельный класс. Или тут надо выделять определенные категории, как в принципе I?
Если не говорить о конкретной задаче выше, вы можете подтвердить или опровергнуть мои соображения?
lentinant, мое понимание SOLID:
S - один класс на одну задачу, сложные задачи разбиваются до подзадач и, соответственно, получаем один класс на большую задачу и по классу на подзадачу. Условно можно выразить так "если класс занимается выпеканием хлеба, то он не должен заниматься его доставкой". Важно не увлекаться дроблением сверх меры на этапе проектирования - если видишь что класс начинает разбухать и обростать группами не связанных методов, то самое время использовать этот принцип.
O - избегать изменения контрактов уже стабилизировавшегося кода. Применяется когда код уже может где-то использоваться как зависимость. По сути это требование обратной совместимости - расширять функционал можно и нужно, но старый код, зависящий от твоего, должен работать даже после превращения калькулятора в подводную лодку с вертикальным взлетом.
L - требование, согласно которому экземпляры родительских объектов должно быть можно заменить экземплярами дочерних объектов, не нарушая целостности программы. По сути это критерий, по которому можно определить и устранить избыточное или неправильное наследование, заменив его наследованием обоих объектов от общего родителя или агрегацией или хоть чертом лысым - сам принцип ничего не говорит о том, как именно его надо выпонять. Есть небольшой нюанс - применяется этот принцип только к экземплярам классов, а не к ссылкам или самому дереву наследования - если, например, где-то есть ссылка на A, который родительский класс для B, но по факту там используются только экземпляры классов B, C и D, то "nothing to do here".
I - аналог S для интерфейсов. Его главная идея в том, что не стоит перегружать класс, реализующий интерфейс, лишними методами.
D - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
prog:
Еще хочу сказать, что слепое следование всем принципам ни к чему хорошему не приводит - важно понимать грань, за которой начинается ад и содомия и вовремя остановиться.
Вот самое важное замечание.
Вот самое важное замечание.
Ну, куратор на работе сказал, что задание надо делать с учетом этих принципов, ибо это необходимый опыт для командной разработки. Вот я и пытаюсь разобраться.
prog:
S - один класс на одну задачу, сложные задачи разбиваются до подзадач и, соответственно, получаем один класс на большую задачу и по классу на подзадачу. Условно можно выразить так "если класс занимается выпеканием хлеба, то он не должен заниматься его доставкой". Важно не увлекаться дроблением сверх меры на этапе проектирования - если видишь что класс начинает разбухать и обрастать группами не связанных методов, то самое время использовать этот принцип.
Так суть принципа - уменьшить количество методов в классе, или инкапсулировать различные обязанности? Если в пекарне сделали отдельно отдел доставки и отдел выпечки, всё равно придется обращаться и к тому, и к тому (если я в примере в классе PointOfSaleTerminal вынесу в отдельный класс методы Scan и Reset, так как они отвечают за менеджмент списка покупок, всё равно надо будет оставить методы, которые будут вызывать Scan и Reset с нашего подкласса).
prog:
D - суть в том, чтобы разорвать связи между объектами, находящимися на разных уровнях абстракции. Применяется в обоих направлениях - и для менее абстрактных объектов, включенных в более абстрактный и наоборот, более того - применяется не только при прямом включении, но и к любым другим ссылкам. Предположим нам нужно составить програмную модель кирпичной стены, класс стены не должен ничего знать о конкретных реализациях кирпичей и работать с любыми кирпичами, какие ему дадут, а кирпичи не должны напрямую зависеть от конкретных реализаций стены и быть пригодны к использованию в любой стене (или другой конструкции, если модель подразумевает не только стены).
Грубо говоря, использовать в классах не прямые типы в ссылочных переменных, а интерфейсы? Чтобы туда можно было всунуть экземпляр любого класса, реализующего интерфейс?
Ладно, куратор потом всё равно сказал, что не страшно, если не всё будет подчиняться этим принципам. Правда, теперь непонятно, зачем я два дня сидел практически без дела.
Возможность добавлять комментарии была ограничена
CollectableItemData.cs
[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]