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


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

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

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

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

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

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

9 95

9 ответов:

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

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

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

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

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

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

FooFactory.create (),whereXXX(..) и 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 );
}

в 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))

ну, рассмотрим это на 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 конструктор плюс завод.

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

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

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

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

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

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

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

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

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

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

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

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

Я использую 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. конструктор, принимающий один параметр object

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# from 3.0 on делает это более элегантным с синтаксисом инициализатора объекта (семантически эквивалентным предыдущему примеру):

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

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