จะหลีกเลี่ยงปัญหา“ พารามิเตอร์มากเกินไป” ในการออกแบบ API ได้อย่างไร


160

ฉันมีฟังก์ชั่น API นี้:

public ResultEnum DoSomeAction(string a, string b, DateTime c, OtherEnum d, 
     string e, string f, out Guid code)

ฉันไม่ชอบมัน เนื่องจากลำดับพารามิเตอร์มีความสำคัญเกินความจำเป็น การเพิ่มฟิลด์ใหม่กลายเป็นเรื่องยาก มันยากที่จะเห็นสิ่งที่ถูกส่งผ่าน มันยากกว่าที่จะ refactor method เป็นชิ้นส่วนเล็ก ๆ เพราะมันจะสร้างโอเวอร์เฮดอีกครั้งในการส่งผ่านพารามิเตอร์ทั้งหมดในฟังก์ชั่นย่อย รหัสอ่านยากกว่า

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

public class DoSomeActionParameters
{
    public string A;
    public string B;
    public DateTime C;
    public OtherEnum D;
    public string E;
    public string F;        
}

นั่นทำให้การประกาศ API ของฉันลดลงเป็น:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

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

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }        

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, 
     string e, string f)
    {
        this.A = a;
        this.B = b;
        // ... tears erased the text here
    }
}

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

public struct DoSomeActionParameters
{
    public readonly string A;
    public readonly string B;
    public readonly DateTime C;
    public readonly OtherEnum D;
    public readonly string E;
    public readonly string F;        
}

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

  • ทุกคน (รวมถึง FXCop และจอนสกีต) ยอมรับว่าการเปิดเผยทุ่งสาธารณะที่ไม่ดี
  • เอริค Lippert et al, กล่าวว่าอาศัยอยู่ในเขตอ่านได้อย่างเดียวสำหรับการเปลี่ยนไม่ได้เป็นเรื่องโกหก

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

คำชี้แจง:

  • โปรดสมมติว่าไม่มีการละเมิดหลักการ responsibiltiy เดียว ในกรณีเดิมของฉันฟังก์ชั่นเพิ่งเขียนพารามิเตอร์ที่กำหนดไปยังระเบียนฐานข้อมูลเดียว
  • ฉันไม่ได้กำลังหาวิธีแก้ปัญหาเฉพาะสำหรับฟังก์ชั่นที่กำหนด ฉันกำลังหาแนวทางทั่วไปในการแก้ไขปัญหาดังกล่าว ฉันสนใจที่จะแก้ปัญหา "พารามิเตอร์มากเกินไป" โดยไม่ต้องแนะนำความไม่แน่นอนหรือการออกแบบที่แย่มาก

UPDATE

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


ทำความสะอาดรหัส: คู่มือของงานฝีมือซอฟต์แวร์เปรียวโดยโรเบิร์ตซีมาร์ตินและหนังสือ Refactoring Martin Fowler ครอบคลุมนี้บิต
Ian Ringrose

1
ตัวสร้างโซลูชันนั้นไม่ซ้ำซ้อนหรือไม่หากคุณใช้ C # 4 ซึ่งคุณมีพารามิเตอร์ทางเลือกหรือไม่
khellang

1
ฉันอาจจะโง่ แต่ฉันล้มเหลวที่จะดูว่าปัญหานี้เป็นอย่างไรโดยพิจารณาว่าDoSomeActionParametersเป็นวัตถุที่ถูกโยนทิ้งซึ่งจะถูกทิ้งหลังจากการเรียกใช้เมธอด
Vilx-

6
โปรดทราบว่าฉันไม่ได้บอกว่าคุณควรหลีกเลี่ยงฟิลด์แบบอ่านอย่างเดียวใน struct struct ที่ไม่เปลี่ยนรูปเป็นวิธีปฏิบัติที่ดีที่สุดและเขตข้อมูลแบบอ่านอย่างเดียวช่วยให้คุณสร้างเอกสารที่ไม่เปลี่ยนรูปแบบด้วยตนเอง ประเด็นของฉันคือคุณไม่ควรพึ่งพาเขตข้อมูลแบบอ่านอย่างเดียวที่ถูกสังเกตว่าไม่มีการเปลี่ยนแปลงเพราะนั่นไม่ได้รับประกันว่าเขตข้อมูลแบบอ่านอย่างเดียวจะให้โครงสร้างของคุณ นี่เป็นกรณีเฉพาะของคำแนะนำทั่วไปที่คุณไม่ควรใช้กับประเภทของค่าราวกับว่าเป็นประเภทอ้างอิง พวกเขาเป็นสัตว์ที่แตกต่างกันมาก
Eric Lippert

