Створення великих незмінних об'єктів без використання конструкторів, що мають довгі списки параметрів


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": цей клас приховано всередині фабрики, і його ніхто більше не побачить, окрім людини, яка пише вільний інтерфейс. Все, що цікавить користувача, - це те, що він отримує "Фу". Я також міг би назвати "FooImpl" "FooPointlessNitpick";)
SyntaxT3rr0r

5
Ви відчуваєте перевагу? ;) В минулому вас здивували з цього приводу. :)
Грег Д

3
Я вважаю , що загальна помилка , він має на увазі, що люди , додати ( і т.д.) методи «withXXX» на на Fooоб'єкт, а не мають окремий FooFactory.
Дін Хардінг,

3
У вас все ще є FooImplконструктор з 8 параметрами. Яке поліпшення?
П'ятниця

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. Так, це зловживання системою репутації, але ... ав ... я так люблю Скалу, що мені довелося це робити. :)
Малакс

1
О, чоловіче ... я щойно відповів щось майже ідентичне! Ну, я в хорошій компанії. :-) Цікаво , що я не бачив відповідь раніше, хоча ... <знизують плечима>
Daniel C. Зібрав

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, який просто потрібно передати на його змінний інтерфейс. Підхід до окремого класу вимагає методів перетворення назад і назад. JodaTime API використовує цей шаблон. Див. 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 ГБ ефемерне сміття в секунду . 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 пристосований до різної ситуації. Перший з них важко любити з причин, на які посилається в ОП, і, як правило, є симптомом дизайну, який зазнав певного повзання і потребує певного рефакторингу.

Те, що я перелічую як (2), добре, коли за "фабрикою" не існує штату, тоді як (3) є вибором, коли існує штат. Я відчуваю, що використовую (2), а не (3), коли я не хочу турбуватися про потоки та синхронізацію, і мені не потрібно турбуватися про амортизацію деяких дорогих налаштувань для виробництва багатьох об’єктів. (3), з іншого боку, викликається, коли реальна робота йде на будівництво заводу (налаштування з SPI, читання файлів конфігурації тощо).

Нарешті, у чужій відповіді згадується варіант (4), де у вас є багато маленьких незмінних об’єктів, і найкращим шаблоном є отримання новин зі старих.

Зауважте, що я не є членом "фан-клубу" - звичайно, деякі речі варті наслідування, але мені здається, що вони починають своє безцільне власне життя, коли люди дають їм імена та кумедні капелюхи.


6
Це шаблон будівельника (варіант 2)
Саймон Нікерсон,

Хіба це не фабричний ("будівельний") об'єкт, який випромінює свої незмінні об'єкти?
bmargulies

ця відмінність видається досить семантичною. Як робиться квасоля? Чим це відрізняється від будівельника?
Карл,

Вам не потрібен пакет конвенцій Java Bean для конструктора (чи для багатьох інших).
Том Хоутін - таклін

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.