Страницы

Поиск по вопросам

Показаны сообщения с ярлыком solid. Показать все сообщения
Показаны сообщения с ярлыком solid. Показать все сообщения

воскресенье, 8 марта 2020 г.

Экспорт модели в .xlsx соблюдая SOLID

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


В БД есть таблица результатов тестирования по русскому языку и математике (SubjectCode=2):

dbo.Result


Во второй таблице находятся просто сведения о школах:

dbo.School



Таких результатов бывает 40.000-70.000. Нужно на выходе получить для каждого тестируемого
вот такой отчет в pdf-файле:



Мое решение:


Создал Excel-шаблон;
Беру данные из базы и заношу их в этот xlsx-шаблон;
Сохраняю этот шаблон в pdf-файл;
И так для каждого ученика.




LearnerReport.cs

namespace so16092016.Models
{
    public class LearnerReport
    {
        public string SNS { get; set; } //Surname Name SecondName
        public string SchoolName { get; set; }
        public string ClassName { get; set; }
        public int TestResult5 { get; set; }
    }
}


Program.cs

using Excel = Microsoft.Office.Interop.Excel;

namespace so16092016
{
    class Program
    {
        static void Main(string[] args)
        {
            resultsEntities context = new resultsEntities();
            ResultsRepository resultsRepository = new ResultsRepository(context);
            var ma_results = resultsRepository.GetTList().Where(x => x.SubjectCode
== 2); //получить результаты по математике

            Excel.Application app = new Excel.Application();
            app.DisplayAlerts = false;
            Excel.Workbook book_template = app.Workbooks.Open(@"шаблон_отчета.xlsx");
            Excel._Worksheet sheet_template = book_template.Sheets["отчет"];

            foreach(var ob in ma_results)
            {
                //1. Создаем объкт LearnerReport из БД
                LearnerReport report = new LearnerReport
                {
                    SNS = $"{ob.surname} {ob.name} {ob.SecondName}",
                    SchoolName = ob.SchoolName,
                    ClassName = ob.ClassName,
                    TestResult5 = ob.TestResult5                     
                };

                //2. Экспорт объкта LearnerReport в шаблон xlsx
                sheet_template.Range["C4"].Value2 = report.SNS;
                sheet_template.Range["C5"].Value2 = report.SchoolName;
                sheet_template.Range["C6"].Value2 = report.ClassName;
                sheet_template.Range["C9"].Value2 = report.TestResult5;

                //3. Сохраняем полученный файл в .pdf на рабочем столе
                string file_name = $@"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\{report.SNS}.pdf";
                sheet_template.ExportAsFixedFormat(Excel.XlFixedFormatType.xlTypePDF,
file_name);
            }

            book_template.Close(0);
            book_template = null;
            app.Quit();
            app = null;
        }
    }
}


Необходимо: Приложение работает и дает необходимый результат. Но вы наверное видите,
что оно далеко от ООП (SOLID) и как результат очень трудно его "допиливать" и масштабировать.
Помогите правильно спроектировать данный механизм формирования подобных отчетов: 


логика экспорта модели в .xlsx должна быть у самой модели или
необходимо создать отдельный класс-менеджер для этого?
какие должны быть модели?
как правильно создавать модель-отчета на основе объектов БД? 
какой порождающий паттерн здесь больше подходит?

    


Ответы

Ответ 1