7
@ssg: ฉันก็รักเช่นกัน เราได้เพิ่มคุณสมบัติใน C # ที่ส่งเสริมการเปลี่ยนแปลงไม่ได้ (เช่น LINQ) ในเวลาเดียวกันกับที่เราได้เพิ่มคุณสมบัติที่ส่งเสริมความไม่แน่นอน (เช่นวัตถุเริ่มต้น) มันจะดีกว่าถ้ามีไวยากรณ์ที่ดีขึ้นเพื่อส่งเสริมประเภทที่ไม่เปลี่ยนรูป เรากำลังคิดอย่างหนักเกี่ยวกับมันและมีความคิดที่น่าสนใจ แต่ฉันไม่คิดว่าจะมีสิ่งใดสำหรับรุ่นถัดไป
Eric Lippert

คำตอบ:


22

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

var request = new HttpWebRequest(a, b);
var service = new RestService(request, c, d, e);
var client = new RestClient(service, f, g);
var resource = client.RequestRestResource(); // O params after 3 objects

การรวมสตริงทั้งหมดลงในอาร์เรย์สตริงจะใช้งานได้ก็ต่อเมื่อเกี่ยวข้องทั้งหมดเท่านั้น จากลำดับของการขัดแย้งดูเหมือนว่าพวกเขาไม่ได้เกี่ยวข้องกันทั้งหมด
icktoofay

ตกลงนอกจากนี้จะใช้งานได้เฉพาะในกรณีที่คุณมีพารามิเตอร์ประเภทเดียวกันจำนวนมาก
Timo Willemsen

มันแตกต่างจากตัวอย่างแรกของฉันในคำถามอย่างไร
Sedat Kapanoglu

นี่เป็นรูปแบบการสวมกอดตามปกติแม้ใน. NET Framework แสดงว่าตัวอย่างแรกของคุณค่อนข้างถูกต้องในสิ่งที่มันทำอยู่ คำถามที่ดีโดยวิธีการ
Teoman Soygul

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

87

ใช้การผสมผสานระหว่างผู้สร้างและ API สไตล์ภาษาเฉพาะโดเมน - ส่วนต่อประสาน Fluent API นั้นมีความละเอียดมากกว่านี้เล็กน้อย แต่ด้วย Intellisense ทำให้พิมพ์และเข้าใจได้ง่าย

public class Param
{
        public string A { get; private set; }
        public string B { get; private set; }
        public string C { get; private set; }


  public class Builder
  {
        private string a;
        private string b;
        private string c;

        public Builder WithA(string value)
        {
              a = value;
              return this;
        }

        public Builder WithB(string value)
        {
              b = value;
              return this;
        }

        public Builder WithC(string value)
        {
              c = value;
              return this;
        }

        public Param Build()
        {
              return new Param { A = a, B = b, C = c };
        }
  }


  DoSomeAction(new Param.Builder()
        .WithA("a")
        .WithB("b")
        .WithC("c")
        .Build());

+1 - วิธีการแบบนี้ (ซึ่งฉันเห็นโดยทั่วไปเรียกว่า "อินเทอร์เฟซที่คล่องแคล่ว") เป็นสิ่งที่ฉันมีอยู่ในใจเช่นกัน
Daniel Pryden

+1 - นี่คือสิ่งที่ฉันลงเอยในสถานการณ์เดียวกัน ยกเว้นว่ามีเพียงชั้นเดียวและDoSomeActionเป็นวิธีการของมัน
Vilx-

+1 ฉันก็คิดเช่นนี้ แต่เนื่องจากฉันยังใหม่กับอินเทอร์เฟซที่คล่องแคล่วฉันไม่ทราบว่ามันเหมาะสมกับที่นี่หรือไม่ ขอบคุณสำหรับการตรวจสอบสัญชาติญาณของฉันเอง
Matt

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

1
@ssg เป็นเรื่องปกติในสถานการณ์เหล่านี้ BCL มีคลาสตัวสร้างสตริงการเชื่อมต่อตัวอย่างเช่น
ซามูเอลเนฟฟ์

10

สิ่งที่คุณมีบ่งบอกได้ค่อนข้างชัดเจนว่าชั้นเรียนที่มีปัญหานั้นละเมิดหลักการความรับผิดชอบเดี่ยวเพราะมีการพึ่งพามากเกินไป มองหาวิธีการ refactor อ้างอิงเหล่านั้นลงในกลุ่มของซุ้มอ้างอิง


1
ฉันยังคง refactor โดยการแนะนำวัตถุพารามิเตอร์อย่างน้อยหนึ่งอย่าง แต่ถ้าคุณย้ายอาร์กิวเมนต์ทั้งหมดไปเป็นประเภทที่ไม่เปลี่ยนรูปแบบเดียวคุณก็ไม่ได้ทำอะไรเลย เคล็ดลับคือการค้นหากลุ่มของอาร์กิวเมนต์ที่เกี่ยวข้องอย่างใกล้ชิดมากขึ้นแล้ว refactor แต่ละกลุ่มเหล่านั้นไปยังวัตถุพารามิเตอร์แยกต่างหาก
Mark Seemann

2
คุณสามารถเปลี่ยนให้แต่ละกลุ่มเป็นวัตถุที่เปลี่ยนไม่ได้ในตัวมันเอง แต่ละวัตถุจะต้องใช้พารามิเตอร์สองสามตัวเท่านั้นดังนั้นในขณะที่จำนวนอาร์กิวเมนต์ที่แท้จริงยังคงเหมือนเดิมจำนวนอาร์กิวเมนต์ที่ใช้ในตัวสร้างเดียวจะลดลง
Mark Seemann

2
+1 @ssg: ทุกครั้งที่ฉันพยายามโน้มน้าวตัวเองในเรื่องนี้ฉันได้พิสูจน์ตัวเองผิดเมื่อเวลาผ่านไปโดยการผลักดันสิ่งที่เป็นนามธรรมออกมาจากวิธีการขนาดใหญ่ที่ต้องการพารามิเตอร์ระดับนี้ หนังสือ DDD โดย Evans อาจให้แนวคิดบางอย่างเกี่ยวกับวิธีการคิดเกี่ยวกับเรื่องนี้ (แม้ว่าระบบของคุณจะดูเหมือนว่ามันอาจจะไกลจากสถานที่ที่เกี่ยวข้องกับการใช้งานของผู้อุปถัมภ์ดังกล่าว) - (และเป็นเพียงหนังสือที่ยอดเยี่ยมทั้งสองวิธี)
Ruben Bartelink

3
@ Ruben: ไม่มีหนังสือที่มีสติจะพูดว่า "ในการออกแบบ OO ที่ดีในชั้นเรียนไม่ควรมีมากกว่า 5 คุณสมบัติ" ชั้นเรียนมีการจัดกลุ่มอย่างมีเหตุผลและประเภทของบริบทไม่สามารถขึ้นอยู่กับปริมาณ อย่างไรก็ตามการสนับสนุนที่ไม่สามารถเปลี่ยนแปลงได้ของ C # เริ่มก่อให้เกิดปัญหาด้วยคุณสมบัติจำนวนหนึ่งก่อนที่เราจะเริ่มละเมิดหลักการออกแบบ OO ที่ดี
Sedat Kapanoglu

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

10

เพียงแค่เปลี่ยนโครงสร้างข้อมูลพารามิเตอร์จากclassa structและคุณก็พร้อมที่จะไป

public struct DoSomeActionParameters 
{
   public string A;
   public string B;
   public DateTime C;
   public OtherEnum D;
   public string E;
   public string F;
}

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code) 

วิธีนี้จะได้รับสำเนาของโครงสร้างของตัวเอง วิธีการเปลี่ยนแปลงที่เกิดขึ้นกับตัวแปรอาร์กิวเมนต์ไม่สามารถสังเกตได้โดยวิธีการและการเปลี่ยนแปลงวิธีการทำให้ตัวแปรที่ไม่สามารถสังเกตได้โดยผู้โทร การแยกทำได้โดยไม่เปลี่ยนรูป

ข้อดี:

  • ใช้งานง่ายที่สุด
  • การเปลี่ยนแปลงพฤติกรรมอย่างน้อยที่สุดในกลศาสตร์พื้นฐาน

จุดด้อย:

  • Immutability ไม่ชัดเจนต้องให้ความสนใจนักพัฒนา
  • การคัดลอกที่ไม่จำเป็นเพื่อรักษาความไม่สามารถเปลี่ยนแปลงได้
  • ตรงบริเวณพื้นที่สแต็ค

+1 จริง แต่ไม่สามารถแก้ปัญหาส่วนที่เกี่ยวกับ "การเปิดเผยฟิลด์โดยตรงเป็นสิ่งที่ชั่วร้าย" ฉันไม่แน่ใจว่าสิ่งเลวร้ายจะเลวร้ายยิ่งขึ้นถ้าฉันเลือกใช้ฟิลด์แทนคุณสมบัติบน API นั้น
Sedat Kapanoglu

@ssg - ทำให้เป็นคุณสมบัติสาธารณะแทนที่จะเป็นฟิลด์ หากคุณถือว่าสิ่งนี้เป็นโครงสร้างที่จะไม่มีรหัสก็ไม่ได้สร้างความแตกต่างมากมายไม่ว่าคุณจะใช้คุณสมบัติหรือเขตข้อมูล หากคุณตัดสินใจที่จะให้รหัส (เช่นการตรวจสอบหรือบางอย่าง) คุณจะต้องทำให้เป็นคุณสมบัติ อย่างน้อยที่สุดกับทุ่งสาธารณะไม่มีใครจะมีภาพลวงตาใด ๆ เกี่ยวกับค่าคงที่ที่มีอยู่ในโครงสร้างนี้ มันจะต้องมีการตรวจสอบโดยวิธีการเหมือนพารามิเตอร์จะได้รับ
Jeffrey L Whitledge

โอ้จริง ! น่ากลัว ขอบคุณ
Sedat Kapanoglu

8
ฉันคิดว่ากฎการเปิดเผยฟิลด์เป็นความชั่วร้ายนั้นนำไปใช้กับวัตถุในการออกแบบเชิงวัตถุ โครงสร้างที่ฉันกำลังเสนอเป็นเพียงพารามิเตอร์ภาชนะโลหะเปลือย เนื่องจากสัญชาตญาณของคุณต้องไปกับสิ่งที่ไม่เปลี่ยนแปลงฉันคิดว่าภาชนะพื้นฐานนั้นอาจเหมาะสมสำหรับโอกาสนี้
Jeffrey L Whitledge

1
@ JeffreyLWhitledge: ฉันไม่ชอบความคิดที่ว่าโครงสร้างข้อมูลที่เปิดเผยเป็นสิ่งที่ชั่วร้าย การอ้างสิทธิ์ดังกล่าวทำให้ฉันรู้สึกว่าเทียบเท่ากับการพูดว่าไขควงเป็นสิ่งชั่วร้ายและผู้คนควรใช้ค้อนเพราะจุดของไขควงหัวบุ๋มเล็บ หากจำเป็นต้องใช้ตะปูควรใช้ค้อน แต่หากจำเป็นต้องใช้ตะปูควงควรใช้ไขควง มีหลายสถานการณ์ที่โครงสร้างของเขตข้อมูลที่เปิดเผยเป็นเครื่องมือที่เหมาะสมสำหรับงาน บังเอิญมีน้อยกว่าที่ struct กับคุณสมบัติรับ / ชุดเป็นเครื่องมือที่เหมาะสม (ในกรณีส่วนใหญ่ที่ ...
supercat

6

วิธีสร้างคลาสบิวเดอร์ภายในคลาสข้อมูลของคุณ คลาสข้อมูลจะมีตัวตั้งค่าทั้งหมดเป็นแบบส่วนตัวและมีเพียงผู้สร้างเท่านั้นที่สามารถตั้งค่าได้

public class DoSomeActionParameters
    {
        public string A { get; private set; }
        public string B  { get; private set; }
        public DateTime C { get; private set; }
        public OtherEnum D  { get; private set; }
        public string E  { get; private set; }
        public string F  { get; private set; }

        public class Builder
        {
            DoSomeActionParameters obj = new DoSomeActionParameters();

            public string A
            {
                set { obj.A = value; }
            }
            public string B
            {
                set { obj.B = value; }
            }
            public DateTime C
            {
                set { obj.C = value; }
            }
            public OtherEnum D
            {
                set { obj.D = value; }
            }
            public string E
            {
                set { obj.E = value; }
            }
            public string F
            {
                set { obj.F = value; }
            }

            public DoSomeActionParameters Build()
            {
                return obj;
            }
        }
    }

    public class Example
    {

        private void DoSth()
        {
            var data = new DoSomeActionParameters.Builder()
            {
                A = "",
                B = "",
                C = DateTime.Now,
                D = testc,
                E = "",
                F = ""
            }.Build();
        }
    }

1
+1 นั่นเป็นทางออกที่ถูกต้องสมบูรณ์แบบ แต่ฉันคิดว่ามันเป็นโครงสร้างที่มากเกินไปในการตัดสินใจออกแบบที่เรียบง่าย โดยเฉพาะอย่างยิ่งเมื่อโซลูชัน "อ่านอย่างเดียว" คือ "มาก" ใกล้เคียงกับอุดมคติ
Sedat Kapanoglu

2
วิธีนี้แก้ปัญหา "พารามิเตอร์มากเกินไป" ได้อย่างไร ไวยากรณ์อาจแตกต่างกัน แต่ปัญหาดูเหมือนกัน นี่ไม่ใช่การวิจารณ์ฉันแค่อยากรู้อยากเห็นเพราะฉันไม่คุ้นเคยกับรูปแบบนี้
alexD

1
@alexD นี้ช่วยแก้ปัญหาด้วยการมีพารามิเตอร์ฟังก์ชั่นมากเกินไปและทำให้วัตถุไม่เปลี่ยนรูป มีเพียงคลาส builder เท่านั้นที่สามารถตั้งค่าคุณสมบัติส่วนตัวและเมื่อคุณได้รับออบเจ็กต์พารามิเตอร์คุณจะไม่สามารถเปลี่ยนแปลงได้ ปัญหาคือต้องใช้รหัสนั่งร้านจำนวนมาก
marto

5
วัตถุพารามิเตอร์ในโซลูชันของคุณไม่เปลี่ยนรูป คนที่บันทึกเครื่องมือสร้างสามารถแก้ไขพารามิเตอร์แม้หลังการสร้าง
astef

1
ถึงจุดของ @ astef เนื่องจากขณะนี้มีการเขียนDoSomeActionParameters.Builderอินสแตนซ์สามารถใช้ในการสร้างและกำหนดค่าหนึ่งDoSomeActionParametersอินสแตนซ์ที่แน่นอน หลังจากเรียกBuild()การเปลี่ยนแปลงคุณสมบัติของอินสแตนซ์ที่ตามมาBuilderจะดำเนินการแก้ไขคุณสมบัติของอินสแตนซ์ดั้งเดิม DoSomeActionParametersต่อไปและการโทรที่ตามมาBuild()จะดำเนินการเพื่อส่งคืนDoSomeActionParametersอินสแตนซ์เดียวกันต่อไป public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }จริงๆมันควรจะเป็น
BACON

6

ฉันไม่ใช่โปรแกรมเมอร์ C # แต่ฉันเชื่อว่า C # รองรับอาร์กิวเมนต์ที่มีชื่อ: (F # ทำและ C # ส่วนใหญ่มีคุณสมบัติที่เข้ากันได้กับสิ่งนั้น): http://msdn.microsoft.com/en-us/library/dd264739 ขอบ # Y342

ดังนั้นการเรียกรหัสต้นฉบับของคุณจะกลายเป็น:

public ResultEnum DoSomeAction( 
 e:"bar", 
 a: "foo", 
 c: today(), 
 b:"sad", 
 d: Red,
 f:"penguins")

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

แก้ไข: นี่คือบทความที่ฉันพบเกี่ยวกับมัน http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ ฉันควรพูดถึง C # 4.0 รองรับอาร์กิวเมนต์ที่มีชื่อ 3.0 ไม่ได้


การปรับปรุงแน่นอน แต่สิ่งนี้จะแก้ปัญหาความสามารถในการอ่านโค้ดเท่านั้นและจะทำเช่นนั้นเมื่อนักพัฒนาเลือกใช้สำหรับการใช้พารามิเตอร์ที่มีชื่อ เป็นเรื่องง่ายที่คนจะลืมระบุชื่อและมอบสิทธิประโยชน์ มันไม่ได้ช่วยเมื่อเขียนรหัสเอง เช่นเมื่อ refactoring ฟังก์ชั่นเป็นคนที่มีขนาดเล็กและส่งข้อมูลไปรอบ ๆ ในแพ็คเก็ตที่มีอยู่เดียว
Sedat Kapanoglu

6

ทำไมไม่เพียงสร้างอินเทอร์เฟซที่บังคับให้ใช้ไม่ได้ (เช่น getters เท่านั้น)

เป็นวิธีแก้ปัญหาแรกของคุณ แต่คุณบังคับให้ฟังก์ชันใช้อินเทอร์เฟซเพื่อเข้าถึงพารามิเตอร์

public interface IDoSomeActionParameters
{
    string A { get; }
    string B { get; }
    DateTime C { get; }
    OtherEnum D { get; }
    string E { get; }
    string F { get; }              
}

public class DoSomeActionParameters: IDoSomeActionParameters
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }        
}

และการประกาศฟังก์ชั่นจะกลายเป็น:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

ข้อดี:

  • ไม่มีปัญหาพื้นที่สแต็กเหมือนstructวิธีแก้ปัญหา
  • วิธีแก้ปัญหาโดยธรรมชาติโดยใช้ความหมายของภาษา
  • ชัดเจนไม่ได้
  • ยืดหยุ่น (ผู้บริโภคสามารถใช้คลาสอื่นได้หากต้องการ)

จุดด้อย:

  • งานซ้ำ ๆ (ประกาศเหมือนกันในสองหน่วยงานที่แตกต่างกัน)
  • นักพัฒนาต้องเดาว่าDoSomeActionParametersเป็นคลาสที่สามารถแมปได้IDoSomeActionParameters

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

ฉันชอบอันนี้ความซ้ำซ้อนและความรู้ของแผนที่ได้รับการจัดการด้วย resharper และฉันสามารถจัดหาค่าเริ่มต้นโดยใช้ตัวสร้างเริ่มต้นของคลาสคอนกรีต
Anthony Johnston

2
ฉันไม่ชอบวิธีการนั้น คนที่มีการอ้างอิงถึงการใช้งานโดยพลการของIDoSomeActionParametersและต้องการที่จะจับค่าในนั้นไม่มีทางรู้ว่าการถือการอ้างอิงจะเพียงพอหรือถ้ามันจะต้องคัดลอกค่าไปยังวัตถุอื่น ๆ ส่วนต่อประสานที่อ่านได้มีประโยชน์ในบางบริบท แต่ไม่ได้หมายถึงการทำให้สิ่งต่าง ๆ ไม่เปลี่ยนรูป
supercat

"IDoSomeActionParameters" สามารถส่งไปยัง DoSomeActionParameters และเปลี่ยนแปลงได้ นักพัฒนาอาจไม่ได้ตระหนักว่าพวกเขากำลังพยายามหลีกเลี่ยงการทำพารามิเตอร์เลียนแบบ
โจเซฟซิมป์สัน

3

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

    public class ExampleClass
    {
        //create properties like this...
        private readonly int _exampleProperty;
        public int ExampleProperty { get { return _exampleProperty; } }

        //Private constructor, prohibiting construction outside of this class
        private ExampleClass(ExampleClassParams parameters)
        {                
            _exampleProperty = parameters.ExampleProperty;
            //and so on... 
        }

        //The object returned from here will be immutable
        public ExampleClass GetFromDatabase(DBConnection conn, int id)
        {
            //do database stuff here (ommitted from example)
            ExampleClassParams parameters = new ExampleClassParams()
            {
                ExampleProperty = 1,
                ExampleProperty2 = 2
            };

            //Danger here as parameters object is mutable

            return new ExampleClass(parameters);    

            //Danger is now over ;)
        }

        //Private struct representing the parameters, nested within class that uses it.
        //This is mutable, but the fact that it is private means that all potential 
        //"damage" is limited to this class only.
        private struct ExampleClassParams
        {
            public int ExampleProperty { get; set; }
            public int AnotherExampleProperty { get; set; }
            public int ExampleProperty2 { get; set; }
            public int AnotherExampleProperty2 { get; set; }
            public int ExampleProperty3 { get; set; }
            public int AnotherExampleProperty3 { get; set; }
            public int ExampleProperty4 { get; set; }
            public int AnotherExampleProperty4 { get; set; } 
        }
    }

2

คุณสามารถใช้วิธีการสร้างสไตล์ แต่ขึ้นอยู่กับความซับซ้อนของDoSomeActionวิธีการของคุณนี้อาจเป็นรุ่นหนา บางสิ่งบางอย่างตามสายเหล่านี้:

public class DoSomeActionParametersBuilder
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    public OtherEnum D { get; set; }
    public string E { get; set; }
    public string F { get; set; }

    public DoSomeActionParameters Build()
    {
        return new DoSomeActionParameters(A, B, C, D, E, F);
    }
}

public class DoSomeActionParameters
{
    public string A { get; private set; }
    public string B { get; private set; }
    public DateTime C { get; private set; }
    public OtherEnum D { get; private set; }
    public string E { get; private set; }
    public string F { get; private set; }

    public DoSomeActionParameters(string a, string b, DateTime c, OtherEnum d, string e, string f)
    {
        A = a;
        // etc.
    }
}

// usage
var actionParams = new DoSomeActionParametersBuilder
{
    A = "value for A",
    C = DateTime.Now,
    F = "I don't care for B, D and E"
}.Build();

result = foo.DoSomeAction(actionParams, out code);

อามาร์โดเอาชนะฉันตามคำแนะนำของผู้สร้าง!
Chris White

2

นอกเหนือจากการตอบสนองของ manji - คุณอาจต้องการแยกการปฏิบัติการหนึ่ง ๆ ออกเป็นปฏิบัติการเล็ก ๆ เปรียบเทียบ:

 BOOL WINAPI CreateProcess(
   __in_opt     LPCTSTR lpApplicationName,
   __inout_opt  LPTSTR lpCommandLine,
   __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
   __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
   __in         BOOL bInheritHandles,
   __in         DWORD dwCreationFlags,
   __in_opt     LPVOID lpEnvironment,
   __in_opt     LPCTSTR lpCurrentDirectory,
   __in         LPSTARTUPINFO lpStartupInfo,
   __out        LPPROCESS_INFORMATION lpProcessInformation
 );

และ

 pid_t fork()
 int execvpe(const char *file, char *const argv[], char *const envp[])
 ...

สำหรับผู้ที่ไม่รู้จัก POSIX การสร้างเด็กสามารถทำได้ง่ายเหมือน:

pid_t child = fork();
if (child == 0) {
    execl("/bin/echo", "Hello world from child", NULL);
} else if (child != 0) {
    handle_error();
}

ตัวเลือกการออกแบบแต่ละตัวแสดงถึงการแลกเปลี่ยนกับการปฏิบัติการที่อาจทำ

PS ใช่ - คล้ายกับตัวสร้าง - เป็นแบบย้อนกลับเท่านั้น (เช่นทางด้าน callee แทนที่จะเป็นผู้โทร) มันอาจจะหรืออาจจะไม่ดีกว่าแล้วสร้างในกรณีเฉพาะนี้


เป็นการดำเนินการขนาดเล็ก แต่เป็นคำแนะนำที่ดีสำหรับผู้ที่ละเมิดหลักการความรับผิดชอบเดี่ยว คำถามของฉันเกิดขึ้นหลังจากได้ทำการปรับปรุงการออกแบบอื่น ๆ ทั้งหมดแล้ว
Sedat Kapanoglu

ยูพีเอส ขออภัยฉันพร้อมคำถามก่อนการแก้ไขของคุณและโพสต์ในภายหลัง - ดังนั้นฉันไม่ได้สังเกตเห็น
Maciej Piechotka

2

นี่คือสิ่งที่แตกต่างจาก Mikeys เล็กน้อย แต่สิ่งที่ฉันพยายามจะทำคือทำให้การเขียนให้น้อยที่สุด

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }

    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }

        public DoSomeActionParameters Create()
        {
            return new DoSomeActionParameters(this);
        }
    }
}

DoSomeActionParameters ไม่สามารถเปลี่ยนได้เนื่องจากสามารถและไม่สามารถสร้างได้โดยตรงเนื่องจากตัวสร้างเริ่มต้นเป็นแบบส่วนตัว

initializer นั้นไม่เปลี่ยนรูป แต่เป็นเพียงการส่งผ่าน

การใช้งานใช้ประโยชน์จาก initializer บน Initializer (ถ้าคุณได้ดริฟท์ของฉัน) และฉันสามารถมีค่าเริ่มต้นใน Constructor เริ่มต้นของ Initializer

DoSomeAction(new DoSomeActionParameters.Initializer
            {
                A = "Hello",
                B = 42
            }
            .Create());

พารามิเตอร์จะเป็นตัวเลือกที่นี่หากคุณต้องการให้มีบางอย่างที่ต้องการคุณสามารถใส่ไว้ในตัวสร้างเริ่มต้นของเครื่องมือเริ่มต้น

และการตรวจสอบสามารถไปในวิธีการสร้าง

public class Initializer
{
    public Initializer(int b)
    {
        A = "(unknown)";
        B = b;
    }

    public string A { get; set; }
    public int B { get; private set; }

    public DoSomeActionParameters Create()
    {
        if (B < 50) throw new ArgumentOutOfRangeException("B");

        return new DoSomeActionParameters(this);
    }
}

