Почему не стало обычным делом использовать сеттеры в конструкторе?

23

Средства доступа и модификаторы (также известные как сеттеры и геттеры) полезны по трем основным причинам:

  1. Они ограничивают доступ к переменным.
    • Например, переменная может быть доступна, но не может быть изменена.
  2. Они проверяют параметры.
  3. Они могут вызвать некоторые побочные эффекты.

Университеты, онлайн-курсы, учебные пособия, статьи в блогах и примеры кода в Интернете - все подчеркивают важность методов доступа и модификаторов, в настоящее время они почти чувствуют себя необходимыми для кода. Таким образом, их можно найти, даже если они не предоставляют никаких дополнительных значений, как в приведенном ниже коде.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Тем не менее, очень часто можно найти более полезные модификаторы, которые действительно проверяют параметры и выдают исключение или возвращают логическое значение, если введен неверный ввод, что-то вроде этого:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

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

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Но можно подумать, что этот второй подход намного безопаснее:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Видите ли вы похожую модель в своем опыте или мне просто не повезло? И если вы это сделаете, то как вы думаете, что вызывает это? Есть ли очевидный недостаток в использовании модификаторов из конструкторов или они просто считаются более безопасными? Это что-то еще?

Влад Спрейс
источник
1
@ STG, спасибо, интересный момент! Не могли бы вы рассказать об этом и привести в ответ пример плохой вещи, которая может случиться?
Влад Спрейс
8
@stg На самом деле, использование шаблона в конструкторе является недопустимым, и многие инструменты качества кода будут указывать на это как на ошибку. Вы не знаете, что будет делать переопределенная версия, и она может делать такие неприятные вещи, как позволить «this» сбежать до завершения работы конструктора, что может привести к разным странным проблемам.
Михал Космульский
1
@gnat: Я не верю, что это дубликат. Должны ли методы класса вызывать свои собственные методы получения и установки? из-за природы вызовов конструктора, вызываемых для объекта, который сформирован не полностью.
Грег Бургхардт
3
Также обратите внимание, что ваша «проверка» просто оставляет возраст не инициализированным (или ноль, или ... что-то еще, в зависимости от языка). Если вы хотите, чтобы конструктор дал сбой, вы должны проверить возвращаемое значение и сгенерировать исключение при сбое. В действительности ваша проверка только что внесла другую ошибку.
бесполезно

Ответы:

37

Очень общие философские рассуждения

Как правило, мы просим, ​​чтобы конструктор предоставил (в качестве постусловий) некоторые гарантии о состоянии построенного объекта.

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

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

Языки также различаются по тому, как они разрешают вызовы методов экземпляра, которые наследуются от базовых классов / переопределяются подклассами, пока конструктор все еще работает. Это добавляет еще один уровень сложности.

Конкретные примеры

  1. Ваш собственный пример того, как вы думаете, это должно выглядеть, сам по себе неверен:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    это не проверяет возвращаемое значение из setAge. Видимо, вызов сеттера не является гарантией правильности в конце концов.

  2. Очень простые ошибки, например, в зависимости от порядка инициализации, такие как:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    где моя временная регистрация сломала все. Упс!

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

Простое предложение

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

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

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

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

И, наконец, для полной полноты, самоутверждающаяся оболочка в C ++. Обратите внимание, что хотя он все еще выполняет утомительную проверку, поскольку этот класс больше ничего не делает , его относительно легко проверить.

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

Хорошо, это не совсем завершено, я пропустил различные конструкторы и назначения копирования и перемещения.

