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

93

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

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

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

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

  • "Отчаяния конструктора с длинными списками параметров"
  • Шаблон Builder

Спасибо за ваш вклад!

  • 5
    Я думаю, что шаблон построения является лучшим и наиболее стандартным решением для этого.
  • 0
    @Zachary: То, что вы защищаете, работает только для особой формы шаблона строителя, как объяснил здесь Джошуа Блох: drdobbs.com/java/208403883?pgno=2 Я предпочитаю использовать связанный термин "свободный интерфейс", чтобы вызвать это «особая форма шаблона строителя» (см. мой ответ).
Теги:
oop
immutability

9 ответов

78
Лучший ответ

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

Я думаю, что вам поможет удобный интерфейс 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 );
}
  • 11
    @ all: пожалуйста, не жалуйтесь на постфикс "Impl" в "FooImpl": этот класс скрыт внутри фабрики, и никто никогда его не увидит, кроме человека, пишущего на свободном интерфейсе. Все, что волнует пользователя, это то, что он получает «Foo». Я мог бы также назвать "FooImpl" "FooPointlessNitpick";)
  • 5
    Чувствуете упреждающий? ;) Вы были придирчивы в прошлом по этому поводу. :)
Показать ещё 13 комментариев
60

В 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))
  • 31
    +1 за изобретение языка Scala. Да, это злоупотребление системой репутации, но ... ооо ... Я так сильно люблю Скала, что мне пришлось это сделать. :)
  • 1
    О, чувак ... Я только что ответил что-то почти идентичное! Ну, я в хорошей компании. :-) Интересно, я раньше не видел твоего ответа ... <пожимает плечами>
21

Хорошо, рассмотрим это на 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.

11

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

Вариант 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 рамки.

  • 3
    Вариант 1 умный (но не слишком умный), поэтому мне это нравится.
  • 4
    Я делал это раньше, но, на мой взгляд, это далеко от того, чтобы быть хорошим решением, потому что объект все еще изменчив, только мутационные методы «скрыты». Может быть, я слишком разборчив в этом вопросе ...
Показать ещё 1 комментарий
6

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

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

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

  • 1
    Downvotes? Кто-нибудь хочет оставить комментарий о том, почему это не хорошее решение?
  • 0
    +1. Может быть, кто-то отрицал это, потому что вы намекаете на использование clone() для получения новых экземпляров.
Показать ещё 2 комментария
5

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

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

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

  • 0
    Предполагаемый недостаток - создание большего количества экземпляров, чем необходимо - на самом деле не такая уж большая проблема. Распределение объектов очень дешево, как и сборка мусора недолговечных объектов. Когда escape-анализ включен по умолчанию, этот вид «промежуточных объектов», скорее всего, будет распределен по стеку и будет стоить буквально ничего.
  • 2
    @ Gustafc: Да. Cliff Click однажды рассказал о том, как они тестировали симуляцию Clojure Ant Colony от Rich Hickey на одном из своих больших блоков (864 ядра, 768 ГБ ОЗУ): 700 параллельных потоков, работающих на 700 ядрах, каждый на 100%, генерируя более 20 ГБ эфемерный мусор в секунду . GC даже не вспотел.
5

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

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), где у вас много маленьких неизменяемых объектов, а предпочтительный шаблон - получать новостные из старых.

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

  • 6
    Это шаблон строителя (вариант 2)
  • 0
    Разве это не был бы фабричный («строительный») объект, который испускает его неизменные объекты?
Показать ещё 2 комментария
4

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

  • 1
    примечание: пробег для этого ответа может варьироваться в зависимости от проблемы, базы кода и навыков разработчика.
1

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

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:
Перепроектируйте свой класс, чтобы не требовалось такое огромное количество параметров. Вы могли бы разделить свои респонсоры на несколько классов. Или передавать параметры не конструктору, а только определенным методам по требованию. Не всегда жизнеспособный, но когда это так, стоит делать.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню