การสร้างวัตถุขนาดใหญ่ที่ไม่เปลี่ยนรูปโดยไม่ใช้ตัวสร้างที่มีรายการพารามิเตอร์ยาว ๆ


96

ฉันมีวัตถุขนาดใหญ่ (มากกว่า 3 ช่อง) ที่สามารถและไม่เปลี่ยนรูปได้ ทุกครั้งที่ฉันเจอกรณีนั้นฉันมักจะสร้างสิ่งที่น่ารังเกียจของตัวสร้างที่มีรายการพารามิเตอร์ยาว ๆ

รู้สึกไม่ถูกต้องใช้ยากและความสามารถในการอ่านไม่เพียงพอ

จะยิ่งแย่ไปกว่านั้นถ้าฟิลด์นั้นเป็นประเภทคอลเลกชันบางประเภทเช่นรายการ วิธีง่ายๆaddSibling(S s)จะทำให้การสร้างวัตถุง่ายขึ้นมาก แต่ทำให้วัตถุไม่แน่นอน

พวกคุณใช้อะไรในกรณีเช่นนี้?

ฉันใช้ Scala และ Java แต่ฉันคิดว่าปัญหาคือภาษาไม่เชื่อเรื่องพระเจ้าตราบใดที่ภาษานั้นมุ่งเน้นไปที่วัตถุ

วิธีแก้ปัญหาที่ฉันคิดได้:

  1. "สิ่งที่น่ารังเกียจของตัวสร้างที่มีรายการพารามิเตอร์ยาว"
  2. รูปแบบตัวสร้าง

5
ฉันคิดว่ารูปแบบตัวสร้างเป็นวิธีที่ดีที่สุดและเป็นมาตรฐานที่สุดสำหรับสิ่งนี้
Zachary Wright

@ Zachary: สิ่งที่คุณสนับสนุนใช้ได้เฉพาะกับรูปแบบตัวสร้างรูปแบบพิเศษตามที่ Joshua Bloch อธิบายไว้ที่นี่: 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: โปรดอย่าร้องเรียนเกี่ยวกับ postfix "Impl" ใน "FooImpl": คลาสนี้ซ่อนอยู่ในโรงงานและจะไม่มีใครเห็นนอกจากคนที่เขียนอินเทอร์เฟซที่คล่องแคล่ว สิ่งที่ผู้ใช้ให้ความสำคัญคือเขาได้รับ "Foo" ฉันสามารถเรียก "FooImpl" "FooPointlessNitpick" ได้เช่นกัน;)
SyntaxT3rr0r

5
รู้สึกว่างเปล่า? ;) คุณเคยถูกมองข้ามเรื่องนี้มาก่อน :)
Greg D

3
ผมเชื่อว่าข้อผิดพลาดทั่วไปที่เขาหมายถึงคือคนเพิ่ม "withXXX" ( ฯลฯ ) วิธีการที่จะวัตถุแทนที่จะต้องแยกFoo FooFactory
Dean Harding

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 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 สำหรับการประดิษฐ์ภาษาสกาล่า ใช่มันเป็นการละเมิดระบบชื่อเสียง แต่ ... อ๊ะ ... ฉันรักสกาลามากฉันต้องทำมัน :)
Malax

1
โอ้มนุษย์ ... ฉันเพิ่งตอบบางอย่างที่เหมือนกัน! ฉันอยู่ใน บริษัท ที่ดี :-) ฉันสงสัยว่าฉันไม่เห็นคำตอบของคุณมาก่อนแม้ว่า ... <shrug>
Daniel C. Sobral

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]จากนั้นให้คนสองคนแต่งงานกัน ฉันคิดวิธีแก้ปัญหานั้นไม่ได้โดยไม่ต้องใช้ทั้ง a 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 framework


3
ตัวเลือกที่ 1 ฉลาด (แต่ไม่ฉลาดเกินไป ) ฉันก็เลยชอบ
ทิโมธี

4
ฉันทำสิ่งนี้ก่อนหน้านี้ แต่ในความคิดของฉันมันยังห่างไกลจากการเป็นทางออกที่ดีเพราะวัตถุยังคงเปลี่ยนแปลงได้มีเพียงวิธีการกลายพันธุ์เท่านั้นที่ "ซ่อนอยู่" บางทีฉันอาจจู้จี้จุกจิกเกินไปเกี่ยวกับเรื่องนี้ ...
Malax

