Статьи

Проектно-ориентированные проектные решения: классы с сохранением состояния или без состояния?

Мы знаем, что такое объектно-ориентированное программирование. Но иногда нам нужно тратить больше времени, чтобы решить, какие свойства необходимы для определенного класса. В противном случае мы пострадаем на более позднем этапе, если класс имеет неправильные атрибуты состояния. Здесь мы собираемся обсудить, какой тип класса должен быть с состоянием, а какой без состояния.

Что означает состояние объекта

Прежде чем обсуждать классы без состояния или с состоянием, мы должны лучше понять, что подразумевается под состоянием объекта. Это то же самое, что в английском означает «определенное состояние, в котором кто-то или что-то находится в определенное время» государства.

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

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

Можем ли мы привести некоторые правила, какие свойства необходимы? Другими словами, какие свойства должно включать состояние объекта? Или объекты должны всегда оставаться без состояния? Ниже приведены некоторые мысли по этому вопросу.

Классы сущностей / бизнес-объекты

Существует несколько имен, таких как классы сущностей, бизнес-объекты и т. Д., Присвоенных классам, которые представляют ясное состояние чего-либо. Если мы возьмем пример класса Employee, его единственная цель — сохранить состояние сотрудника. Что может содержать это состояние? EmpId, Компания, Обозначение, JoinedDate и т. Д … Я надеюсь, что на этом этапе не будет путаницы. Все согласны с тем, что этот тип класса должен быть с учетом состояния без особых аргументов, потому что этому учат в колледже.

Но как нам сделать расчет зарплаты?

  • Должен ли CalculateSalary()быть метод внутри класса Employee?
  • Должен ли быть SalaryCalculatorкласс, и этот класс должен содержать метод Calculate ()?
  • Если есть SalaryCalculatorкласс:

    • Должны ли они иметь такие свойства, как BasicPay, DA HRA и т. Д.?
    • Или Employeeобъект должен быть закрытой переменной-членом в том SalaryCalculator, который вводится через конструктор?
    • Или следует  SalaryCalculator выставить публичное свойство Employee ( Get&Set Employeeметоды в Java)?

Классы Помощник / Операция / Манипулятор

Это тип класса, который выполняет задачу. SalaryCalculator попадает в этот тип. Существует много имен для этого типа, где классы выполняют действия и могут быть найдены в программах со многими префиксами и суффиксами, такими как

  • класс, SomethingCalculatorнапример: SalaryCalculator
  • класс, SomethingHelperнапример: DB Helper
  • класс,  SomethingControllerнапример: контроллер БД
  • учебный класс SomethingManager
  • учебный класс SomethingExecutor
  • учебный класс SomethingProvider
  • учебный класс SomethingWorker
  • учебный класс SomethingBuilder
  • учебный класс SomethingAdapter
  • учебный класс SomethingGenerator

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

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

Гибридные классы

Согласно Википедии, инкапсуляция в объектно-ориентированном программировании — это «… упаковка данных и функций в один компонент». Означает ли это, что все методы, которые манипулируют этим объектом, должны присутствовать в классе сущности? Я так не думаю. Класс объекта может иметь государственные методы доступа , такие как GetName(), SetName(), GetJoiningDate, и GetSalary()т.д. …

Но CalculateSalary()должен быть снаружи. Почему это так?

Согласно принципу SOLID — Single Responsibility : «класс должен меняться только по одной причине». Если мы сохраним CalculateSalary()метод внутри класса Employee, этот класс изменится по любой из следующих двух причин (что, таким образом, нарушает SRP):

  • Изменение состояния в классе Employee, например: новое свойство было добавлено в Employee
  • В логике расчета произошли изменения

Я надеюсь, что это понятно. Теперь у нас есть два класса в этом контексте: класс Employee и класс SalaryCalculator. Как они соединяются друг с другом? Есть несколько способов. Один из них заключается в создании объекта класса SalaryCalculator внутри метода GetSalary и вызове Calculate () для установки переменной salary класса Employee. Если мы это сделаем, он станет гибридным, потому что он действует как класс сущностей и инициирует работу как вспомогательный класс. Я действительно не поощряю этот тип гибридного класса. Но в ситуациях, таких как метод сущности Save, это нормально с некоторым делегированием операции.

Основное правило: «Всякий раз, когда вы чувствуете, что ваш класс попадает в эту гибридную категорию, подумайте о рефакторинге. Если вы чувствуете, что ваши классы не попадают ни в одну из вышеуказанных категорий, прекратите кодирование».

Состояние в классе помощника / манипулятора

В чем проблема, если наши вспомогательные классы сохраняют состояние? Перед этим давайте рассмотрим, какие комбинации значений состояний SalaryCalculatorможет принимать класс. Ниже приведены несколько примеров:

