Создание больших, неизменяемых объектов без использования конструкторов с длинными списками параметров

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

Это не правильно, это трудно использовать, и читаемость страдает.

Еще хуже, если поля представляют собой некий тип коллекции, например списки. Простой addSibling(S s) сильно облегчит создание объекта, но сделает объект изменчивым.

Что вы, ребята, используете в таких случаях?

Я использую Scala и Java, но я думаю, что проблема не зависит от языка, если язык является объектно-ориентированным.

Решения, которые я могу придумать:

  1. "Конструктор мерзости с длинными списками параметров"
  2. Образец Строителя

Ответ 1

Ну, вы хотите, чтобы как простой, так и неизменный объект был создан после создания?

Я думаю, что вам поможет удобный интерфейс CORRECTLY DONE.

Это будет выглядеть так (чисто составленный пример):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

Я написал "ПРАВИЛЬНО СОВЕРШЕННО" жирным шрифтом, потому что большинство программистов Java плохо понимают интерфейсы и загрязняют их объект с помощью метода, необходимого для создания объекта, что, конечно, совершенно неверно.

Фокус в том, что только метод build() фактически создает Foo (следовательно, Foo может быть неизменным).

FooFactory.create(), гдеXXX (..) и сXXX (..) все создают "что-то еще".

Что-то еще может быть FooFactory, вот один из способов сделать это....

Вы FooFactory выглядели бы так:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

Ответ 2

В Scala 2.8 вы можете использовать именованные параметры и параметры по умолчанию, а также метод copy для класса case. Вот пример кода:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

Ответ 3

Хорошо, рассмотрим это на Scala 2.8:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Конечно, это имеет свою долю проблем. Например, попробуйте сделать espouse и Option[Person], а затем получить двух человек, состоящих в браке друг с другом. Я не могу придумать способ решить эту проблему, не прибегая к конструктору private var и/или private плюс factory.

Ответ 4

Вот еще несколько вариантов:

Вариант 1

Сделать реализацию самой изменчивой, но отделить интерфейсы, которые она предоставляет, к изменяемым и неизменяемым. Это взято из дизайна библиотеки Swing.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

Вариант 2

Если ваше приложение содержит большой, но предопределенный набор неизменяемых объектов (например, объекты конфигурации), вы можете рассмотреть возможность использования Spring рамки.

Ответ 5

Это помогает вспомнить, что различные виды неизменности. Для вашего случая я думаю, что неизменность "popsicle" будет работать очень хорошо:

Непостоянство попсов: это то, что я причудливо называют небольшое ослабление постоянство однократной записи. Можно было бы представить объект или поле, которое некоторое время оставался изменчивым во время его инициализации, а затем "заморожены" навсегда. Этот вид неизменность особенно полезна для неизменяемых объектов, которые циркулярно ссылаться друг на друга или неизменно объекты, которые были сериализованы для диска и при десериализации необходимо быть "текучим" до тех пор, пока процесс десериализации осуществляется по которые указывают, что все объекты могут быть замороженная.

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

Ответ 6

Рассмотрим четыре возможности:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Для меня каждый из 2, 3 и 4 адаптирован к ситуации разницы. Первый из них трудно любить по причинам, указанным OP, и, как правило, является симптомом дизайна, который страдал некоторой ползучестью и нуждается в некотором рефакторинге.

То, что я перечисляю как (2), хорошо, когда нет состояния < factory ', тогда как (3) является выбором выбора, когда есть состояние. Я считаю себя использующим (2), а не (3), когда я не хочу беспокоиться о потоках и синхронизации, и мне не нужно беспокоиться о том, чтобы амортизировать какую-то дорогостоящую настройку над производством многих объектов. (3), с другой стороны, вызывается, когда реальная работа переходит в конструкцию factory (настройка из SPI, чтение конфигурационных файлов и т.д.).

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

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

Ответ 7

Вы также можете заставить объекты неизменяемых объектов выставлять методы, которые выглядят как мутаторы (например, addSibling), но позволяют им возвращать новый экземпляр. Это то, что делают неизменные коллекции Scala.

Недостатком является то, что вы можете создать больше экземпляров, чем необходимо. Это также применимо только тогда, когда существуют промежуточные допустимые конфигурации (например, некоторые node без братьев и сестер, которые в большинстве случаев подходят), если вы не хотите иметь дело с частично построенными объектами.

Например, край графа, который еще не имеет адресата, не является допустимым графом графа.

Ответ 8

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

Ответ 9

Я использую С#, и это мои подходы. Рассмотрим:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Вариант 1. Конструктор с необязательными параметрами

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Используется как, например, new Foo(5, b: new Bar(whatever)). Не для Java или С# версий до 4.0. но все же стоит показать, так как это пример того, как не все решения являются языковыми агностиками.

Вариант 2. Конструктор, принимающий единственный объект параметра

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Пример использования:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

С# from 3.0 on делает это более элегантным с синтаксисом инициализатора объекта (семантически эквивалентным предыдущему примеру):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

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