ตัวแปรในตัวเลือกที่ 1 คือมีคลาสแยกกันสำหรับตัวแปรที่ไม่เปลี่ยนรูปและเปลี่ยนแปลงได้ สิ่งนี้ให้ความปลอดภัยที่ดีกว่าวิธีการเชื่อมต่อ แน่นอนว่าคุณกำลังโกหกผู้บริโภคของ API เมื่อใดก็ตามที่คุณให้วัตถุที่เปลี่ยนแปลงไม่ได้ชื่อไม่เปลี่ยนแปลงซึ่งจำเป็นต้องส่งไปยังอินเทอร์เฟซที่เปลี่ยนแปลงได้ วิธีการแยกชั้นเรียนต้องใช้วิธีการแปลงไปมา JodaTime API ใช้รูปแบบนี้ ดู DateTime และ MutableDateTime
toolbear

6

มันจะช่วยให้จำได้ว่ามีความแตกต่างกันชนิดของการเปลี่ยนไม่ได้ สำหรับกรณีของคุณฉันคิดว่าการไม่เปลี่ยนรูปของ "ไอติม" จะใช้ได้ดีจริงๆ:

ความไม่เปลี่ยนรูปของไอติม: เป็นสิ่งที่ฉันเรียกว่าการไม่เปลี่ยนรูปแบบเขียนครั้งเดียวที่อ่อนแอลงเล็กน้อย เราสามารถจินตนาการถึงวัตถุหรือสนามที่ยังคงเปลี่ยนแปลงได้ชั่วขณะในระหว่างการเริ่มต้นและจากนั้นก็ "ถูกแช่แข็ง" ตลอดไป ความไม่เปลี่ยนรูปแบบนี้มีประโยชน์อย่างยิ่งสำหรับวัตถุที่ไม่เปลี่ยนรูปซึ่งอ้างอิงกันแบบวงกลมหรือวัตถุที่ไม่เปลี่ยนรูปซึ่งถูกทำให้เป็นอนุกรมกับดิสก์และเมื่อทำการ deserialization จะต้องเป็น "ของไหล" จนกว่ากระบวนการ deserialization ทั้งหมดจะเสร็จสิ้นเมื่อถึงจุดนั้นวัตถุทั้งหมดอาจเป็น แช่แข็ง

ดังนั้นคุณเริ่มต้นวัตถุของคุณจากนั้นตั้งค่าสถานะ "หยุด" บางประเภทเพื่อระบุว่าวัตถุนั้นไม่สามารถเขียนได้อีกต่อไป คุณควรซ่อนการกลายพันธุ์ไว้เบื้องหลังฟังก์ชันเพื่อให้ฟังก์ชันนี้ยังคงบริสุทธิ์สำหรับลูกค้าที่ใช้ API ของคุณ


1
โหวตดาวน์? ทุกคนต้องการแสดงความคิดเห็นว่าทำไมถึงไม่ใช่ทางออกที่ดี?
Juliet

+1. อาจจะมีคนลดระดับลงเนื่องจากคุณกำลังบอกใบ้ถึงการใช้clone()อินสแตนซ์ใหม่
finnw

หรืออาจเป็นเพราะมันสามารถลดทอนความปลอดภัยของเธรดใน Java: java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

ข้อเสียคือต้องให้นักพัฒนาในอนาคตระมัดระวังในการจัดการแฟล็ก "หยุด" หากมีการเพิ่มวิธีการกลายพันธุ์ในภายหลังโดยลืมยืนยันว่าวิธีนี้ไม่ได้รับการแช่แข็งอาจมีปัญหาได้ ในทำนองเดียวกันหากมีการเขียนตัวสร้างใหม่ที่ควร แต่ไม่เรียกใช้freeze()เมธอดสิ่งต่าง ๆ อาจน่าเกลียด
stalepretzel

5

คุณยังสามารถทำให้อ็อบเจกต์ไม่เปลี่ยนรูปแสดงเมธอดที่ดูเหมือน mutators (เช่น addSibling) แต่ปล่อยให้มันส่งคืนอินสแตนซ์ใหม่ นั่นคือสิ่งที่คอลเลกชัน Scala ไม่เปลี่ยนรูปทำ

ข้อเสียคือคุณอาจสร้างอินสแตนซ์มากเกินความจำเป็น นอกจากนี้ยังใช้ได้เฉพาะเมื่อมีการกำหนดค่าที่ถูกต้องระดับกลาง (เช่นบางโหนดที่ไม่มีพี่น้องซึ่งในกรณีส่วนใหญ่ก็ใช้ได้) เว้นแต่คุณจะไม่ต้องการจัดการกับวัตถุที่สร้างขึ้นบางส่วน

ตัวอย่างเช่นขอบกราฟที่ไม่มีปลายทาง แต่ไม่ใช่ขอบกราฟที่ถูกต้อง


ข้อเสียที่ถูกกล่าวหาคือการสร้างอินสแตนซ์มากเกินความจำเป็นไม่ได้เป็นปัญหามากนัก การจัดสรรวัตถุมีราคาถูกมากเช่นเดียวกับการเก็บขยะของวัตถุอายุสั้น เมื่อเปิดใช้งานการวิเคราะห์การหลีกเลี่ยงโดยค่าเริ่มต้น "อ็อบเจ็กต์ระดับกลาง" ประเภทนี้มีแนวโน้มที่จะได้รับการจัดสรรสแต็กและไม่ต้องเสียค่าใช้จ่ายใด ๆ ในการสร้าง
gustafc

2
@gustafc: อ๋อ. Cliff Click เคยบอกเล่าเรื่องราวว่าพวกเขาเปรียบเทียบการจำลอง Clojure Ant Colony ของ Rich Hickey บนกล่องใหญ่ ๆ ได้อย่างไร (864 คอร์, 768 GB RAM): 700 เธรดขนานที่รันบน 700 คอร์แต่ละอันที่ 100% สร้างได้มากกว่า 20 GB ขยะชั่วคราวต่อวินาที GC ไม่ได้ทำให้เหงื่อแตก
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)
Simon Nickerson

นั่นจะเป็นวัตถุโรงงาน ('ผู้สร้าง') ที่ปล่อยวัตถุที่ไม่เปลี่ยนรูปของเขาไม่ใช่หรือ?
bmargulies

ความแตกต่างนั้นดูเหมือนเป็นความหมายที่ค่อนข้างดี ถั่วทำอย่างไร? สิ่งนี้แตกต่างจากช่างก่อสร้างอย่างไร?
Carl

คุณไม่ต้องการแพคเกจของอนุสัญญา Java Bean สำหรับตัวสร้าง (หรืออื่น ๆ อีกมากมาย)
Tom Hawtin - แท็กไลน์

4

อีกทางเลือกหนึ่งที่เป็นไปได้คือการปรับโครงสร้างใหม่เพื่อให้มีฟิลด์ที่กำหนดค่าได้น้อยลง หากกลุ่มของเขตข้อมูลทำงานร่วมกัน (ส่วนใหญ่) เท่านั้นให้รวบรวมไว้ในวัตถุขนาดเล็กที่ไม่เปลี่ยนรูป ตัวสร้าง / ตัวสร้างวัตถุ "เล็ก" นั้นควรจะจัดการได้มากกว่าเช่นเดียวกับตัวสร้าง / ตัวสร้างสำหรับวัตถุ "ใหญ่" นี้


1
หมายเหตุ: ระยะทางสำหรับคำตอบนี้อาจแตกต่างกันไปตามปัญหาฐานรหัสและทักษะของนักพัฒนา
Carl

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 on ทำให้สิ่งนี้ดูหรูหรายิ่งขึ้นด้วยไวยากรณ์ของ object initializer (เทียบเท่าความหมายกับตัวอย่างก่อนหน้า):

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

ตัวเลือกที่ 3:
ออกแบบชั้นเรียนของคุณใหม่โดยไม่จำเป็นต้องมีพารามิเตอร์จำนวนมากเช่นนี้ คุณสามารถแบ่งความรับผิดชอบออกเป็นหลายชั้นเรียน หรือส่งผ่านพารามิเตอร์ไม่ไปยังตัวสร้าง แต่ไปยังวิธีการเฉพาะตามต้องการ ไม่ได้ผลเสมอไป แต่เมื่อเป็นเช่นนั้นก็คุ้มค่าที่จะทำ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.