Создание больших неизменяемых объектов без использования конструкторов с длинными списками параметров - PullRequest
96 голосов
/ 17 мая 2010

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

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

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

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

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

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

Ответы [ 9 ]

75 голосов
/ 17 мая 2010

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

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

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

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 );
}
60 голосов
/ 17 мая 2010

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

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))
20 голосов
/ 17 мая 2010

Хорошо, рассмотрим это на 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 голосов
/ 17 мая 2010

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

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

6 голосов
/ 18 мая 2010

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

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

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

5 голосов
/ 17 мая 2010

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

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

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

5 голосов
/ 17 мая 2010

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

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

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

4 голосов
/ 17 мая 2010

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

2 голосов
/ 17 мая 2010

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

...