ดังนั้นตอนนี้ดูเหมือนว่า

DoSomeAction(new DoSomeActionParameters.Initializer
            (b: 42)
            {
                A = "Hello"
            }
            .Create());

ยังคงเป็นกูกี้เล็ก ๆ น้อย ๆ ที่ฉันรู้ แต่จะลองต่อไป

แก้ไข: การย้ายวิธีการสร้างเป็นแบบคงที่ในวัตถุพารามิเตอร์และการเพิ่มผู้แทนซึ่งผ่านการเริ่มต้นใช้เวลาบางส่วนของ kookieness จากการโทร

public class DoSomeActionParameters
{
    readonly string _a;
    readonly int _b;

    public string A { get { return _a; } }
    public int B{ get { return _b; } }

    DoSomeActionParameters(Initializer data)
    {
        _a = data.A;
        _b = data.B;
    }

    public class Initializer
    {
        public Initializer()
        {
            A = "(unknown)";
            B = 88;
        }

        public string A { get; set; }
        public int B { get; set; }
    }

    public static DoSomeActionParameters Create(Action<Initializer> assign)
    {
        var i = new Initializer();
        assign(i)

        return new DoSomeActionParameters(i);
    }
}

ดังนั้นตอนนี้การโทรจะเป็นแบบนี้

DoSomeAction(
        DoSomeActionParameters.Create(
            i => {
                i.A = "Hello";
            })
        );

1

ใช้โครงสร้าง แต่แทนที่จะเป็นเขตข้อมูลสาธารณะมีคุณสมบัติสาธารณะ:

•ทุกคน (รวมถึง FXCop & Jon Skeet) ยอมรับว่าการเปิดเผยช่องสาธารณะนั้นไม่ดี

จอนและ FXCop จะได้รับการตอบรับอย่างดีเนื่องจากคุณกำลังเปิดเผยว่าไม่ใช่สาขา

• Eric Lippert และคนอื่น ๆ กล่าวว่าการใช้ฟิลด์ที่อ่านไม่ออกสำหรับการเปลี่ยนไม่ได้เป็นเรื่องโกหก

Eric จะพอใจเพราะใช้คุณสมบัติคุณสามารถมั่นใจได้ว่าค่าจะถูกตั้งค่าเพียงครั้งเดียว

    private bool propC_set=false;
    private date pC;
    public date C {
        get{
            return pC;
        }
        set{
            if (!propC_set) {
               pC = value;
            }
            propC_set = true;
        }
    }

วัตถุกึ่งไม่เปลี่ยนรูปหนึ่ง (สามารถตั้งค่าได้ แต่ไม่เปลี่ยนแปลง) ใช้งานได้กับค่าและประเภทการอ้างอิง


+1 อาจเป็นฟิลด์แบบอ่านอย่างเดียวส่วนตัวและคุณสมบัติผู้ทะเยอทะยานในที่สาธารณะเท่านั้นที่สามารถนำมารวมกันได้ในโครงสร้างเพื่ออนุญาตให้ใช้โซลูชัน verbose ที่น้อยลง
Sedat Kapanoglu

คุณสมบัติการอ่าน - เขียนพับลิกบนโครงสร้างที่กลายพันธุ์thisนั้นมีความชั่วร้ายมากกว่าฟิลด์สาธารณะ ฉันไม่แน่ใจว่าทำไมคุณคิดว่าการตั้งค่าเพียงครั้งเดียวจะทำให้สิ่งที่ "ตกลง"; หากเริ่มต้นด้วยอินสแตนซ์เริ่มต้นของ struct ปัญหาที่เกี่ยวข้องกับthisคุณสมบัติ -mutating จะยังคงอยู่
supercat

0

คำตอบของซามูเอลที่แตกต่างที่ฉันใช้ในโครงการเมื่อฉันมีปัญหาเดียวกัน:

class MagicPerformer
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    public DateTime Param3 { get; set; }

    public MagicPerformer SetParam1(int value) { this.Param1 = value; return this; }
    public MagicPerformer SetParam2(string value) { this.Param2 = value; return this; }
    public MagicPerformer SetParam4(DateTime value) { this.Param3 = value; return this; }

    public void DoMagic() // Uses all the parameters and does the magic
    {
    }
}

และใช้:

new MagicPerformer().SeParam1(10).SetParam2("Yo!").DoMagic();

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


นี่คือคลาสที่ไม่แน่นอน
Sedat Kapanoglu

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