Конкретно в вашем случай нет необходимость, применять какие либо паттерны, т.к. у вас довольно простая программа и паттерны добавят лишь ненужную сложность (применять паттерны ради паттернов плохая практика). Паттерны сгодятся если вы пишите большие Enterprise приложения, где необходима гибкость и масштабируемость. Если я правильно понял, то вы хотите отрефакторить программу. 1) Из кода я не понял что вы используете для получения данных из БД, но посоветовал бы использовать какую нибудь ORM, например Entity Framework 6. Создать DbContext и использовать его в репозиториях ResultsRepository и SchoolRepository Все выборки засунуть в соответствующий репозиторий например class ResultsRepository { // возвращает все результаты public IEnumerable GetResults() // возвращает все результаты по заданому предмету. public IEnumerable GetResults(Subject subject) // возвращает все результаты для заданой школы. public IEnumerable GetResults(int idSchool) // возвращает все результаты для заданой школы по заданому предмету. public IEnumerable GetResults(int idSchool, Subject subject) } class SchoolRepository { // возвращает список всех школ. public IEnumerable GetSchools() } 2) Модель данных приблизительно выглядит так class Result { [Key] public Guid Id { get; set; } public string Surname { get; set; } public string Name { get; set; } public string SecondName { get; set; } [NotMapped] public string SNS => $"{Surname} {Name} {SecondName}"; public School School { get; set; } public string ClassName { get; set; } public Subject Subject { get; set; } public int TestResult5 { get; set; } } class School { [Key] public int Id { get; set; } public string Name { get; set; } } enum Subject { RussianLanguage = 1, Mathematics = 2 } 3) Можно создать отдельный класс для работы отчётами class LearnerReportManager { public Excel._Worksheet CreateReport(Result result) public SaveReport(Excel._Worksheet reportWorksheet, string fileName) } 4) Для формирования Excel отчёта луче используйте EPPlus или более крутой File Formats от Syncfusion (у них есть бесплатная версия). вы так не будите зависеть от установленного MS Office. 5) Если кол. формируемых отчетов за раз от 40 до 70 тыс., то было бы не плохо добавить многопоточное создание отчетов, это существенно уменьшит время создания отчётов.

пятница, 24 января 2020 г.

Принцип открытости/закрытости при динамическом определении типа файла

#c_sharp #шаблоны_проектирования #проектирование #инспекция_кода #solid


Есть код приложения в котором необходимо динамически определить тип файла (HTML или
TXT) по содержимому и в зависимости от полученного типа файла вызвать соответствующий
алгоритм обработки.

interface IFileType
{
    void Process();
}
class HtmlFile : IFileType
{
    public void Process()
    {
        Console.WriteLine("HTML");
    }
}
class TxtFile : IFileType
{
    public void Process()
    {
        Console.WriteLine("TXT");
    }
}
class FileTypeHandler
{
    public static IFileType Define(string fileContent)
    {
        var file = fileContent.IndexOf("


Ответы

Ответ 1



Я вижу следующие проблемы в вашем коде. Метод FileTypeHandler содержит логику определения, для всех форматов. На данном этапе это не страшно, т.к. их не много, но если их количество будет увеличиваться, метод станет запутанный и тяжел в расширении. Рекомендую, создать сущности, для каждого формата, и в них реализовывать определение. Это позволит избежать запутанности и облегчит добавление новых. Приведу код, как я бы в данной ситуации реализовал. В нем есть небольшие упрощения. Написал, на java, но думаю вы разберетесь. enum FormatTypes { TXT("txt", new TxtDetector()), HTML("html", new HtmlDetector()); public final String name; private final Detector detector; FormatTypes(String name, Detector detector) { this.name = name; this.detector = detector; } } interface Detector { boolean isCorrectType(String fileName, String content); } class TxtDetector implements Detector { } class HtmlDetector implements Detector { } class DetectorHandler { public static String getType(String fileName) { String content = //чтение содержимого for (FormatTypes types : FormatTypes.values()) if (types.detector.isCorrectType(fileName, content)) //просто возвращает название формата, //можно при необходимости в FormatTypes положить какую то логику return types.name; throw new IllegalArgumentException("type is not supported"); } }

четверг, 23 января 2020 г.

Трактовка принципа Открытости-Закрытости

#java #ооп #solid


Пример с дополнением интерфейса:  

class Playback {
    private Media current;

    public void play(Media media) {
        current.stop();
        current = media;
        current.play();
    }

    public void next() {
        Media media // получение следующей по какому либо алгоритму
        play(media);
    }

    public void prev() {
        Media media // получение предыдущей по какому либо алгоритму
        play(media);
    }
}


Пример с добавлением классов операций:  

class Playback {
    private Media current;

    public Media current() {
        return current;
    }

    public void play(Media media) {
        current.stop();
        current = media;
        current.play();
    }
}

abstract class PlayOperation {
    private Playback playback;    

    public void execute() {
        Media media = provide();
        playback.play(media);
    }

    protected abstract Media provide();
}

class NextOperation extends PlayOperation {
    public Media provide() {
        Media media // получение следующей по какому либо алгоритму
        return media;
    }
}

class PrevOperation extends PlayOperation {
    public Media provide() {
        Media media // получение предыдущей по какому либо алгоритму
        return media;
    }
}

class RandomOperation extends PlayOperation {
    private Random random;    

    public Media provide() {
        Media media // получение рандомной
        return media;
    }
}


Пример с новыми классами выглядит привлекательнее, а стоит ли оно того?
    


Ответы

Ответ 1



Пример с добавлением классов операций То что вы описали в примере, это почти шаблон проектирования "Посетитель" . NextOperation , PrevOperation , RandomOperation - посетители класса Playback . В данном случае этот шаблон вряд ли обоснован, так как он призван добавлять поведение иерархии классов, и он действительно при работе с иерархией играет на OCP (т.к. необходимость добавления поведения в дерево наследников с помощью добавления метода в базовый класс как-раз нарушит OCP). У вас же класс один Playback - OCP не будет нарушено в первом случае - Playback открыт для расширения наследованием (если конечно поменять private на protected), и закрыт для изменения (это значит, что переписывать его вам не требуется для доработки поведения в наследниках). Нельзя не увидеть минус второго подхода - вы серьёзно усложняете систему, размазывая простую логику на множество классов ( это Anti-SRP ). Поэтому я бы посоветовал оставить первый вариант, как минимум, когда система простая - так усложнять её нельзя, YAGNI против. Но если Playback становится базовым классом иерархии - использование посетителей будет уже обосновано, и следует добавить метод visit(PlayOperation operation), при чём возможность посещения Playback - не значит что метод next(), например, обязательно должен быть вынесен в класс-посетитель: в посетителей лучше выносить дополнительные операции, а основные - оставлять внутри класса.

вторник, 31 декабря 2019 г.

Может ли нарушаться принцип подстановки Лисков при использовании интерфейса/абстрактного класса?

#java #ооп #solid


На размышления меня натолкнула вот эта статья: 
http://blog.byndyu.ru/2009/10/blog-post_29.html
Приведу немного переработанный пример из нее:

public interface IList {

public void add(int e);

}

public class List implements IList{

@Override
public void add(int e) {
    // добавляет элемент
}

}

public class DoubleList implements IList{

@Override
public void add(int e) {
    // добавляет элемент
    // добавляет элемент
}

}


Метод add в классе DoubleList добавляет элемент дважды. 
В клиентском коде можно написать примерно так:

public void LSPTest(IList list){
int oldLen = list.getLength();

list.add(1);

int newLen = list.getLength();

if(newLen - oldLen == 1){
    // делать что то полезное
}
}


Очевидно, что поведение программы будет разным, в зависимости от того, получит функция
LSPTest объект класса List или DoubleList. Но разве это нарушает LSP? Ведь DoubleList
наследует не класс List, а интерфейс IList. Интерфейс не может задавать никаких предусловий
и постусловий (в данном случае точно не задает). И интерфейсы же для того и написаны,
что бы иметь разные реализации, иногда имеющие совсем мало общего. Я всегда считал,
что если бы, например, DoubleList был наследован от List, то выделение интерфейса и
опускание классов на один уровень - это как раз решение проблемы при нарушении LSP.
По-моему это называется факторизация. И к тому же, LSP говорит о том, что прогрмма
не должна меняться, если вместо объекта базового класса подставить объект производного.
Но как можно подставить что то вместо объекта базового класса, если базовый класс является
абстрактным? Или тем более интерфейсом? Вместо него нельзя ничего подставить, потому
что его просто нельзя создать.
    


Ответы

Ответ 1



Интерфейс не может задавать никаких предусловий и постусловий Формально вы правы. Объявление интерфейса само по себе не задает "материального" (назовем это так) контракта -- т.е. контракта, который может быть проверен на этапе компиляции или выполнения. Т.е. нет никаких средств, гарантирующих, что все классы, реализующие интерфейс, будут реализовывать его одинаково с т.з. LSP. Хитрость заключается в том, что при использовании интерфейсов мы всегда говорим о "нематериальном" контракте. Он выражается в названии методов и в комментариях. Это такой неформальный уговор среди разработчиков. В случае с IList из .NET это выглядит так*: // Adds an item to the list. The exact position in the list is // implementation-dependent, so while ArrayList may always insert // in the last available location, a SortedList most likely would not. // The return value is the position the new element was inserted in. int Add(Object value); Как видно из комментария, если класс-наследник будет добавлять в список сразу два элемента, он нарушит два пункта из комментария: что добавлять должен один элемент и что метод возвращает позицию добавленного элемента (а что возвращать в случае двух элементов?). В то же время, комментарии к методу ICollection.Add() ничего не говорят о том, что этот метод предполагает делать. И, как мы видим, это согласуется с тем, что, например, HashSet (реализующий ICollection) после двух вызовов метода Add() с одинаковыми аргументами будет содержать лишь один элемент. Это тонкий лед, т.к., повторюсь, нет средств для обеспечения выполнения обозначенного контракта -- это лежит целиком на совести программиста. Более того, интерфейс может и не продоставлять никаких комментариев и о его контракте придется как-то догадываться -- из документации, интернета, от коллег. Но тем не менее 99.9% разработчиков, увидев название интерфейса IList, будут ожидать, что метод Add() добавит в список один элемент. Если же вдруг этот метод будет добавлять два элемента, не добавлять ничего, или даже удалять, это очень удивит эти 99.9%. В этом, собственно, и заключается нарушение LSP. Если вы пока не очень понимаете LSP, то забудьте на время про интерфейсы и посмотрите на примеры использования наследования классов. Например, на классическую проблему прямоугольника и квадрата. *желающим попудрить себе мозг читать ниже. Метод IList.Add(), как мы уже видели, явно в комментарии обозначает свой контракт. А что насчет IList? Как видно, он наследуются от ICollection, а это значит, что на метод Add() не налагается ограничений. Т.о. класс DoubleList : IList формально уже не будет нарушать LSP. Но бьюсь об заклад, ваши коллеги не будут рады такой реализации :).

Ответ 2



Именно этот вопрос я довольно детально рассматривал в статье "Наследование интерфейсов и контракты". Продублирую выводы, сделанные в моей статье: разные виды коллекций в .NET (для Java нужно проводить отдельные исследования) обладают разными постусловиями. Постусловие интерфейса IList более строгое: метод Add должен добавлять элемент, причем только один. Постусловие же базовой коллекции ICollection - слабее: метод Add не гарантирует, что элемент будет вообще добавлен, поскольку есть такие типы, как "наборы" (HashSet и другие), в которых не должно быть дубликатов и метод Add в этом случае ничего не будет делать. В этом случае, если наша коллекция реализует IList, то метод Add этой коллекции должен добавить элемент, причем только один. Если же наша коллекция реализует лишь ICollection, то метод Add вполне может и не добавлять элементы. Интерфейс не может задавать никаких предусловий и постусловий В общем случае - это не так. В языке может не быть инструментов для формального определения предусловий/постусловий для интерфейсов или абстрактных классов, но интерфейс ими обладает. Например, в .NET можно использовать библиотеку Code Contracts, которая позволяет задать контракты интерфейсов. При этом есть аннотация стандартной библиотеки, в которой можно посмотреть, какие контракты у стандартных интерфейсов. Вот, например, контракт метода ICollection.Add: public void Add(T item) { //Contract.Requires(!@this.IsReadOnly); // The Ensures below is tricky for wrappers. Needs quantified invariant in wrapper to prove wrapper correct // Forall(value => !m_backing.Contains(value) || this.Contains(value) ) //Contract.Ensures(this.Contains(item)); Contract.Ensures(this.Count >= Contract.OldValue(this.Count)); throw new global::System.NotImplementedException(); } Есть популярное мнение, что реализация интерфейса может делать все, что угодно, но в этом случае у клиента этого интерфейса пропадает возможность понять, а что же от этого зверя ожидать. Принцип LSP вообще невозможен без контрактов. Контракт определяет ожидаемое поведение, а нарушение LSP как раз и говорит о нарушении этого самого поведения. Тут можно провести аналогию с багами: помимо явных крэшей, мы не можем говорить, является ли поведение ошибкой или нет, если мы не знаем, какое поведение является ожидаемым.

пятница, 27 декабря 2019 г.

Противоречит ли реализация нескольких интерфейсов одним классом принципам SOLID

#шаблоны_проектирования #интерфейс #solid


Вопрос немного философский. Для начала 1 и 4 принципы SOLID из wiki:


  Существует лишь одна причина, приводящая к изменению класса.


и


  Много интерфейсов, специально предназначенных для клиентов, лучше, чем один интерфейс
общего назначения.


Иначе говоря - один класс должен выполнять свою узко-направленную задачу и никакие
другие.  Тоже и интерфейс - лучше разделять один общий интерфейс(аналог класса "помойки")
на составные, содержащие обьявления полей и методов необходимых для выполнения своей
конкретной задачи, причем только одной. Из 1+4 принципов можно сделать вывод, что один
класс должен реализовывать 1 интерфейс что бы не противоречить 1 принципу. Если же
1 класс реализует несколько интерфейсов, то следовательно обязан реализовать выполнение
нескольких задач что никак не вяжется с 1 принципом. 

Есть ли здесь противоречие, или я неправильно понял эти принципы (если да, то прошу
обьяснить где именно)? 
    


Ответы

Ответ 1



Если же 1 класс реализует несколько интерфейсов, то следовательно обязан реализовать выполнение нескольких задач что никак не вяжется с 1 принципом Считаю, это не следует воспринимать с фанатизмом. Во-первых, как сказал @АлексейШиманский, Если у интерфейса задача "чтение", то он и должен "читать". А у класса задача - работа с json строками, например. Во-вторых, классы, которые взаимозависимы друг от друга, не могут быть разделены по одной простой причине, что вы не сможете внедрить один в другой и наоборот одновременно. Пример: Есть интерфейсы: public interface IAuthInfo { bool IsAuthorized { get; } IUser CurrentUser { get; } } public interface IAuthorizer { Task LogUserInAsync(string email, SecureString password); } И их реализация: public class AuthService : IAuthorizer, IAuthInfo { public bool IsAuthorized => CurrentUser != null; public IUser CurrentUser { get; private set; } public async Task LogUserInAsync(string email, SecureString password) { // some actions to set CurrentUser property } } По факту IAuthInfo отвечает за текущую инфу об авторизации, IAuthorizer же позволяет авторизоваться. Вы не сможете разбить это на два класса, т.к вам нужно напрямую взаимодействовать из одного с другим и наоборот.

воскресенье, 15 декабря 2019 г.

SOLID - обсуждение Open Closed Principle

#php #solid



  Бертран Мейер в основном известен как основоположник термина Принцип открытости/закрытости,
который появился в 1988 году в его книге Object-Oriented Software Construction. Идея
была в том, что однажды разработанная реализация класса в дальнейшем требует только
исправления ошибок, а новые или изменённые функции требуют создания нового класса.
Этот новый класс может переиспользовать код исходного класса через механизм наследования.
Производный подкласс может реализовывать или не реализовывать интерфейс исходного класса.


То есть получается если мы создаем например класс Человек, который имеет методы скажем
поесть, поспать, попить. Потом скажем через время нам нужно создать класс Работник
с методами - работать, получать зарплату и скажем ему нужен еще метод общаться, то
что получается мы не можем человеку добавить этот метод общаться, так как это нарушит
принципы SOLID? А только можем добавить его в класс Работник или любой другой который
наследуется Человека?...

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

Этот принцип же породит множество ненужных классов, наследующих базовый.

Из википедии:


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


Но данный принцип противоречит концепции (разборчиво написанного кода и расширяемости).
Т.е если у нас уже есть множество созданных объектов 'Человек', и в проекте мы хотим
чтобы они теперь могли общаться, после добавления данного метода в другой класс, мы
должны все наши объекты создать на базе другого класса. 

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


Ответы

Ответ 1



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

пятница, 13 декабря 2019 г.

Что лучше, два метода или один с параметром по умолчанию?

#c_sharp #ооп #архитектура #solid


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

// 1.

interface IExample {
    void Method(int a);
    void Method(int a, int b);
}

// 2.

interface IExample {
    void Method(int a, int b = DEFAULT);
}

    


Ответы

Ответ 1



Если Вы сами говорите, что для второго параметра в случае его неуказания будет назначено значение по умолчанию, то написание двух методов просто бессмысленно. Это тратит память на сохранение двух разных методов с, по сути, идентичным функционалом. Второй вариант однозначно лучше. Перегруженные методы создаются в тех случаях, когда во втором (n-ом в общем случае) параметре нет необходимости и он никак не участвует в вычислениях, производимых методом.

Не могу понять принцип единственной обязанности (Single Responsibility Principle)

#ооп #проектирование #solid


В теории все понятно, а на практике постоянно затруднения. Связаны они с тем, что
не понятно в каком масштабе должна рассматриваться эта самая "Единственная обязанность".
Вот нужно мне работать с базой данных. Если я создам класс DataBaseInteracting можно
считать что у него будет одна обязанность? Но ведь тогда он и записывает данные в базу
и читает их оттуда и обновляет. Может лучше создать 3 класса: DataBaseWriter, DataBaseReader
и DataBaseUpdater?
    


Ответы

Ответ 1



Принцип единой обязанности действует практически для любого масштаба: метод, класс, модуль, подсистема и т.д. При выборе обязанности, как и всегда, должна быть некоторая разумность. Более того, от всех принципов иногда можно отступать. Это придет с опытом. В вашем примере я бы сказал, что необходимости в разбиении класса DataBaseInteracting согласно действиям нет. Обязанность "обеспечение хранения данных" достаточно атомарна, на мой взляд, и делить ее на чтение/запись/обновление не нужно. Однако имеет смысл поделить в других плоскостях. Например, если в этом классе есть код построения SQL запросов, то он должен быть вынесен в отдельный класс. Более того, если нужна поддержка нескольких БД, то таких классов должно быть несколько. Т.о. если возникнет потребность какого-то высокоуровневого изменения в "обеспечении хранения данных", вы будете менять класс DataBaseInteracting. Если обнаружится баг с построением неоптимального запроса, вы будете менять класс, который строит запросы и даже не будете трогать DataBaseInteracting. Если вам нужно будет поддержать новую БД, вы просто добавите новый класс. Понимаете? При этом все эти классы должны лежать в проекте DAL. И в этом проекте должно лежать только то, что относится к логике взаимодействия с хранилищем (БД в вашем случае). Так вы поддержите SRP на уровне проекта.

Ответ 2



Так называемый "Принцип единственной ответственности" (SRP) на самом деле не имеет никакого отношения к количеству фич, которые реализует сущность. Принципы SOLID направлены на то, чтобы решать конкретные проблемы, возникающие при работе с кодом, и в случае SRP - это проблема множественных изменений кода при изменении технического задания (ТЗ). Если код, отвечающий за выполнение пункта ТЗ разбросан по многим исходникам, то при изменении этого пункта ТЗ, придется править исходники во многих местах. По этому SRP рекомендует нам собирать весь код, отвечающий за тот или иной пункт ТЗ в одном месте. Чтобы когда ТЗ поменяется, надо было бы править только одно место в коде (один модуль, или один файл, или один класс, или одну функцию, или одну строчку). Из этого следует, что если несколько пунктов ТЗ можно сделать одним куском кода - то это нормально, а вот обратная ситуация - когда на один пункт ТЗ надо много кусков кода - это уже плохо. Ответственность размазывается по исходинкам, SRP не соблюдается, коллеги плачут от коммитов по 40 файлов. По этому если DataBaseInteracting - это один небольшой класс, то с точки зрения SRP - нет никакого смысла разбивать его на DataBaseWriter, DataBaseReader и DataBaseUpdater. Другое дело, что есть другие принципы, например OCP (замена одних кусков кода на другие), или проблема изоляции кусков кода при тестировании.

Ответ 3



Проблема возникает из-за того, что крупные системы состоят из нескольких слоёв, и уровень детализации на каждом слое различается. Поэтому единственную обязанность логично определять для степени детализации характерной для слоя. Поскольку класс DatabaseInteracting кажется мне слишком абстрактным, предложу вместо него рассмотреть известный паттерн Repository (Хранилище). Есть несколько его модификаций, и чтобы не путаться, давайте за основу возьмём модификацию, предложенную Эвансом в книге по DDD. Эванс пишет, что Хранилище обеспечивает долговременное хранение объектов доменной области — это его ответственность. Она реализуется посредством операций чтения, обновления и удаления (сюда же можно присовокупить и создание, но это отличается от канона DDD). Перейдя на уровень ниже (детально рассматривая операции) мы понимаем, что для ускорения работы с базой можно применить модель master-slave, где одна БД считается главной, и ещё несколько — подчинёнными. Подчинённые БД постоянно синхронизируют своё содержание с главной БД, так что на всех серверах одни и те же данные. Запись всегда происходит в главную БД, а читать можно из любой. При большом количестве чтений такая организация приложения даёт существенный прирост в скорости. На этом уровне детализации мы понимаем, что наши подключения к серверу БД будут двух видов: для чтения и для записи-чтения. Соответственно, операция чтения будет создавать подключение первого вида, а операции обновления и удаления — второго. У вас появятся классы ReadOnlyConnection и ReadWriteConnection, но на уровне предметной логики вы будете оперировать только Хранилищем. По своему опыту могу сказать, что причиной неясного кода очень часто является смешение уровней детализации. Здесь должны помочь групповые обсуждения кода или прицельная работа ведущих программистов.

Ответ 4



Во-первых, этот принцип очень спорный и потому почти не применим на практике. Почти никогда нет ТЗ, по которому пишется программный продукт. А если подробное ТЗ есть, то оно уже не меняется. Кроме того, совершенно очевидно, что для любого ТЗ можно легко придумать такую правку, из-за которой придется радикально переписывать программу. Ведь разные части программы опираются на одни и те же идеи из ТЗ, которые могут измениться. Во-вторых этот принцип противоречит принципу KISS (keep it simple stupid / чем проще, тем лучше), одному из немногих, который действительно работает при дизайне системм. Если вы разбиваете объект на части, то усложняете программу, вместо одного класса у вас получается 2, 3, 4 и больше. В результате, зачастую, если исходную программу можно было легко понять, то "улучшенную" уже сложно, т.к. в ней много объектов и они все не помещаются в сознание. тут подробнее

вторник, 10 декабря 2019 г.

Как immutable объекты позволяют соблюдать принцип подстановки Лисков?

#java #ооп #solid


На эти размышления меня натолкнула следующая статья.

В ней приведен классический для принципа Лисков пример с прямоугольник и квадратом.
В коде это можно выразить так:

class Rectangle {

private int width;
private int height;

public int getWidth() {
    return width;
}

public void setWidth(int width) {
    this.width = width;
}

public int getHeight() {
    return height;
}

public void setHeight(int height) {
    this.height = height;
}

public int area() {
    return width * height;
}

}

class Square extends Rectangle{

@Override
public void setWidth(int width){
    super.setWidth(width);
    super.setHeight(width);
}

@Override
public void setHeight(int height){
    super.setHeight(height);
    super.setWidth(height);
}

}

public class Use {

public static void main(String[] args) {
    Rectangle sq = new Square();
    LSPTest(sq);
}

public static void LSPTest(Rectangle rec) {
    rec.setWidth(5);
    rec.setHeight(4);

    if (rec.area() == 20) {
        // делать что то полезное
    }
}

}


Если в метод LSPTest подставить объект Square вместо Recatangle поведение программы
изменится. Это противоречит принципу Лисков.

Автор вышеупомянутой статьи делает такое заявление:


  Квадрат перестает быть нормальным прямоугольником, ТОЛЬКО если квадрат и прямоугольник
являются изменяемыми! Так, если мы сделаем их неизменяемыми (immutable), то проблема
с контрактами, принципом подстановки и нарушением поведения клиентского кода при замене
прямоугольников квадратами пропадет. Если клиент не может изменить ширину и высоту,
то его поведение будет одинаковым как для квадратов, так и для прямоугольников!


Я не понимаю почему. Может это от того что я не хорошо понимаю сам LSP или immutable.
Я переписал пример:

Добавил конструктор в Rectangle:

public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
}


И изменил методы установки длины и ширины.

public Rectangle setWidth(int width) {
    return new Rectangle(width, this.height);
}

public Rectangle setHeight(int height) {
    return new Rectangle(this.width, height);
}


Вот как изменился класс Square:

public Square() {

}

public Square(int width, int height) {
    super(width, height);
}

@Override
public Rectangle setWidth(int width) {
    return new Rectangle(width, width);
}

@Override
public Rectangle setHeight(int height) {
    return new Rectangle(height, height);
}

}


И клиентский код:

public class Use {

public static void main(String[] args) {
    Rectangle sq = new Square(4, 4);
    LSPTest(sq);
}

public static void LSPTest(Rectangle rec) {
    rec = rec.setHeight(5);

    if (rec.area() == 20) {
        System.out.println("yes");
    }
}

}


Все те же проблемы остались. Какая разница, изменяется ли сам объект или возвращается
новый объект? Программа то ведет себя по-разному для базового класса и его подкласса.
    


Ответы

Ответ 1



Призвали бы в пост, ответил бы раньше:) Можно рассматривать разные варианты неизменяемости. С одной стороны, объект может быть неизменяемым, но при этом предоставлять методы withNewValue или setValue, которые вернут новый экземпляр объекта. А с другой стороны, тип может не предоставлять даже этих возможностей. В первом случае объект неизменяемый, но мы можем огрести проблемы с LSP, как и проблемы с многопоточностью (это, на самом же деле, в некоторой мере один из мифов, что неизменыемые объекты безопасны в многопоточной среде; они менее опасны, но наличие методов setXXX может привести к гонкам). В случае "полной неизменяемости" (да, такого термина нет), этих проблем не будет. Еще раз приведу приведенную в вопросе цитату: Квадрат перестает быть нормальным прямоугольником, ТОЛЬКО если квадрат и прямоугольник являются изменяемыми! Так, если мы сделаем их неизменяемыми (immutable), то проблема с контрактами, принципом подстановки и нарушением поведения клиентского кода при замене прямоугольников квадратами пропадет. Если клиент не может изменить ширину и высоту, то его поведение будет одинаковым как для квадратов, так и для прямоугольников! Я зря сделал акцент на неизменямость. Главная мысль выделена мною сейчас жирным: даже при наличии setWidth методов, возвращающие новый экземпляр, нарушение LSP возможно, вы правы. Если же у клиента этой возможности нет совсем (просто такого API не существует), то тогда нарушение невовзможно. З.Ы. Приведенный пример уважаемого @VladD будет нарушать LSP;), поскольку трюк с final противоречит следующему неформальному, но вполне вменяемому контракту: вызов метод withXXX не меняет тип объекта: public class Use { public static void main(String[] args) { Rectangle sq = new Square(3); LSPTest(sq); } public static void LSPTest(Rectangle rec) { Rectangle oldRec = rec; rec = rec.withWidth(4).withHeight(5); // Первое из следующих утрвеждений будет нарушено! assert(oldRec.getClass().equals(rec.getClass()), "Неявный контракт: метод withWidth/withHeight не должны поменять тип объекта"); assert(rec.getWith() == 4); assert(rec.getHeight() == 5); if (rec.area() == 20) { // делать что то полезное } } }

Ответ 2



Смотрите, в чём проблема. Пусть у нас есть ссылка на прямоугольник, которяа пришла из другой части программы. И мы надеемся, что если вы установим его ширину в 3, а длину в 4, то у нас таки-будет прямоугольник шириной в 3 и длиной в 4. Но если нам кто-то подсунул вместо прямоугольника квадрат (они совместимы по присваиванию, так что мы ничего и не заподозрим!), то у нас получится прямоугольник длиной и шириной в 4. Проблема, правда? Теперь, в случае иммутабельности у нас не возникает этой проблемы. Что бы у нас в начале ни было — прямоугольник или квадрат — когда мы просим установить ширину в 3 и длину в 4, мы получаем новый прямоугольник (а не квадрат) нужных размеров. Всё работает как нужно. Код, который вы привели в качестве примера для immutable-квадрата, неправильный. Вам вовсе не нужно для этого случая переопределять setWidth и setHeight, унаследованные функции делают уже в точности то, что надо. Ведь если поменять квадрату только длину, но не ширину, полученная фигура будет прямоугольником. Вы можете, если хотите, добавить для квадрата метод public Square setSideLength(int width) { return new Square(width, width); } Фокус в том, что с мутабельными объектами вы не можете правильным образом переопределить методы типа setWidth для квадрата. А вот для иммутабельного случая можете. Итак, правильный код для иммутабельного случая такой: Прямоугольник: class Rectangle { private int width; private int height; public final int getWidth() { return width; } // setWidth переименовали в withWidth public final Rectangle withWidth(int width) { return new Rectangle(width, this.height); } public final int getHeight() { return height; } // setHeight переименовали в withHeight public final Rectangle withHeight(int height) { return new Rectangle(this.width, height); } public final int area() { return width * height; } } Класс квадрата: class Square extends Rectangle { public Square(int sideLength) { super(sideLength, sideLength); } public final Square withSideLength(int sideLength) { return new Square(sideLength); } } Теперь использование: public class Use { public static void main(String[] args) { Rectangle sq = new Square(3); LSPTest(sq); } public static void LSPTest(Rectangle rec) { rec = rec.withWidth(4).withHeight(5); if (rec.area() == 20) { // делать что то полезное } } } Этот тест работает без проблем.

воскресенье, 8 декабря 2019 г.

Проблема имплементации SOLID при наследовании метода со switch-блоком

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


Недавно в процессе работы столкнулся с интересной проблемой имплементации принципов
SOLID на практике, которой хочу поделиться с сообществом. 

Допустим, есть у нас функция, открытый член класса (в данном случае название класса
и функции неважно), которой на вход передается единственный параметр (допустим целочисленный).
В теле функции происходит проверка входного параметра с помощью блока switch. Внутри
блока switch имеется несколько веток case (сейчас 5), где в случае совпадения происходят
некоторые действия влияющие на возвращаемый результат. Казалось бы проще простого, но...

Главная проблема - это каким образом я могу обеспечить возможность расширяемости
функционала данного класса в случае, если в будущем возникнет необходимость обрабатывать
большее количество веток case (то есть, еще несколько, помимо уже имеющихся 5-ти) без
модификации тела уже имеющейся функции (SOLID, open/closed principle)?
    


Ответы

Ответ 1



Не вдаваясь в то, что там действительно могут быть проблемы с архитектурой class A public meth(int i) switch i case 1 ... class B extends A public meth(int i) switch i case 7 ... default return A.meth(i)

Ответ 2



Не существует "абстрактного подхода" к применению принципов проектирования и уж тем более, нет такого понятия как "имплементация принципов на практике". Принципы проектирования были сформулированы для решения определенных проблем, в заданном наборе контрекстов. Вот, например, SRP возник в результате неспособности человеческого мозга справиться с наплывом сложности. OCP возник как необходимость параллельной разработки программных систем множеством людей. LSP возник для решения проблемы нанследования и формализации того, когда наследование корректно моделирует отношение "Является", а когда - нет. Не зная контекста задачи нельзя дать очень точного ответа, но исходя из описания я могу сделать следующий вывод: если сложность метода, который оперирует переданным аргументом относительно невысока (пол-экрана кода), код простой и понятный, то с ним все в порядке и никаких изменений вносить в него категорически не следует. Полиморфизмы и фабрики сделают дизайн сложным в понимании и сопровождении. Простой switch - это наиболее естественный способ решения проблемы, когда вся логика находится в одном месте. Не нужно пытаться предугадать изменения требований. Это все равно не получится сделать. Если внесение изменений будет продиктовано изменениями требований (и появится еще один case в блоке switch), то код менять все равно придется. Придется еще и тесты поправить, да и задеплоить изменения куда следует. OCP не подразумевает гибкость решения. Он говорит об изоляции изменения нужными местами. Бертран Мейер, автор OCP отдельно выделяет дополнительное правило, которое он называет "Принципом единственного выбора", который заключается в том, что система не нарушает OCP, если некоторое решение принимается в одном месте. Вывод: не нужно ничего менять. С методом все в порядке, пока подобных switch-ей больше нет в приложении. Никакие принципы не нарушаются, котята целы, а клиенты довольны. И не гонитесь за принципами, ради принципов! З.Ы. Довольно подробно я рассматривал принцип LSP и принцип единственного выбора в статье Liskov Substitution Principle. UPDATE: как раз нендавно был поднят вопрос о YAGNI и SOLID принципах в вопросе "Нарушает ли OCP и DIP (из SOLID) принцип YAGNI?".

Ответ 3



Вам нужно заменить конструкцию switch на полиморфизм: наследование или стратегию. Я бы выбрал второй вариант ("Предпочитайте композицию наследованию"). Например, был у вас метод: void Foo(int i) { switch (i) { case 0: Console.WriteLine("Zero"); break; case 1: Console.WriteLine("One"); break; case 2: Console.WriteLine("Two"); break; default: Console.WriteLine("Unknown"); break; } } Введем интерфейс стратегии: interface INumberStrategy { void Do(); } И его наследники, по одному на каждую ветку switch: class ZeroNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Zero"); } } class OneNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("One"); } } class TwoNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Two"); } } class UnknownNumberStrategy : INumberStrategy { void Do() { Console.WriteLine("Unknown"); } } Теперь наш метод превращается в: void Foo(INumberStrategy strategy) { numberStrategy.Do(); } Плюс вам понадобится место, где будут создаваться нужные стратегии: class NumberStrategyFactory { INumberStrategy CreateStrategy(int i) { switch (i) { case 0: return new ZeroNumberStrategy(); case 1: return new OneNumberStrategy(); case 2: return new TwoNumberStrategy(); default: return UnknownNumberStrategy(); } } } Т.о. при добавлении новой ветки алгоритма мы не будем менять исходный класс. Мы добавим новый класс стратегии, соответствующий новой ветке, и изменим фабрику (от добавления новой ветки в фабрике уже никуда не деться :)). Конечно, на таком простом примере это все выглядит излишним, но в сложной системе (я так полагаю, вы свой пример упростили) такие изменения оправдывают себя. Также важно помнить о том, что SOLID ради SOLID'а, шаблоны ради шаблонов и т.п. -- это неверный путь. Код писать нужно как можно проще, главное не в ущерб расширяемости и поддерживаемости. И во многих случаях может быть действительно проще остаться с изначальным switch'ем.

Ответ 4



Вариация решения от Etki, которая гарантирует выполнение метода основного класса (но и запрещает изменять реализацию для описанных в нем значений) class A { protected methDelegate(int i) {} public final meth(int i) { switch i { case 1 ... default methDelegate(i); } } } class B extends A { @Override protected methDelegate(int i) { switch i { case 7 ... } } } Тут пока что остается проблема с блоком default из класса А. Если что-то должно выполняться, когда ни один вариант не подошел — как заставить B вызывать этот блок? Сделать метод defaultDelegate() и надеяться, что methDelegate() его вызовет? Костыль, в котором methDelegate возвращает false в default и true иначе?

Ответ 5



Вот тут описана ваша проблема и её решение. Коротко: switch-case заменить полиморфизмом.

Ответ 6



SOLID - это принципы проектирования. Их нельзя "имплементировать". Их можно только применять (или не применять). Каким образом я могу обеспечить возможность расширяемости функционала данного класса в случае, если в будущем возникнет необходимость обрабатывать большее количество веток case (то есть, еще несколько, помимо уже имеющихся 5-ти) без модификации тела уже имеющейся функции (SOLID, open/closed principle)? Ваша формулировка подразумевает следующие ограничения: код надо написать раз и навсегда в этом коде есть switch на вход кода подается целое число в зависимости от его значения нужно вызвать соответствующий код. Последние два ограничения подразумевают switch. Или его аналоги с регистрацией обработчиков для каждого значения (вроде Dictionary). Или еще какой-то финт, который на самом деле сводится к хорошо спрятанному switch. Единственный вариант с таким набором ограничений - это написать новый метод, который обработает новые ветки. А для старых вызовет старый метод (как в примере @Etki). Иначе вы нарушите Open/Closed. А это плохо (по крайней мере все так считают). На самом же деле у вас уже нарушены и Open/Closed и Single Responsibility Principle. Т.к. у вас есть явно больше чем одна причина для изменения кода: может поменяться логика для 1 может поменяться логика для 2 могут быть добавлены новые значения Open/Closed был нарушен в тот момент, когда вы написали switch. Как не нарушить уже нарушенный принцип? Никак. Надо сначала починить то, что есть. Вам нужно убирать switch - это как-то убрать целое число из параметров. Например, заменив его на набор классов с общим интерфейсом (как в решении от @andreycha) Для применения принципов проектирования (любых) надо осознавать какую проблему каждый из этих принципов решает. Open/Closed решает проблему "как не поломать старый код". Так вот, кроме фанатичного применения блестящей идеи 30-летней давности "а давайте старый код не менять!" есть куча современных альтернатив. Например, вместо того, чтобы думать как применить sOlid к коду со свитчем на 5 вариантов можно написать на него тесты! Еще лучше - написать тесты до того, как этот switch будет написан! И вообще писать тесты вне зависимости от того, умеете вы применять SOLID или нет. А Open/Closed применять не как бездумный "запрет на редактирование", а как принцип, предлагающий "дописывать/расширять" код вместо "переписывания".

воскресенье, 1 декабря 2019 г.

Реализация интерфейса ICollection в конструкторе класса

#c_sharp #проектирование #solid


Когда читаю различные туториалы, да и наш любимый StackOverflow, то часто вижу подобный код:

namespace MvcApplication2.Models
{
    public class Category
    {
        public int ID { get; set; }

        public string Name { get; set; }
    }

    public class Product
    {
        public ICollection CategoryID { get; set; }

        public Product()
        {
            CategoryID = new List();
        }
    }
}


Объясните зачем свойство CategoryID объявлять как интерфейс ICollection, если в конструкторе
он явно инициализируется при помощи List?

Что пытается избежать проектировщик при таком подходе?

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

Какой концептуальный момент я упустил? 
    


Ответы

Ответ 1



Значение торчит наружу и клиентский код не знает, что там List. То есть, по велению левой пятки архитектора, в новой версии библиотеки List может быть заменено на LinkedList и никто не пострадает. Это абстракция над реализацией коллекции.

Ответ 2



Здесь автор, по идее, старается следовать принципу наименьшего знания и не выставлять наружу детали реализации. Возможно данный код старый или не показаны все составляющие, но в данной реализации дизайн не хорош. Свойство CategoryID мутабельное. Клиент может записать туда null с последующим огребанием NullReferenceException. Свойство только старается спрятать детали реализации, но делает это плохо. Все сильно зависит от контекста, но существует как минимум два способа сделать этот дизайн более жестким с одной стороны и более простым с другой. Во-первых, можно сделать тип неизменяемым и получать коллекцию в конструкторе. В этом случае, свойство CategoryID вместо типа ICollection может стать IReadOnlyCollection или IReadOnlyList. Во-вторых, если класс нельзя сдлать иммутабельным, то есть смысл добавить метод AddCategory и все же сделать свойство CategoryID типом IReadOnlyCollection/IReadOnlyList. Тут сложно говорить без контекста, но меня всегда поражают подобные объекты-данные в пространствах имен с именем Model. Модель в имени пространства имен говорит мне о том, что здесь будет спрятана вся суть приложения, ее доменные объекты, с поведением и всякими наворотами. А когда я вижу в таком пространстве имен простые объекты-данные, у меня происходит некоторое несовпадение ожиданий с реальностью. Другими словами, если есть желание создавать модели, то есть смысл прятать внутренности полноценно, а не убирая "реальный тип списка". Тогда можно будет добавлять более высокоуровневое поведение (какую-нибудь логику фильтрации по категориям и чего-нить еще) не ломая существующих клиентов. А теперь немного по теме: Интерфейсы коллекций в BCL немного сумасшедшие в том плане, что сейчас уже очень сложно сказать, что они означают. Это особенно относится к типу ICollection: что это за коллекция? Является ли она изменяемой? Вроде бы да, там же есть метод Add. Но вот беда, массивы тоже реализуют ICollection, метод Add которых бросает исключение. Да, там есть свойство IsReadOnly, но точно ли все клиенты его проверят? Ну, конечно, у коллекций есть методы Contains и Remove, но первый дает сложность O(N), что почти всегда плохо, а второй также не работает для всех коллекций. Вот и получается, что этот интерфейс зачастую используется в качестве такого себе IEnumerable + Count, но и в этом случае лучше подойдут IReadOnlyXXX представления. В качестве заключения: нужно понимать что и от кого вы прячите и прячите ли вы вообще что-либо. Если данный код используется в приложения, то есть два варианта: использовать конкретную коллекцию, если класс является хранилищем данных или же прятать коллекцию полноценно и выставлять IReadonlyXXX представление со специализированными методами Add.

суббота, 30 ноября 2019 г.

Почему композиция не нарушает Принцип единственной обязанности?

#java #ооп #архитектура #solid


Я решаю задачу о нахождении лидера (leader election).

Задача чисто алгоритмическая. Есть 2 формы задачи. У меня есть абстрактный класс
для представления данных и абстрактный класс Solver. Для каждой формы задачи эти классы
я расширяю в соответствии с нуждами этой формы задачи. То есть, решая задачу для первой
формы мне нужно написать так в клиентском коде:

MyData data = new MyDataForm1();
MySolver solver = new MySolverForm1();


Это можно было бы скомпозировать так: 

public abstract class AbstractTask{
// some code
}

public class Task1{
    MyData data = new MyDataForm1();
    MySolver solver = new MySolverForm1();

    //some code
}

public class Task2{
    MyData data = new MyDataForm2();
    MySolver solver = new MySolverForm2();

    //some code
}


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

AbstractTask task = new Task1();


Это же композиция? Но тут такая проблема: Получается что у классов Task 2 обязанности.
Они и данные хранят и задачу решают (да, делегируя это экземплярам MyData и MySolver,
но все же). И так ведь получается всегда при композиции. Мы включаем экземпляры нескольких
классов в один класс в качестве полей. У включенных классов были какие то обязанности.
Значит у включающего класса будет несколько обязанностей. 

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


Ответы

Ответ 1



У включенных классов были какие то обязанности. Значит у включающего класса будет несколько обязанностей. Не-а. Только его собственная. Во всяком случае, так будет выглядеть со стороны. Действует инкапсуляция. Обязанность вложенного класса является уже внутренней деталью реализации и эта деталь не должна быть видна тому, кто этим классом пользуется. Из этого же вытекает правило, по которому можно понять, разумно ли применение композиции в каждом конкретном случае: полностью ли "внутренняя" обязанность "погружена" во "внешнюю" (без существенного дополнительного "натягивания").

Ответ 2



Я понимаю тут так, что это более высокий уровень абстракции. Например, на уровне отдельных деталей машины можно рассматривать мотор, магнитолу и руль как отдельные классы, которые выполняют какую-то обязанности. Но потом мы начинаем рассматривать машину. Она состоит из этих частей. Но теперь мы рассматриваем на более высоком уровне абстракции (нам просто надо ездить), поэтому можно сказать что класс Машина все-таки выполняют всего одну задачу (езда). Вы здесь сами верно ответили на вопрос. Дело в том, что у нового класса будет обязанность, отличная от обязанностей объектов, которые он в себя включает. Если говорить конкретно, то обязанность нового класса заключается в том, чтобы соединять вместе данные и алгоритм решения. В последующем может обнаружиться, что такой объект будет весьма полезен, если понадобятся промежуточные шаги. Эта обязанность звучит несколько абстрактно, но тем не менее имеет право на жизнь. И, как можно заметить, она более высокоуровневая, чем обязанности включаемых классов. Об этом я вскользь упоминал в предыдущем ответе на ваш вопрос об SRP. Можно вывести правило: чем выше уровень "модуля", тем более высокоуровневыми являются его обязанности. Возьмем тип float. Его обязанность -- реализовывать работу с числами с плавающей точкой (представление, плюс базовые математические операции). Используя тип float (т.е. используя композицию), вы пишете функцию расчета гипотенузы по заданным катетам. Обязанность этой функции -- считать гипотенузу. Дальше вы включаете эту функцию в пакет PlanimetryAlgorithms. Обязанность этого пакета -- предоставлять различные алгоритмы, связанных с планиметрий (т.е. геометрией фигур на плоскости). Пакет PlanimetryAlgorithms может, в свою очередь, входить в библиотеку Geometry, куда также будут входить пакеты для других разделов геометрии. Обязанность этой библиотеки -- предоставлять различные функции, касающиеся геометрии вообще. Как видно, с повышением гранулярности обязанность становится более высокоуровневой, но при этом ее единственность соблюдается. Функция расчета гипотенузы не выдает нам заодно значения всех углов в этом треугольнике, пакет PlanimetryAlgorithms не содержит методов для рисования фигур, а библиотека Geometry не начинает вдруг заниматься физикой.

Ответ 3



Верно, все дело именно в уровне абстракции. Объекты, выполняющие единственную функцию высшего уровня, для выполнения своих обязанностей пользуются функциями включенных в себя объектов. Обязанность электродвигателя как отдельного объекта - превращать электрическую энергию в механическую (вращение вала). Объект, например, наждак выполняет свою функцию - предоставляет инструмент для заточки ножей. А троллейбус - другую функцию.И тот, и другой включают в себя электродвигатель в том или другом виде (наследники абстрактного электродвигателя), и используют его для выполнения своей функции. Также у нас бывают объекты типа "швейцарский нож": объект вроде один, но функций как бы много. С этим нужно быть осторожным при проектировании, чтобы не включить "посторонних" функций и не нарушить принцип. Тоже хороший пример композиции.

Что такое принцип открытости и закрытости?

#c_sharp #архитектура #шаблоны_проектирования #solid


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

Читал книги, но мне не понятен такой момент: я как раз и не понимаю, если класс закрыт
от изменения, то как его можно расширить
    


Ответы

Ответ 1



Итак, принцип гласит, что программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения Итак, видно, что речь идет о: Классах Модулях Функциях и т.д. Все они должны быть открыты для расширения, но закрыты для модификации. Звучит отлично, но понять этот принцип по его названию довольно сложно. Начнем с "закрыты для модификации". Это означает, то единственная причина, по которой вы можете менять код класса\функции\модуля - это непосредственно изменение заложенной в него функции. Все. Больше причин менять этот код быть не должно. Именно в этой точке идет пересечение в принципом единственности ответственности. Далее, что значит открыты для расширения? Это означает, что если вам надо, чтобы ваш класс\функция\модуль могли выполнять заложенные функции в новом окружении - они должны это поддерживать без изменения их кода. Давайте, для наглядности, рассмотрим пример. Расширение класса делегированием Допустим, у нас есть класс для сортировки массива public class BubbleSorter { public void Sort(int[] data) { int n = data.Length; for (int i = 0; i < n - 1; i++) for (int j = 0; j < n - i - 1; j++) if (data[j] > data[j + 1]) { int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; } } } Поглядим на этот класс. Попробуем понять, какие точки расширения у него есть. Точки расширения - это те требования, которые скорее всего в вашем приложении возникнут (или уже возникли) в ходе разных сценариев использования вашего кода. Например, мы видим, что этот метод сортирует только числа. Но с вероятностью 99% нам надо будет сортировать что то ещё, кроме чисел. Также, сортировка идет только по возрастанию, но, скорее всего нам понадобится и сортировка по убыванию. По сути, мы опять пересекаемся с принципом единственности ответственности - мы определяем, какие ответственности сейчас есть у класса и пробуем их разделить. Давайте используем существующие в дотнете интерфейсы и перепишем немного код: public class BubbleSorter { IComparer _comparer; public BubbleSorter(IComparer comparer) { _comparer = comparer; } public void Sort(T[] data) { int n = data.Length; for (int i = 0; i < n - 1; i++) for (int j = 0; j < n - i - 1; j++) if (_comparer.Compare(data[j], data[j + 1]) > 0) { var temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; } } } Теперь наш сортировщик стал немного более гибким. Теперь, в случае, если нам понадобится сортировать строки, вместо чисел, нам не придется вносить изменение в наш сортировщик. Если нам надо будет сортировать по убыванию, а не по возрастанию, нам не придется менять код сортировщика. Таким образом, мы можем расширить наш класс новым функционалом, по сути не меняя сам класс. То же самое справедливо и для функций. Например, var sorted = data.OrderBy(x=> /* ваше направление сортировки */ ); То же самое, справедливо для программных модулей (например, когда вы можете переиспользовать один и тот же модуль в разных сценариях). Однако, изложенное выше не означает, что вам нужно немедленно броситься к своим классам и начать внедрять в них подобные приемы. Как всегда, прежде, чем выполнять подобное, надо думать головой. Например, вы можете видеть, что код выше сортирует только массивы. причем сортирует на месте (меняет исходный массив). Я намеренно это оставил, так как хотел показать, что не надо уходить в крайности. Не надо слепо следовать принципу, надо перед тем, как что то написать, хорошенько подумать. Потому что, если бездумно следовать принципу, то можно в конце получить слишком абстрактный код, в котором никто ничего уже не поймет. В моем случае, я решил, что мне не нужно сортировать другие типы данных, кроме массивов. Я предположил, что в текущем проекте мне это не понадобится (считайте, что это часть синтетического примера). Но мы рассмотрели только один из вариантов расширения класса - передачи части его ответственности на другой объект, который можно подменить динамически. Что ещё мы можем сделать с классом? Расширение класса наследованием Предположим, у нас типичный класс писателя в файл CSV public class CsvWriter { protected void WriteBody(IEnumerable obj, TextWriter stream){ foreach(var ob in obj) stream.WriteLine(ob); } protected virtual void WriteInternal(IEnumerable obj, TextWriter stream) { WriteBody(obj, stream); } public void Write(IEnumerable obj, TextWriter stream) { WriteInternal(obj, stream); } } Уже наметанным глазом, вы можете увидеть, что в данном случае сам класс решает, как именно будут выглядеть записи объектов в файле. Это, конечно, кандидат на выделение в отдельную ответственность, но это нас сейчас не волнует. А вот что волнует - так это то, что наш класс не пишет заголовок файла. То есть колонки он может как то и запишет, но вот заголовка каждой колонки - нет. Что же делать? Из сигнатуры функций класса можно заметить, что у него есть виртуальный метод, полностью определяющий порядок записи данных в файл. Мы можем легко написать наследника класса и добавить в нем заголовок файла, при этом не изменяя базовый класс: public class CsvWriterWithHeader : CsvWriter { protected virtual void WriteHeader(TextWriter stream){ stream.WriteLine("I AM HEADER!"); } protected override void WriteInternal(IEnumerable obj, TextWriter stream) { WriteHeader(stream); WriteBody(obj, stream); } } Как итог - функционал расширен, исходный класс не тронут. Расширение класса аггрегированием Итак, например у нас есть интерфейс и класс для репозитория. Возможно, у вас даже несколько реализаций такого репозитория. public interface IRepository { void SaveStuff(); } public class Repository : IRepository { public void SaveStuff() { // save stuff } } И, конечно же есть какой-то клиент всего этого class RepoClient { public void DoSomethig(IRepository repo) { //... repo.SaveStuff(); } } И однажды вам босс говорит, что вам нужно логгировать каждый вызов каждой реализации вашего репозитория. Но менять из за этого все реализации не хочется (и вы уже знаете почему). Что же делать? Конечно накатать новую реализацию-обертку public class RepositoryLogDecorator : IRepository { public IRepository _inner; public RepositoryLogDecorator(IRepository inner) { _inner = inner; } public void SaveStuff() { // log enter to method try { _inner.SaveStuff(); } catch(Exception ex) { // log exception } // log exit to method } } Что она делает? По сути, она принимает декорируемый объект и проксирует вызовы к нему с логгированием. И если раньше ваш код выглядел так: var client = new RepoClient(); client.DoSomethig(new Repository()); То теперь он будет выглядеть как то так: var client = new RepoClient(); client.DoSomethig(new RepositoryLogDecorator(new Repository ())); Меняли ли мы наши реализации репозиториев? Нет. Добавили к ним функционал? Да. Ещё немного инфы про подобные фокусы с классами тут Как итог, хочу отметить, что мы затронули всего пару вариантов, как можно расширить функционал, не затрагивая реализации. На самом деле вариантов больше, тут важно понять принцип - вы пишете класс\функцию\модуль, и они делают только то, для чего они созданы и расширяем их функционал уже не затрагивая их исходный код. И не забываем про принцип единственности ответственности - как вы уже должны были заметить, он очень тесто связан с принципом открытости\закрытости. Также повторюсь, что необходимо иметь гибкость ваших структур данных такую, которая обеспечивает соблюдение требований проекта, и не порождать избыточной гибкости, так как излишняя гибкость усложняет ваши абстракции почем зря. Если вы таки дочитали до этого момента, то раскрою вам великую тайну - каждый из методов выше использует тот или иной паттерн. Попробуйте подумать какой паттерн где, пробегитесь по основным паттернам из самой известной книги по паттернам (от банды), подумайте, какие из них расширяют функционал класса, и тогда, надеюсь, мозаика начнет складываться.

Принцип минимальной информированности, когда можно нарушать?

#ооп #архитектура #solid


Вот пример такой композиции в коде, как я понимаю нарушает принцип минимальной информированности
(Principle of Least Knowledge) (см. Закон Деметры):

class A:

    def method1(self):
        pass

    def method2(self):
        pass

    def method3(self):
        pass


class B:

    def __init__(self):
        self.a = A

    def method1(self):
        pass


b = B()
b.a.method1()


Однако, я заметил, что часто используют такой подход для расширения интерфейса класса.

Пример из джавы и C#:

System.out.println()


Пример из Джанго:

MyModel.objects.create()


Вот и интересно, когда расширять класс таким образом можно считать правильным?
    


Ответы

Ответ 1



Ограничения накладываются согласно здравому смыслу и предметной области. Я напишу о человеке. Человек, в большинстве случаев, может двигать конечностями, думать головой и делать ещё какие то действия, которые довольно поверхностны. Если вы пишете игру и человеки там - не предмет каких то модификаций, то доступны у них должны быть те же операции, что и в реальной жизни. Стоит иметь в виду, что для мед. оборудования или игр с модификацией людей - доступ может быть намного шире. Простой пример - человек.рука.двигать(). Что бы ни случилось в реализации, такой код останется валидным и не должен превращаться в человек.ДвигатьРукой() или человек.Двигать(рука). При этом, люди не умеют контролировать конкретные мышцы в руке, к примеру, а значит, объект должен скрывать собой внутренности реализации. Т.е. рука.Двигать() но не рука.мышцы[x].двинуть(y, z). Ещё проще - когда свойство обязательно и неизменно, обосновано предметной областью - можно ссылаться на него, иначе его должен скрывать основной класс.

Ответ 2



Для начала нужно разобраться в чем проблема, когда один объект выставляет свои внутренности наружу, после чего станет понятным, когда это проблемой не является. Инкапсуляция и сокрытие информации направлены на упрощение разработки. Хороший дизайн позволяет сосредоточиться на минимальном числе концепций в один момент времени и ограничивает число изменений минимальным числом модулей в случае изменения требований. Когда объект выставляет свои внутренности наружу, то это может привести к более хрупкому решению. По своей природе, открытый интерфейс класса должен быть более стабильным, а детали реализации – скрытыми от его клиентов. В этом случае, реализация может быть заменена, не нарушая работу клиентов. Когда в открытый интерфейс просачиваются детали реализации, в виде открытых полей или даже свойств, то это ограничивает возможности автора класса на изменения. Код вида A.B.C.D.E.Foo() обладает низкой стабильностью, поскольку внесение изменений в один из 5 классов наверняка его сломает. С другой стороны, не любое открытое свойство раскрывает детали реализации. Иногда, подобные свойства являются частью самой сути решаемой проблемы. Например, у квадрата есть координаты и обращение вида: square.Position.X вполне может быть оправданным. Иногда, приходится нарушать данное правило по другой причине, например, при разработке библиотек. Любая сложная система является иерархической, и нам просто необходимо группировать связанные концепции. Тот же System.out из мира Java является подобным примером. Мы хотим объединить все системные операции за некоторым фасадом, который будет удобной точкой входа для исследования. В этом плане мы не собираемся перемещать out в другой класс, и точно не будем дублировать операции из PrintStream в классе System. Итак, в качестве заключения: закон Деметры не нарушается, если отношение целое-часть является частью предметной области. Закон Деметры не нарушается в случае фасадных классов, единственная задача которых предоставить доступ к некоторым объеткам. Но закон Деметры нарушается, если выставленное свойство является деталью реализации, которая может измениться в будущем.

понедельник, 25 ноября 2019 г.

Нарушает ли OCP и DIP (из SOLID) принцип YAGNI?


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

Также выделять абстракцию может заставить необходимость тестирования пользователей типа. Но в рамках java, как я понимаю, такой необходимости нет. 

Так вот, нужно ли выделять абстракцию сразу же? Нужно ли следовать OCP и DIP?
    


Ответы

Ответ 1



Разные принципы проектирования направлены на решение определенной задачи проектирования, и в некоторых случаях они могут противоречить друг другу. Можно сказать, что разные принципы «тянут» дизайн в разные стороны и нужно найт правильный вектор, наиболее полезный в данном конкретном случае: SRP – говорит о простоте решения, OCP – об изоляции компонентов модулей, DIP – о «правильности» отношений между классами, а LSP – о «правильном» полиморфизме. Следование одному принципу может привести к нарушению другого. Так, например, любо наследование можно рассматривать как нарушение SPR, поскольку теперь за одну ответственност (рисование фигур) отвечает целая группа классов. Следование DIP и OCP могут привести к появлению дополнительных «швов», т.е. интерфейсов/базовых классов в системе, что, опять-таки, приведет к нарушению SRP и/или ISP. Но такое отношение между принципами не является фиксированным. Для простого случа выделение иерархии фигур для рисования является нарушением SRP, поскольку «рисование в первой итерации может заключаться в выводе текста на консоль и размазывание этой информаци по нескольким классам будет избыточным. Но по мере усложнения решения, появление иерархии наследования будет оправданной с точки зрения SRP, поскольку сложность отображения каждой отдельной фигуры будет столь высокой, что понятие «ответственности» тоже поменяется. Если вначале «единой ответственностью» было отображение всех фигур, то теперь одна ответственность будет разбита на множество: «отображение круга», «отображение квадрата» и т.п. Принцип YAGNI (You Aren’t Gonna Need It) – это более фундаментальный принцип («принци высшего порядка» или «метапринцип»), который поможет понять, когда следовать принципам/паттернам/правилам, а когда нет. В основе принципа YAGNI лежит несколько наблюдений: Программисты, как и люди в целом, плохо предсказывают будущее. Ни одно гибкое решение не будет достаточно гибким. Эти наблюдения приводят к следующим выводам: попытка создать гибкое решение на ранни стадиях разработки обречено на создание переусложненного решения. Связано это с тем, что на ранних этапах еще не известно, какие именно изменения в системе потребуются, и просто не понятно, где «подстилать солому» для будущих изменений. Поскольку на ранних этапах мы не знаем, какая именно гибкость нужна, мы заложим гибкост не там, где нужно: мы предусмотрим замену слоя доступа к данным, но из-за «дырявых абстракций» мы все равно залочим решение на определенной базе данных, или же такая гибкость просто никогда не понадобиться. Мы создадим «фреймворк» парсинга аргументов командной строки, который будет использоваться в одном приложении, а стоимость прикручивания его в другое приложение будет таким большим, что никто этим заниматься не будет. Хороший дизайн заключается в простоте решения, когда изменения требований ведет к линейным трудозатратам. Проще всего добиться этого путем эволюционного дизайна: мы начинаем с разбиения систем на крупные компоненты, но не занимаемся выделением лишнего. Не нужны базовые классы если сейчас нет хотя бы 2-х-3-х наследников. И даже если такие наследники «могут появиться в будущем», то выделить иерархию типов нужно именно тогда, когда это самое будущее настанет. Принцип YAGNI можно выразить следующим образом: выделение лишних абстракций (и любо другое усложнение) оправдано лишь в том случае, если стоимость их выделения в будущем будет существенно дороже, чем сейчас. Инвестиции в продуманность интерфейса программирования библиотеки (API) – будут оправданны поскольку стоимость внесения изменений очень высока. Стоимость же выделения интерфейса/базового класса приложении является практически одинаковой сегодня или через год. Решение проблемы по мере поступления позволяет сосредоточиться на задачах, актуальных сегодня и позволяет избежать работы, которая может и не понадобиться совсем. P.S. Ну и мне кажется, что у вас не совсем правильное понимание принципов OCP и DIP, которые совсем не сводятся к необходимости применения наследования. Вот несколько статей по теме: Шпаргалка по SOLID принципам Open-Closed Principle Dependency Inversion Principle Критический взгляд на DIP И отдельно, в статье "О принципах проектирования" я рассматриваю примерно то же самое что и в этом ответе: что слепое следование принципам приведет к переусложненному и тяжелому в сопровождении решению.

Когда НЕ нужно использовать SOLID?


Читал, что SOLID - это хорошие рекомендации, проверенные временем, но пихать их 
каждый проект не стоит. Гуглил примеры, когда его применять не нужно и не нашел. Скажите, в каких случаях применение SOLID неоправданно?
    


Ответы

Ответ 1



Касательно любого паттерна/принципа разработки можно сказать: Если вы следуете ему, нет гарантии, что ваш код автоматически станет более корректным, расширяемым и сопровождаемым. Если вы не следуете ему, нет гарантии, что ваш код автоматически становится проблемным, не расширяемым и не сопровождаемым. Следуя ему вы можете решить одну из проблем своего кода (или не решить), но создать новые проблемы. Также имеет место быть неполное или даже некорректное понимание используемых принципов/паттернов И возможно даже возведение в абсолют, когда буквальное следование им становится превыше здравого смысла. Таким образом вопрос не в том, когда не нужно использовать SOLID, а в том, насколько использовать каждый из его принципов в своем, конкретном случае. S - Single Responsibility Principle Класс должен иметь только одну ответственность При недостаточном разделении ответственности получаем антипаттерн "god", класс становитьс кухонным комбайном. В то же время при чрезмерном разделении код может раздробится так, что в каждом классе остается чуть ли не один метод, состоящий из одной строки кода. В итоге сложность восприятия может увеличиться. O - Open/Closed Principle Программные сущности должны быть открытыми для расширения, но закрытыми для модификации Возьмем ситуацию, когда базовый класс содержит общие детали реализации, а нескольк наследников уточняют её. Следуя этому принципу, при добавлении новых потомков, их общи черты придется выносить в один промежуточный класс, чтобы не трогать базовый. Со временем может образоваться причудливая иерархия, хотя, вероятно, стоило рассмотреть вариант модификации базового класса (несмотря на возможные поломки уже имеющихся потомков). L - Liskov Substitution Principle Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы Ради обеспечения этого принципа иногда приходится использовать сомнительные решения К примеру класс Ellipse имеет подкласс Circle, который является случаем Ellipse с одинаково длиной по осям X и Y. В соответствии с принципом, подкласс Circle обязан реализовать поведение родителя. Если Ellipse содержит метод stretchX, позволяющий модифицировать длину по оси X, то класс Cirlce также обязан реализовать это поведение, несмотря на то, что для круга это невозможно. I - Interface Segregation Principle Много интерфейсов, специально предназначенных для клиентов, лучше, чем один интерфейс общего назначения Требует, чтобы клиенты не зависели от интерфейсов, которые они не используют. Н практике, особенно при плохо продуманной архитектуре, это может вылиться в дробление на очень мелкие интерфейсы из одного-двух методов, чтобы удовлетворить множество клиентов. D - Dependency Inversion Principle Принцип инверсии зависимости Возьмем ситуацию когда объект класса A вызывает методы объекта класса B. Значит зависит от B. С применением этого принципа, объект типа B должен быть инстанциирован вне A и передан как зависимость в A. Без применения этого принципа, это может сделать сам объект A, что во многих случаях гораздо удобнее. Основано на http://www.tonymarston.net/php-mysql/not-so-solid-oo-principles.html. В процессе чтения узнал много нового. Холивар приветствуется )

Ответ 2



Как и любой инструмент, принципы проектирования нужно применять с умом. Можно выделить два случая, когда применение принципов проектирования приведет к увеличению проблем, и не приведет ни к чему хорошему. YAGNI Подробности — в ответе на вопрос «Нарушает ли OCP и DIP (из SOLID) принцип YAGNI?». Принципы проектирования предназначены для смягчения определенной проблемы разработк (да, именно «смягчения», но не решения проблемы), добавляя при этом свои собственные проблемы. Поскольку иногда проблемы программисты придумывают себе сами, то следование (особенн буквальное) принципам проектирования приведет к перекосу дизайна, не решая при этом реальной проблемы. Другими словами, чрезмерное увлечения принципами проектирования может привести переусложненному решению там, где эта сложность не нужна. «over»-SOLID Подробнее в статье «О принципах проектирования». Есть ряд типовых паталогических случаев использования SOLID-принципов: Anti-SRP – Принцип размытой ответственности. Классы разбиты на множество мелких классов, в результате чего логика размазывается по нескольким классам/модулям. Anti-OCP – Принцип фабрики фабрик. Дизайн является слишком обобщенным и расширябельным, выделяется слишком большое число уровней абстракции. Anti-LCP – Принцип непонятного наследования. Принцип проявляется либо в чрезмерно количестве наследования, либо в его полном отсутствии, в зависимости от опыта и взглядов местного главного архитектора. Anti-ISP – Принцип тысячи интерфейсов. Интерфейсы классов разбиваются на слишко большое число составляющих, что делает их неудобными для использования всеми клиентами. Anti-DIP – Принцип инверсии сознания или DI-головного мозга. Интерфейсы выделяютс для каждого класса и пачками передаются через конструкторы. Понять, где находится логика становится практически невозможно.

вторник, 28 мая 2019 г.

Экспорт модели в .xlsx соблюдая SOLID

В БД есть таблица результатов тестирования по русскому языку и математике (SubjectCode=2)
dbo.Result
Во второй таблице находятся просто сведения о школах:
dbo.School

Таких результатов бывает 40.000-70.000. Нужно на выходе получить для каждого тестируемого вот такой отчет в pdf-файле

Мое решение:
Создал Excel-шаблон; Беру данные из базы и заношу их в этот xlsx-шаблон; Сохраняю этот шаблон в pdf-файл; И так для каждого ученика.

LearnerReport.cs
namespace so16092016.Models { public class LearnerReport { public string SNS { get; set; } //Surname Name SecondName public string SchoolName { get; set; } public string ClassName { get; set; } public int TestResult5 { get; set; } } }
Program.cs
using Excel = Microsoft.Office.Interop.Excel;
namespace so16092016 { class Program { static void Main(string[] args) { resultsEntities context = new resultsEntities(); ResultsRepository resultsRepository = new ResultsRepository(context); var ma_results = resultsRepository.GetTList().Where(x => x.SubjectCode == 2); //получить результаты по математике
Excel.Application app = new Excel.Application(); app.DisplayAlerts = false; Excel.Workbook book_template = app.Workbooks.Open(@"шаблон_отчета.xlsx"); Excel._Worksheet sheet_template = book_template.Sheets["отчет"];
foreach(var ob in ma_results) { //1. Создаем объкт LearnerReport из БД LearnerReport report = new LearnerReport { SNS = $"{ob.surname} {ob.name} {ob.SecondName}", SchoolName = ob.SchoolName, ClassName = ob.ClassName, TestResult5 = ob.TestResult5 };
//2. Экспорт объкта LearnerReport в шаблон xlsx sheet_template.Range["C4"].Value2 = report.SNS; sheet_template.Range["C5"].Value2 = report.SchoolName; sheet_template.Range["C6"].Value2 = report.ClassName; sheet_template.Range["C9"].Value2 = report.TestResult5;
//3. Сохраняем полученный файл в .pdf на рабочем столе string file_name = $@"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\{report.SNS}.pdf"; sheet_template.ExportAsFixedFormat(Excel.XlFixedFormatType.xlTypePDF, file_name); }
book_template.Close(0); book_template = null; app.Quit(); app = null; } } }
Необходимо: Приложение работает и дает необходимый результат. Но вы наверное видите, что оно далеко от ООП (SOLID) и как результат очень трудно его "допиливать" и масштабировать. Помогите правильно спроектировать данный механизм формирования подобных отчетов:
логика экспорта модели в .xlsx должна быть у самой модели или необходимо создать отдельный класс-менеджер для этого? какие должны быть модели? как правильно создавать модель-отчета на основе объектов БД? какой порождающий паттерн здесь больше подходит?


Ответ

Конкретно в вашем случай нет необходимость, применять какие либо паттерны, т.к. у вас довольно простая программа и паттерны добавят лишь ненужную сложность (применять паттерны ради паттернов плохая практика). Паттерны сгодятся если вы пишите большие Enterprise приложения, где необходима гибкость и масштабируемость.
Если я правильно понял, то вы хотите отрефакторить программу.
1) Из кода я не понял что вы используете для получения данных из БД, но посоветовал бы использовать какую нибудь ORM, например Entity Framework 6
Создать DbContext и использовать его в репозиториях ResultsRepository и SchoolRepository Все выборки засунуть в соответствующий репозиторий например
class ResultsRepository { // возвращает все результаты public IEnumerable GetResults()
// возвращает все результаты по заданому предмету. public IEnumerable GetResults(Subject subject)
// возвращает все результаты для заданой школы. public IEnumerable GetResults(int idSchool)
// возвращает все результаты для заданой школы по заданому предмету. public IEnumerable GetResults(int idSchool, Subject subject) }
class SchoolRepository { // возвращает список всех школ. public IEnumerable GetSchools() }
2) Модель данных приблизительно выглядит так
class Result { [Key] public Guid Id { get; set; }
public string Surname { get; set; } public string Name { get; set; } public string SecondName { get; set; }
[NotMapped] public string SNS => $"{Surname} {Name} {SecondName}";
public School School { get; set; }
public string ClassName { get; set; }
public Subject Subject { get; set; }
public int TestResult5 { get; set; } }
class School { [Key] public int Id { get; set; } public string Name { get; set; } }
enum Subject { RussianLanguage = 1, Mathematics = 2 }
3) Можно создать отдельный класс для работы отчётами
class LearnerReportManager { public Excel._Worksheet CreateReport(Result result)
public SaveReport(Excel._Worksheet reportWorksheet, string fileName) }
4) Для формирования Excel отчёта луче используйте EPPlus или более крутой File Formats от Syncfusion (у них есть бесплатная версия). вы так не будите зависеть от установленного MS Office.
5) Если кол. формируемых отчетов за раз от 40 до 70 тыс., то было бы не плохо добавить многопоточное создание отчетов, это существенно уменьшит время создания отчётов.

понедельник, 18 марта 2019 г.

Принцип открытости/закрытости при динамическом определении типа файла

Есть код приложения в котором необходимо динамически определить тип файла (HTML или TXT) по содержимому и в зависимости от полученного типа файла вызвать соответствующий алгоритм обработки.
interface IFileType { void Process(); } class HtmlFile : IFileType { public void Process() { Console.WriteLine("HTML"); } } class TxtFile : IFileType { public void Process() { Console.WriteLine("TXT"); } } class FileTypeHandler { public static IFileType Define(string fileContent) { var file = fileContent.IndexOf("class Program { static void Main(string[] args) { FileProcessor fileProcessor = new FileProcessor(); fileProcessor.ProcessFile(@"d:\index.html"); Console.ReadKey(); } }
Вывод:
HTML
Все работает как мне надо. Меня интересует насколько гибок мой код к появлению в будущем новых типов файлов, к примеру JSON. Ведь с появлением нового класса реализующего интерфейс IFileType, также изменится алгоритм определения типа по содержимому в классе FileTypeHandler.
Все ли я правильно спроектировал, касательно принципа открытости/закрытости?


Ответ

Я вижу следующие проблемы в вашем коде.
Метод FileTypeHandler содержит логику определения, для всех форматов. На данном этапе это не страшно, т.к. их не много, но если их количество будет увеличиваться, метод станет запутанный и тяжел в расширении.
Рекомендую, создать сущности, для каждого формата, и в них реализовывать определение. Это позволит избежать запутанности и облегчит добавление новых.
Приведу код, как я бы в данной ситуации реализовал. В нем есть небольшие упрощения. Написал, на java, но думаю вы разберетесь.
enum FormatTypes { TXT("txt", new TxtDetector()), HTML("html", new HtmlDetector());
public final String name; private final Detector detector;
FormatTypes(String name, Detector detector) { this.name = name; this.detector = detector; } }
interface Detector { boolean isCorrectType(String fileName, String content); }
class TxtDetector implements Detector { }
class HtmlDetector implements Detector { }
class DetectorHandler { public static String getType(String fileName) { String content = //чтение содержимого
for (FormatTypes types : FormatTypes.values()) if (types.detector.isCorrectType(fileName, content)) //просто возвращает название формата, //можно при необходимости в FormatTypes положить какую то логику return types.name;
throw new IllegalArgumentException("type is not supported"); } }

среда, 13 марта 2019 г.

Трактовка принципа Открытости-Закрытости

Пример с дополнением интерфейса:
class Playback { private Media current;
public void play(Media media) { current.stop(); current = media; current.play(); }
public void next() { Media media // получение следующей по какому либо алгоритму play(media); }
public void prev() { Media media // получение предыдущей по какому либо алгоритму play(media); } }
Пример с добавлением классов операций:
class Playback { private Media current;
public Media current() { return current; }
public void play(Media media) { current.stop(); current = media; current.play(); } }
abstract class PlayOperation { private Playback playback;
public void execute() { Media media = provide(); playback.play(media); }
protected abstract Media provide(); }
class NextOperation extends PlayOperation { public Media provide() { Media media // получение следующей по какому либо алгоритму return media; } }
class PrevOperation extends PlayOperation { public Media provide() { Media media // получение предыдущей по какому либо алгоритму return media; } }
class RandomOperation extends PlayOperation { private Random random;
public Media provide() { Media media // получение рандомной return media; } }
Пример с новыми классами выглядит привлекательнее, а стоит ли оно того?


Ответ

Пример с добавлением классов операций
То что вы описали в примере, это почти шаблон проектирования "Посетитель" . NextOperation , PrevOperation , RandomOperation - посетители класса Playback . В данном случае этот шаблон вряд ли обоснован, так как он призван добавлять поведение иерархии классов, и он действительно при работе с иерархией играет на OCP (т.к. необходимость добавления поведения в дерево наследников с помощью добавления метода в базовый класс как-раз нарушит OCP). У вас же класс один Playback - OCP не будет нарушено в первом случае - Playback открыт для расширения наследованием (если конечно поменять private на protected), и закрыт для изменения (это значит, что переписывать его вам не требуется для доработки поведения в наследниках).
Нельзя не увидеть минус второго подхода - вы серьёзно усложняете систему, размазывая простую логику на множество классов ( это Anti-SRP ). Поэтому я бы посоветовал оставить первый вариант, как минимум, когда система простая - так усложнять её нельзя, YAGNI против.
Но если Playback становится базовым классом иерархии - использование посетителей будет уже обосновано, и следует добавить метод visit(PlayOperation operation), при чём возможность посещения Playback - не значит что метод next(), например, обязательно должен быть вынесен в класс-посетитель: в посетителей лучше выносить дополнительные операции, а основные - оставлять внутри класса.

среда, 19 декабря 2018 г.

Может ли нарушаться принцип подстановки Лисков при использовании интерфейса/абстрактного класса?

На размышления меня натолкнула вот эта статья: http://blog.byndyu.ru/2009/10/blog-post_29.html Приведу немного переработанный пример из нее:
public interface IList {
public void add(int e);
}
public class List implements IList{
@Override public void add(int e) { // добавляет элемент }
}
public class DoubleList implements IList{
@Override public void add(int e) { // добавляет элемент // добавляет элемент }
}
Метод add в классе DoubleList добавляет элемент дважды. В клиентском коде можно написать примерно так:
public void LSPTest(IList list){ int oldLen = list.getLength();
list.add(1);
int newLen = list.getLength();
if(newLen - oldLen == 1){ // делать что то полезное } }
Очевидно, что поведение программы будет разным, в зависимости от того, получит функция LSPTest объект класса List или DoubleList. Но разве это нарушает LSP? Ведь DoubleList наследует не класс List, а интерфейс IList. Интерфейс не может задавать никаких предусловий и постусловий (в данном случае точно не задает). И интерфейсы же для того и написаны, что бы иметь разные реализации, иногда имеющие совсем мало общего. Я всегда считал, что если бы, например, DoubleList был наследован от List, то выделение интерфейса и опускание классов на один уровень - это как раз решение проблемы при нарушении LSP. По-моему это называется факторизация. И к тому же, LSP говорит о том, что прогрмма не должна меняться, если вместо объекта базового класса подставить объект производного. Но как можно подставить что то вместо объекта базового класса, если базовый класс является абстрактным? Или тем более интерфейсом? Вместо него нельзя ничего подставить, потому что его просто нельзя создать.


Ответ

Интерфейс не может задавать никаких предусловий и постусловий
Формально вы правы. Объявление интерфейса само по себе не задает "материального" (назовем это так) контракта -- т.е. контракта, который может быть проверен на этапе компиляции или выполнения. Т.е. нет никаких средств, гарантирующих, что все классы, реализующие интерфейс, будут реализовывать его одинаково с т.з. LSP.
Хитрость заключается в том, что при использовании интерфейсов мы всегда говорим о "нематериальном" контракте. Он выражается в названии методов и в комментариях. Это такой неформальный уговор среди разработчиков. В случае с IList из .NET это выглядит так*:
// Adds an item to the list. The exact position in the list is // implementation-dependent, so while ArrayList may always insert // in the last available location, a SortedList most likely would not. // The return value is the position the new element was inserted in. int Add(Object value);
Как видно из комментария, если класс-наследник будет добавлять в список сразу два элемента, он нарушит два пункта из комментария: что добавлять должен один элемент и что метод возвращает позицию добавленного элемента (а что возвращать в случае двух элементов?).
В то же время, комментарии к методу ICollection.Add() ничего не говорят о том, что этот метод предполагает делать. И, как мы видим, это согласуется с тем, что, например, HashSet (реализующий ICollection) после двух вызовов метода Add() с одинаковыми аргументами будет содержать лишь один элемент.
Это тонкий лед, т.к., повторюсь, нет средств для обеспечения выполнения обозначенного контракта -- это лежит целиком на совести программиста. Более того, интерфейс может и не продоставлять никаких комментариев и о его контракте придется как-то догадываться -- из документации, интернета, от коллег.
Но тем не менее 99.9% разработчиков, увидев название интерфейса IList, будут ожидать, что метод Add() добавит в список один элемент. Если же вдруг этот метод будет добавлять два элемента, не добавлять ничего, или даже удалять, это очень удивит эти 99.9%. В этом, собственно, и заключается нарушение LSP.
Если вы пока не очень понимаете LSP, то забудьте на время про интерфейсы и посмотрите на примеры использования наследования классов. Например, на классическую проблему прямоугольника и квадрата

*желающим попудрить себе мозг читать ниже.
Метод IList.Add(), как мы уже видели, явно в комментарии обозначает свой контракт. А что насчет IList? Как видно, он наследуются от ICollection, а это значит, что на метод Add() не налагается ограничений. Т.о. класс DoubleList : IList формально уже не будет нарушать LSP. Но бьюсь об заклад, ваши коллеги не будут рады такой реализации :).