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


96

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

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

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

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

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

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

  1. «Конструкторские мерзости с длинными списками параметров»
  2. Паттерн Строитель

5
Я думаю, что шаблон строитель - лучшее и стандартное решение этой проблемы.
Захари Райт

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

Ответы:


76

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

Я думаю, что свободный интерфейс ПРАВИЛЬНО СДЕЛАНО вам поможет.

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

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

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

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

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

Что-то еще может быть 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» тоже;)
SyntaxT3rr0r

5
Чувствуете упреждение? ;) В прошлом к ​​вам придирались. :)
Greg D

3
Я считаю , что общая ошибка , он имеет в виду, что люди , добавить ( и т.д.) методы «withXXX» на на Fooобъект, а не имеющие отдельный FooFactory.
Дин Хардинг

3
У вас все еще есть FooImplконструктор с 8 параметрами. Какое улучшение?
Mot

4
Я бы не стал называть этот код immutableи боялся, что люди будут повторно использовать фабричные объекты, потому что они так думают. Я имею в виду: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();где tallsбы сейчас содержались только женщины. Этого не должно происходить с неизменяемым API.
Thomas Ahle

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. Да, это злоупотребление системой репутации, но ... ох ... Я так люблю Scala, что пришлось это сделать. :)
Malax

1
О, чувак ... Я только что ответил на что-то почти идентичное! Что ж, я в хорошей компании. :-) Интересно, что я не видел вашего ответа раньше, хотя ... <пожимает плечами>
Дэниел С. Собрал

20

Что ж, рассмотрим это на 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конструктору плюс фабрика.


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 должен иметь отдельные классы для неизменяемых и изменяемых вариантов. Это обеспечивает лучшую безопасность, чем интерфейсный подход. Возможно, вы лжете потребителю API всякий раз, когда предоставляете ему изменяемый объект с именем Immutable, который просто нужно преобразовать в его изменяемый интерфейс. Подход с отдельным классом требует методов для преобразования туда и обратно. API JodaTime использует этот шаблон. Смотрите DateTime и MutableDateTime.
toolbear

6

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

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

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


1
Голосов против? Кто-нибудь хочет оставить комментарий, почему это не лучшее решение?
Джульетта

+1. Возможно, кто-то проголосовал против этого, потому что вы намекаете на использование clone()для получения новых экземпляров.
finnw

Или, может быть, это произошло потому, что это может поставить под угрозу безопасность потоков в Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

Недостатком является то, что будущие разработчики должны быть осторожны при обращении с флагом «замораживания». Если позже добавляется метод мутатора, который забывает подтвердить, что метод разморожен, могут возникнуть проблемы. Точно так же, если будет написан новый конструктор, который должен вызывать freeze()метод, но не вызывает его , все может стать некрасивым.
stalepretzel

5

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

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

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


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

2
@gustafc: Ага. Клифф Клик однажды рассказал историю о том, как они тестировали моделирование Clojure Ant Colony Rich Hickey на одном из своих больших блоков (864 ядра, 768 ГБ ОЗУ): 700 параллельных потоков, работающих на 700 ядрах, каждое на 100%, генерируя более 20 ГБ эфемерный мусор в секунду . ГК даже не вспотел.
Jörg W Mittag

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), хорошо, когда за «фабрикой» нет состояния, тогда как (3) является предпочтительной схемой, когда есть состояние. Я обнаружил, что использую (2), а не (3), когда не хочу беспокоиться о потоках и синхронизации, и мне не нужно беспокоиться об амортизации некоторых дорогостоящих настроек при производстве многих объектов. (3), с другой стороны, вызывается, когда реальная работа идет на создание фабрики (настройка из SPI, чтение файлов конфигурации и т. Д.).

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

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


6
Это шаблон Строителя (вариант 2)
Саймон Никерсон

Разве это не был бы объект фабрики («строитель»), который испускает его неизменяемые объекты?
bmargulies

это различие кажется довольно семантическим. Как делают бобы? Чем это отличается от строителя?
Карл

Вам не нужен пакет соглашений Java Bean для конструктора (или для чего-то еще).
Том Хотин - tackline

4

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


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

2

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

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 или C # до 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);`

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.