Сценарий 1 — Примитивные Ценности

   class SalaryCalculator
    {
        public double Basic { get; set; }
        public double DA { get; set; }
        public string Designation { get; set; }

        public double Calculate()
        {
            //Calculate and return
        }
    }

Cons

Существует вероятность того, что базовый оклад может быть «бухгалтером», а назначение может быть «директором», что совсем не соответствует. Не существует принудительного способа убедиться, что SalaryCalculator может работать независимо.

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

Сценарий 2 — Объект как государство

    class SalaryCalculator
    {
        public Employee Employee { get; set; }

        public double Calculate()
        {
            //Calculate and return
        }
    }

Cons

Если один объект SalaryCalculator используется двумя потоками и каждый поток предназначен для отдельного сотрудника, последовательность выполнения может быть следующей, что вызывает логические ошибки:

  • Поток 1 устанавливает employee1объект
  • Поток 2 устанавливает employee2объект
  • Поток 1 вызывает Calculateметод и получает Salaryзаemployee2

Мы можем утверждать, что Employeeзависимость может быть введена через конструктор и сделать свойство доступным только для чтения. Затем нам нужно создать SalaryCalculatorобъекты для каждого объекта сотрудника. Лучше:  не создавайте свои вспомогательные классы таким образом.

Сценарий 3 — Нет государства

    class SalaryCalculator
    {
        public double Calculate(Employee input)
        {
            //Calculate and return
        }
    }

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

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

    interface ISalaryCalculator
    {
        double Calculate(Employee input);
    }
    class SimpleSalaryCalculator:ISalaryCalculator
    {
        public virtual double Calculate(Employee input)
        {
            return input.Basic + input.HRA;
        }
    }
    class TaxAwareSalaryCalculator : SimpleSalaryCalculator
    {
        public override double Calculate(Employee input)
        {
            return base.Calculate(input)-GetTax(input);
        }
        private double GetTax(Employee input)
        {
            //Return tax
            throw new NotImplementedException();
        }
    }

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

Ниже приведен способ использования Calculatorкласса (классов).

    class SalaryCalculatorFactory
    {
        internal static ISalaryCalculator GetCalculator()
        {
            // Dynamic logic to create the ISalaryCalculator object
            return new SimpleSalaryCalculator();
        }
    }
    class PaySlipGenerator
    {
        void Generate()
        {
            Employee emp = new Employee() { };
            double salary =SalaryCalculatorFactory.GetCalculator().Calculate(emp);
        }
    }

Класс Factory инкапсулирует логику принятия решения, какой дочерний класс будет использоваться. Он может быть статическим, как указано выше, или динамическим с использованием отражения. Поскольку причиной изменения в этом классе является создание объекта, мы не нарушаем «Принцип единой ответственности».

В случае, если вы собираетесь в гибридный класс, вы можете вызвать расчет из Employee.Salaryсвойства или Employee.GetSalary()как показано ниже.

    class Employee
    {
        public string Name { get; set; }
        public int EmpId { get; set; }
        public double Basic { get; set; }
        public double HRA { get; set; }

        public double Salary
        {
            //NOT RECOMMENDED 
            get{return SalaryCalculatorFactory.GetCalculator().Calculate(this);}
        }
    }

Заключение

«Не кодируйте, когда мы думаем. Не думайте, когда мы кодируем». Этот принцип даст нам достаточно свободы, чтобы думать, должен ли класс быть лицом без гражданства или государством. Если с состоянием, вот что должно показать состояние объекта:

  • Сделайте классы сущностей с состоянием.
  • Классы Помощник / Операция должны быть без состояний.
  • Убедитесь, что классы Helper не являются статичными.
  • Даже если есть гибридный класс, убедитесь, что он не нарушает SRP.
  • Потратьте некоторое время на дизайн класса, прежде чем кодировать. Покажите диаграмму классов 2-3 программистам и узнайте их мнение.
  • Назовите класс с умом. Имена помогут нам решить состояние. Там нет жесткого правила для именования. Ниже приведены некоторые из следующих:

    • Классы сущностей должны быть названы с существительными, которые представляют тип объекта — например: Employee
    • Имена классов помощника / рабочего должны отражать, что это рабочий. например: SalaryCalculatorи PaySlipGeneratorт. д.
    • Глагол никогда не должен использоваться в качестве имени класса — например: класс CalculateSalary{}

Точки интереса

  • Большинство классов, которые мы пишем, попадают в гибридную категорию, которая нарушает SRP. Пожалуйста, прокомментируйте, если есть какой-либо сценарий, который не может быть закодирован без гибридных классов