Бесполезный
источник
4
Сильно недооцененный ответ! Позвольте мне добавить только то, что ОП видел описанную ситуацию в учебнике, не делает его хорошим решением. Если в классе есть два способа установить атрибут (с помощью конструктора и установщика), и кто-то хочет наложить ограничение на этот атрибут, оба способа должны проверить ограничение, иначе это ошибка проектирования .
Док Браун
Так что вы могли бы рассмотреть использование методов сеттера в конструкторе с плохой практикой, если 1) нет логики в установщиках или 2) логика в установщиках не зависит от состояния? Я делаю это не часто, но лично я использовал методы сеттера в конструкторе именно по последней причине (когда в
сеттере
Если есть какой - то тип условия , которые должны всегда применяться, то есть логика в инкубаторе. Я, конечно, думаю, что лучше по возможности закодировать эту логику в члене, поэтому она вынуждена быть автономной и не может приобретать зависимости от остальной части класса.
бесполезно
13

Когда объект создается, он по определению не полностью сформирован. Рассмотрим, как вы установили сеттер:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Что, если часть проверки setAge()включала проверку ageсвойства, чтобы гарантировать, что объект может только возрастать? Это условие может выглядеть так:

if (age > this.age && age < 25) {
    //...

Это кажется довольно невинным изменением, поскольку кошки могут перемещаться во времени только в одном направлении. Единственная проблема заключается в том, что вы не можете использовать его для установки начального значения, this.ageпоскольку оно предполагает, что оно this.ageуже имеет допустимое значение.

Отказ от сеттеров в конструкторах проясняет, что единственное , что делает конструктор, это устанавливает переменную экземпляра и пропускает любое другое поведение, которое происходит в сеттере.

Калеб
источник
Хорошо ... но, если вы на самом деле не тестируете конструкцию объекта и не обнаруживаете потенциальные ошибки, в любом случае , есть потенциальные ошибки . И теперь у вас есть две две проблемы: у вас есть та скрытая потенциальная ошибка, и вы должны провести проверку возрастного диапазона в двух местах.
svidgen
Нет проблем, так как в Java / C # все поля обнуляются перед вызовом конструктора.
Дедупликатор
2
Да, вызывать методы экземпляра из конструкторов сложно, и это обычно не является предпочтительным. Теперь, если вы хотите переместить свою логику проверки в закрытый, финальный класс / статический метод и вызвать его как из конструктора, так и из установщика, это было бы хорошо.
бесполезно
1
Нет, методы неэкземпляра избегают хитрости методов экземпляра, потому что они не касаются частично созданного экземпляра. Вам, конечно же, все еще нужно проверить результат проверки, чтобы решить, была ли конструкция успешной.
бесполезно
1
В этом ответе ИМХО не хватает смысла. «Когда объект конструируется, он по определению не полностью сформирован» - конечно, в середине процесса построения, но когда конструктор завершен, должны быть сохранены любые намеренные ограничения на атрибуты, в противном случае можно обойти это ограничение. Я бы назвал это серьезным недостатком дизайна.
Док Браун
6

Здесь уже есть несколько хороших ответов, но никто до сих пор не заметил, что вы спрашиваете здесь две вещи в одной, что вызывает некоторую путаницу. ИМХО ваш пост лучше всего рассматривать как два отдельных вопроса:

  1. если в установщике есть проверка ограничения, не должно ли оно быть в конструкторе класса, чтобы убедиться, что оно не может быть обойдено?

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

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

Однако ответом на второй вопрос обычно является «нет», если вы не принимаете меры, чтобы избежать переопределения метода установки в производном классе. Следовательно, лучшая альтернатива - реализовать проверку ограничения в частном методе, который можно повторно использовать как из установщика, так и из конструктора. Я не собираюсь здесь повторять пример @Useless, который показывает именно этот дизайн.

Док Браун
источник
Я до сих пор не понимаю, почему лучше извлечь логику из сеттера, чтобы конструктор мог ее использовать. ... Чем это действительно отличается от простого вызова сеттеров в разумном порядке ?? Особенно если учесть, что если ваши сеттеры настолько просты, насколько они «должны» быть, единственная часть сеттера, которую ваш конструктор на данный момент не вызывает, - это фактическая установка базового поля ...
svidgen
2
@svidgen: см. пример JimmyJames: короче говоря, не рекомендуется использовать переопределенные методы в ctor, это вызовет проблемы. Вы можете использовать сеттер в ctor, если он финальный и не вызывает никаких других переопределенных методов. Более того, отделение проверки от способа ее обработки в случае неудачи дает вам возможность обрабатывать ее по-разному в ctor и в установщике.
Док Браун
Ну, теперь я еще менее убежден! Но, спасибо, что указал мне на другой ответ ...
svidgen
2
Под разумным порядком подразумевается хрупкая, невидимая зависимость порядка, легко нарушаемая невинно выглядящими изменениями , верно?
бесполезно
@ бесполезно, ну нет ... Я имею в виду, забавно, что вы предлагаете, чтобы порядок выполнения вашего кода не имел значения. но это было не совсем так. Помимо надуманных примеров, я просто не могу вспомнить пример из моего опыта, когда я думал, что было бы неплохо иметь валидацию перекрестных свойств в установщике - такого рода вещи обязательно приводят к «невидимым» требованиям порядка вызова , Независимо от того, происходят ли эти вызовы в конструкторе ..
svidgen
2

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

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

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

Я думаю, что основная причина, по которой это не стало распространенным, заключается в том, что он делает код немного сложнее для понимания. Лично у меня почти никогда нет методов установки, и мои переменные-члены почти неизменно (каламбур) конечны. Из-за этого значения должны быть установлены в конструкторе. Если вы используете неизменяемые объекты (и для этого есть много веских причин), вопрос будет спорным.

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

JimmyJames
источник
Как это вообще проблема использования сеттеров в конструкторе вместо проблемы плохо реализованного наследования ??? Другими словами, если вы фактически лишаете законной силы базовый конструктор, зачем вам это даже называть !?
svidgen
1
Я думаю, дело в том, что переопределение сеттера не должно делать конструктор недействительным.
Крис Волерт
@ChrisWohlert Хорошо ... но, если вы измените свою логику сеттера так, чтобы она конфликтовала с логикой конструктора, это проблема независимо от того, использует ли ваш конструктор сеттер. Либо он терпит неудачу во время строительства, либо терпит неудачу позже, либо происходит невидимо ( поскольку вы обошли свои бизнес-правила).
svidgen
Вы часто не знаете, как реализован Конструктор базового класса (он может быть где-то в сторонней библиотеке). Однако переопределение переопределяемого метода не должно нарушать контракт базовой реализации (и, возможно, этот пример делает это, не устанавливая nameполе).
Халк
@svidgen проблема здесь в том, что вызов переопределенных методов из конструктора уменьшает полезность их переопределения - вы не можете использовать какие-либо поля дочернего класса в переопределенных версиях, потому что они могут еще не инициализироваться. Что еще хуже, вы не должны передавать thisссылку на какой-либо другой метод, потому что вы не можете быть уверены, что он полностью инициализирован.
Халк
1

Это зависит!

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

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

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


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

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

Итак, я бы сказал, что это конкретное решение не должно быть стремительным и догматичным.

svidgen
источник