Làm cách nào để tránh quá nhiều thông số Vấn đề về lỗi trong thiết kế API?


160

Tôi có chức năng API này:

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

Tôi không thích nó. Bởi vì thứ tự tham số trở nên quan trọng không cần thiết. Nó trở nên khó khăn hơn để thêm các lĩnh vực mới. Thật khó để nhìn thấy những gì đang được thông qua. Việc tái cấu trúc phương thức thành các phần nhỏ hơn khó hơn vì nó tạo ra một chi phí khác để chuyển tất cả các tham số trong các hàm phụ. Mã khó đọc hơn.

Tôi đã đưa ra một ý tưởng rõ ràng nhất: có một đối tượng đóng gói dữ liệu và truyền nó đi thay vì truyền từng tham số một. Đây là những gì tôi đã đưa ra:

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

Điều đó đã giảm khai báo API của tôi xuống:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Đẹp. Trông rất ngây thơ nhưng chúng tôi thực sự đã giới thiệu một sự thay đổi lớn: chúng tôi đã giới thiệu tính đột biến. Bởi vì những gì chúng ta trước đây đã làm là thực sự để vượt qua một đối tượng bất biến ẩn danh: tham số hàm trên stack. Bây giờ chúng tôi đã tạo ra một lớp mới rất dễ thay đổi. Chúng tôi tạo ra khả năng thao túng trạng thái của người gọi . Đó là hút. Bây giờ tôi muốn đối tượng của mình bất biến, tôi phải làm gì?

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
    }
}

Như bạn thấy tôi thực sự đã tạo lại vấn đề ban đầu của mình: quá nhiều tham số. Rõ ràng đó không phải là con đường để đi. Tôi sẽ làm gì đây? Tùy chọn cuối cùng để đạt được sự bất biến như vậy là sử dụng cấu trúc "chỉ đọc" như thế này:

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;        
}

Điều đó cho phép chúng tôi tránh các nhà xây dựng có quá nhiều tham số và đạt được sự bất biến. Trên thực tế, nó sửa tất cả các vấn đề (thứ tự tham số vv). Chưa:

Đó là khi tôi bối rối và quyết định viết câu hỏi này: Cách đơn giản nhất trong C # để tránh vấn đề "quá nhiều tham số" mà không đưa ra khả năng biến đổi? Có thể sử dụng cấu trúc chỉ đọc cho mục đích đó mà không có thiết kế API xấu?

XÁC NHẬN:

  • Hãy cho rằng không có vi phạm nguyên tắc responsibiltiy duy nhất. Trong trường hợp ban đầu của tôi, hàm chỉ ghi các tham số đã cho vào một bản ghi DB.
  • Tôi không tìm kiếm một giải pháp cụ thể cho chức năng đã cho. Tôi đang tìm kiếm một cách tiếp cận tổng quát cho các vấn đề như vậy. Tôi đặc biệt quan tâm đến việc giải quyết vấn đề "quá nhiều tham số" mà không đưa ra khả năng biến đổi hoặc thiết kế khủng.

CẬP NHẬT

Các câu trả lời được cung cấp ở đây có những lợi thế / bất lợi khác nhau. Vì vậy, tôi muốn chuyển đổi nó thành wiki cộng đồng. Tôi nghĩ rằng mỗi câu trả lời với mẫu mã và Ưu / Nhược điểm sẽ tạo ra một hướng dẫn tốt cho các vấn đề tương tự trong tương lai. Bây giờ tôi đang cố gắng tìm hiểu làm thế nào để làm điều đó.


Clean Code: Cẩm nang về thủ công phần mềm linh hoạt của Robert C. Martin và Martin Fowler's tái cấu trúc cuốn sách này bao gồm một chút
Ian Ringrose

1
Không phải là giải pháp Builder-dự phòng nếu bạn sử dụng C # 4 khi bạn có các tham số tùy chọn ?
khellang

1
Tôi có thể ngu ngốc, nhưng tôi không thấy đây là một vấn đề như thế nào, vì cho rằng đó DoSomeActionParameterslà một đối tượng vứt đi, sẽ bị loại bỏ sau khi gọi phương thức.
Vilx-

6
Lưu ý rằng tôi không nói rằng bạn nên tránh các trường chỉ đọc trong một cấu trúc; cấu trúc bất biến là một thực tiễn tốt nhất và các trường chỉ đọc giúp bạn tạo ra các cấu trúc bất biến tự ghi lại. Quan điểm của tôi là bạn không nên dựa vào các trường chỉ đọc được quan sát để không bao giờ thay đổi , bởi vì đó không phải là một đảm bảo rằng trường chỉ đọc cung cấp cho bạn trong một cấu trúc. Đây là một trường hợp cụ thể về lời khuyên chung chung hơn mà bạn không nên đối xử với các loại giá trị như thể chúng là các loại tham chiếu; chúng là một động vật rất khác nhau
Eric Lippert

7
@ssg: Tôi cũng thích điều đó. Chúng tôi đã thêm các tính năng vào C # để thúc đẩy tính bất biến (như LINQ) cùng lúc với việc chúng tôi đã thêm các tính năng thúc đẩy tính biến đổi (như trình khởi tạo đối tượng.) Thật tuyệt khi có cú pháp tốt hơn để quảng bá các loại bất biến. Chúng tôi đang suy nghĩ kỹ về nó và có một số ý tưởng thú vị, nhưng tôi sẽ không mong đợi bất kỳ điều gì như vậy cho phiên bản tiếp theo.
Eric Lippert

Câu trả lời:


22

Một kiểu được bao bọc trong các khung công tác thường giống như việc nhóm các tham số liên quan vào các lớp liên quan (nhưng lại gặp vấn đề với tính biến đổi):

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

Ngưng tất cả các chuỗi thành một chuỗi chuỗi chỉ có ý nghĩa nếu tất cả chúng có liên quan. Từ thứ tự của các đối số, có vẻ như chúng không hoàn toàn liên quan.
icktoofay

Đồng ý, bên cạnh đó, điều đó sẽ chỉ hoạt động nếu bạn có nhiều loại giống như tham số.
Timo Willemsen

Làm thế nào khác với ví dụ đầu tiên của tôi trong câu hỏi?
Sedat Kapanoglu

Vâng, đây là kiểu được chấp nhận thông thường ngay cả trong .NET Framework, chỉ ra rằng ví dụ đầu tiên của bạn hoàn toàn đúng trong những gì nó làm ngay cả khi nó đưa ra các vấn đề biến đổi nhỏ (mặc dù ví dụ này rõ ràng là từ một thư viện khác). Nhân tiện câu hỏi hay.
Teoman Soygul

2
Tôi đã hội tụ phong cách này nhiều hơn theo thời gian. Rõ ràng số lượng đáng kể của các vấn đề "quá nhiều tham số" có thể được giải quyết bằng các nhóm logic và trừu tượng tốt. Cuối cùng, nó làm cho mã dễ đọc hơn và được mô đun hóa nhiều hơn.
Sedat Kapanoglu

87

Sử dụng kết hợp API kiểu trình tạo và ngôn ngữ dành riêng cho miền - Giao diện trôi chảy. API dài dòng hơn một chút nhưng với intellisense, nó rất nhanh để gõ và dễ hiểu.

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 - cách tiếp cận này (mà tôi thường thấy được gọi là "giao diện trôi chảy") chính xác là những gì tôi đã nghĩ trong đầu.
Daniel Pryden

+1 - đây là những gì tôi đã kết thúc trong tình huống tương tự. Ngoại trừ việc chỉ có một lớp và DoSomeActionlà một phương thức của nó.
Vilx-

+1 Tôi cũng đã nghĩ về điều này, nhưng vì tôi chưa quen với giao diện lưu loát, tôi không biết liệu nó có phù hợp hoàn hảo ở đây không. Cảm ơn đã xác nhận trực giác của riêng tôi.
Matt

Tôi đã không nghe nói về mẫu Builder trước khi tôi hỏi câu hỏi này vì vậy đây là một trải nghiệm sâu sắc đối với tôi. Tôi tự hỏi làm thế nào phổ biến nó là? Bởi vì bất kỳ nhà phát triển nào chưa từng nghe về mẫu này có thể gặp khó khăn trong việc hiểu cách sử dụng mà không có tài liệu hướng dẫn.
Sedat Kapanoglu

1
@ssg, nó là phổ biến trong các kịch bản. BCL có các lớp xây dựng chuỗi kết nối, ví dụ.
Samuel Neff

10

Những gì bạn có là một dấu hiệu khá chắc chắn rằng lớp đang nghi vấn đang vi phạm Nguyên tắc Trách nhiệm duy nhất vì nó có quá nhiều phụ thuộc. Tìm cách để cấu trúc lại các phụ thuộc đó thành các cụm Phụ thuộc mặt tiền .


1
Tôi vẫn tái cấu trúc bằng cách giới thiệu một hoặc nhiều đối tượng tham số, nhưng rõ ràng, nếu bạn di chuyển tất cả các đối số sang một loại bất biến duy nhất, bạn đã không làm được gì. Bí quyết là tìm kiếm các cụm đối số có liên quan chặt chẽ hơn, sau đó cấu trúc lại từng cụm đó thành một đối tượng tham số riêng.
Mark Seemann

2
Bạn có thể biến mỗi nhóm thành một đối tượng bất biến trong chính nó. Mỗi đối tượng sẽ chỉ cần lấy một vài tham số, vì vậy trong khi số lượng đối số thực tế vẫn giữ nguyên, số lượng đối số được sử dụng trong bất kỳ hàm tạo nào sẽ bị giảm.
Mark Seemann

2
+1 @ssg: Mỗi lần tôi cố gắng thuyết phục bản thân về điều gì đó như thế này, tôi đã chứng minh rằng mình đã sai theo thời gian bằng cách đưa sự trừu tượng hóa hữu ích ra khỏi các phương pháp lớn đòi hỏi mức tham số này. Cuốn sách DDD của Evans có thể cung cấp cho bạn một số ý tưởng về cách suy nghĩ về điều này (mặc dù hệ thống của bạn có vẻ như rất xa nơi áp dụng các ứng dụng như vậy) - (và đó cũng chỉ là một cuốn sách tuyệt vời).
Ruben Bartelink

3
@Ruben: Không có cuốn sách lành mạnh nào có thể nói "trong một thiết kế OO tốt, một lớp không nên có nhiều hơn 5 thuộc tính". Các lớp được nhóm một cách hợp lý và loại bối cảnh hóa đó không thể dựa trên số lượng. Tuy nhiên, hỗ trợ bất biến của C # bắt đầu tạo ra sự cố tại một số thuộc tính nhất định trước khi chúng tôi bắt đầu vi phạm các nguyên tắc thiết kế OO tốt.
Sedat Kapanoglu

3
@Ruben: Tôi không đánh giá kiến ​​thức, thái độ hay tính khí của bạn. Tôi hy vọng bạn cũng làm như vậy. Tôi không nói rằng các khuyến nghị của bạn là không tốt. Tôi đang nói rằng vấn đề của tôi có thể xuất hiện ngay cả trên thiết kế hoàn hảo nhất và đó dường như là một điểm bị bỏ qua ở đây. Mặc dù tôi hiểu tại sao những người có kinh nghiệm hỏi những câu hỏi cơ bản về những sai lầm phổ biến nhất, nhưng sẽ ít thú vị hơn khi làm rõ nó sau một vài vòng. Tôi phải một lần nữa nói rằng rất có thể có vấn đề đó với một thiết kế hoàn hảo. Và cảm ơn cho upvote!
Sedat Kapanoglu

10

Chỉ cần thay đổi cấu trúc dữ liệu tham số của bạn từ a classsang a structvà bạn sẽ ổn.

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) 

Phương thức này sẽ nhận được bản sao riêng của cấu trúc. Thay đổi được thực hiện cho biến đối số không thể được quan sát bởi phương thức và thay đổi phương thức làm cho biến không thể được quan sát bởi người gọi. Cô lập được thực hiện mà không bất biến.

Ưu điểm:

  • Dễ nhất để thực hiện
  • Thay đổi hành vi ít nhất trong cơ học cơ bản

Nhược điểm:

  • Bất biến là không rõ ràng, đòi hỏi sự chú ý của nhà phát triển.
  • Sao chép không cần thiết để duy trì tính bất biến
  • Chiếm không gian ngăn xếp

+1 Điều đó đúng nhưng không giải quyết được phần nào về "phơi bày trực tiếp các trường là xấu". Tôi không chắc những điều tồi tệ có thể trở nên tồi tệ như thế nào nếu tôi chọn sử dụng các trường thay vì các thuộc tính trên API đó.
Sedat Kapanoglu

@ssg - Vì vậy, làm cho chúng thuộc tính công khai thay vì các trường. Nếu bạn coi đây là một cấu trúc sẽ không bao giờ có mã, thì nó sẽ không tạo ra nhiều khác biệt cho dù bạn sử dụng thuộc tính hay trường. Nếu bạn quyết định cung cấp cho nó mã (chẳng hạn như xác nhận hoặc một cái gì đó), thì bạn chắc chắn sẽ muốn làm cho chúng thuộc tính. Ít nhất là với các lĩnh vực công cộng, sẽ không có ai có bất kỳ ảo tưởng nào về những bất biến tồn tại trên cấu trúc này. Nó sẽ phải được xác nhận bằng phương thức chính xác như các tham số đã có.
Jeffrey L Whitledge

Ồ đó là sự thật. Tuyệt vời! Cảm ơn.
Sedat Kapanoglu

8
Tôi nghĩ rằng quy tắc phơi bày trường-ác là áp dụng cho các đối tượng trong một thiết kế hướng đối tượng. Cấu trúc mà tôi đang đề xuất chỉ là một thùng chứa các tham số bằng kim loại trần. Vì bản năng của bạn là đi cùng với một thứ gì đó bất biến, tôi nghĩ rằng một thùng chứa cơ bản như vậy có thể phù hợp cho dịp này.
Jeffrey L Whitledge

1
@JeffreyLWhitledge: Tôi thực sự không thích ý tưởng rằng các cấu trúc trường lộ là xấu xa. Một tuyên bố như vậy đánh tôi là tương đương với việc nói rằng tua vít là xấu xa và mọi người nên sử dụng búa vì các điểm của tua vít đầu đinh. Nếu cần lái xe đinh, người ta nên dùng búa, nhưng nếu cần lái vít, người ta nên dùng tuốc nơ vít. Có nhiều tình huống trong đó một cấu trúc trường tiếp xúc chính xác là công cụ phù hợp cho công việc. Ngẫu nhiên, có rất ít trong đó một cấu trúc với các thuộc tính get / set thực sự là công cụ phù hợp (trong hầu hết các trường hợp trong đó ...
supercat

6

Làm thế nào về việc tạo một lớp xây dựng bên trong lớp dữ liệu của bạn. Lớp dữ liệu sẽ có tất cả các setters là riêng tư và chỉ người xây dựng mới có thể đặt chúng.

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 Đó là một giải pháp hoàn toàn hợp lệ nhưng tôi nghĩ rằng đó là quá nhiều giàn giáo để duy trì một quyết định thiết kế đơn giản như vậy. Đặc biệt là khi giải pháp "readonly struct" "rất" gần với lý tưởng.
Sedat Kapanoglu

2
Làm thế nào để giải quyết vấn đề "quá nhiều tham số" này? Cú pháp có thể khác nhau, nhưng vấn đề trông giống nhau. Đây không phải là một lời chỉ trích, tôi chỉ tò mò vì tôi không quen với mô hình này.
alexD

1
@alexD điều này giải quyết vấn đề với việc có quá nhiều tham số hàm và giữ cho đối tượng không thay đổi. Chỉ lớp trình xây dựng mới có thể đặt các thuộc tính riêng tư và khi bạn nhận được đối tượng tham số, bạn không thể thay đổi nó. Vấn đề là nó đòi hỏi rất nhiều mã giàn giáo.
marto

5
Đối tượng tham số trong giải pháp của bạn không phải là bất biến. Ai đó lưu trình xây dựng có thể chỉnh sửa tham số ngay cả sau khi xây dựng
astef

1
Theo quan điểm của @ astef, vì hiện tại nó DoSomeActionParameters.Buildercó thể được sử dụng để tạo và cấu hình chính xác một DoSomeActionParametersthể hiện. Sau khi gọi Build()các thay đổi tiếp theo cho các Builderthuộc tính của 'sẽ tiếp tục sửa đổi các thuộc tính của thể hiện ban đầu DoSomeActionParameters và các cuộc gọi tiếp theo Build()sẽ tiếp tục trả về cùng một DoSomeActionParametersthể hiện. Nó thực sự nên được public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON

6

Tôi không phải là lập trình viên C # nhưng tôi tin rằng C # hỗ trợ các đối số có tên: (F # does và C # phần lớn là tính năng tương thích cho loại điều đó) Nó thực hiện: http://msdn.microsoft.com/en-us/l Library / dd264739 .aspx # Y342

Vì vậy, gọi mã gốc của bạn trở thành:

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

điều này không còn nhiều không gian / suy nghĩ rằng việc tạo đối tượng của bạn và có tất cả các lợi ích, thực tế là bạn đã không thay đổi những gì đang xảy ra trong hệ thống không trả lời. Bạn thậm chí không phải mã hóa lại bất cứ điều gì để chỉ ra các đối số được đặt tên

Chỉnh sửa: đây là một nghệ thuật tôi tìm thấy về nó. http://www.globalnerdy.com/2009/03/12/default-and-named-parameter-in-c-40-sith-lord-in-training/ Tôi nên đề cập đến C # 4.0 hỗ trợ các đối số có tên, 3.0 không


Một sự cải tiến thực sự. Nhưng điều này chỉ giải quyết khả năng đọc mã và nó chỉ thực hiện điều đó khi nhà phát triển chọn tham gia sử dụng các tham số được đặt tên. Thật dễ dàng để người ta quên chỉ định tên và cho đi những lợi ích. Nó không giúp gì khi tự viết mã. ví dụ: khi tái cấu trúc hàm thành các hàm nhỏ hơn và truyền dữ liệu xung quanh trong một gói chứa.
Sedat Kapanoglu

6

Tại sao không chỉ tạo một giao diện thực thi tính bất biến (tức là chỉ các getters)?

Về cơ bản, đây là giải pháp đầu tiên của bạn, nhưng bạn buộc chức năng sử dụng giao diện để truy cập tham số.

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; }        
}

và khai báo hàm trở thành:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Ưu điểm:

  • Không có vấn đề không gian ngăn xếp như structgiải pháp
  • Giải pháp tự nhiên sử dụng ngữ nghĩa ngôn ngữ
  • Bất biến là hiển nhiên
  • Linh hoạt (người tiêu dùng có thể sử dụng một lớp khác nếu anh ta muốn)

Nhược điểm:

  • Một số công việc lặp đi lặp lại (khai báo giống nhau ở hai thực thể khác nhau)
  • Nhà phát triển phải đoán đó DoSomeActionParameterslà một lớp có thể được ánh xạ tớiIDoSomeActionParameters

+1 Tôi không biết tại sao tôi không nghĩ về điều đó? :) Tôi đoán tôi nghĩ rằng đối tượng vẫn sẽ bị một nhà xây dựng có quá nhiều tham số nhưng đó không phải là trường hợp. Vâng, đó cũng là một giải pháp rất hợp lệ. Vấn đề duy nhất tôi có thể nghĩ đến là người dùng API không thực sự đơn giản để tìm đúng tên lớp hỗ trợ giao diện đã cho. Giải pháp đòi hỏi ít tài liệu nhất là tốt hơn.
Sedat Kapanoglu

Tôi thích cái này, sự trùng lặp và kiến ​​thức về bản đồ được xử lý bằng cách chia sẻ lại và tôi có thể cung cấp các giá trị mặc định bằng cách sử dụng hàm tạo mặc định của lớp cụ thể
Anthony Johnston

2
Tôi không thích cách tiếp cận đó. Một người có tham chiếu đến việc triển khai tùy ý IDoSomeActionParametersvà muốn nắm bắt các giá trị trong đó không có cách nào để biết liệu việc giữ tham chiếu có đủ hay không, nếu nó phải sao chép các giá trị sang một đối tượng khác. Giao diện dễ đọc là hữu ích trong một số bối cảnh, nhưng không phải là một phương tiện để làm cho mọi thứ trở nên bất biến.
supercat

"Tham số IDoSomeActionParameter" có thể được chuyển thành DoSomeActionParameter và thay đổi. Nhà phát triển thậm chí có thể không nhận ra rằng họ đang phá vỡ một nỗ lực để làm cho các tham số không thể thay đổi
Joseph Simpson

3

Tôi biết đây là một câu hỏi cũ nhưng tôi nghĩ rằng tôi đã lội qua với đề nghị của mình vì tôi vừa phải giải quyết vấn đề tương tự. Bây giờ, vấn đề của tôi hơi khác với bạn vì tôi có yêu cầu bổ sung là không muốn người dùng có thể tự xây dựng đối tượng này (tất cả hydrat hóa dữ liệu đến từ cơ sở dữ liệu, vì vậy tôi có thể bỏ qua tất cả việc xây dựng bên trong). Điều này cho phép tôi sử dụng một hàm tạo riêng và mẫu sau;

    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

Bạn có thể sử dụng cách tiếp cận theo kiểu Builder, mặc dù tùy thuộc vào độ phức tạp của DoSomeActionphương thức của bạn , đây có thể là một trọng lượng nặng. Một cái gì đó dọc theo những dòng này:

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

Ah, marto đánh tôi để đề nghị xây dựng!
Chris White

2

Ngoài phản hồi manji - bạn cũng có thể muốn tách một thao tác thành nhiều thao tác nhỏ hơn. Đối chiếu:

 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[])
 ...

Đối với những người không biết POSIX, việc tạo ra đứa trẻ có thể dễ dàng như:

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

Mỗi lựa chọn thiết kế đại diện cho sự đánh đổi về những hoạt động mà nó có thể làm.

Tái bút Có - nó tương tự như trình xây dựng - chỉ ngược lại (tức là ở phía callee thay vì người gọi). Nó có thể hoặc không thể tốt hơn sau đó xây dựng trong trường hợp cụ thể này.


Đó là một hoạt động nhỏ nhưng đó là một khuyến nghị tốt cho bất cứ ai vi phạm nguyên tắc trách nhiệm đơn lẻ. Câu hỏi của tôi diễn ra ngay sau khi tất cả các cải tiến thiết kế khác đã được thực hiện.
Sedat Kapanoglu

Up Xin lỗi - Tôi đã sẵn sàng một câu hỏi trước khi chỉnh sửa của bạn và đăng nó sau - do đó tôi đã không nhận thấy nó.
Maciej Piechotka

2

đây là một cái hơi khác với Mikeys, nhưng những gì tôi đang cố gắng làm là làm cho toàn bộ càng ít để viết càng tốt

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);
        }
    }
}

DoSomeActionParameter là bất biến vì nó có thể và không thể được tạo trực tiếp vì hàm tạo mặc định của nó là riêng tư

Bộ khởi tạo không phải là bất biến, mà chỉ là một phương tiện giao thông

Việc sử dụng tận dụng lợi thế của trình khởi tạo trên Trình khởi tạo (nếu bạn nhận được sự trôi dạt của tôi) Và tôi có thể có các giá trị mặc định trong trình xây dựng mặc định của Trình khởi tạo

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

Các tham số sẽ là tùy chọn ở đây, nếu bạn muốn một số yêu cầu, bạn có thể đặt chúng vào hàm tạo mặc định của Trình khởi tạo

Và xác nhận có thể đi theo phương thức Tạo

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);
    }
}

Vì vậy, bây giờ nó trông giống như

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

Vẫn còn một ít kooki tôi biết, nhưng dù sao cũng sẽ thử

Chỉnh sửa: di chuyển phương thức tạo thành tĩnh trong đối tượng tham số và thêm một ủy nhiệm vượt qua trình khởi tạo sẽ đưa một số kookieness ra khỏi cuộc gọi

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);
    }
}

Vì vậy, cuộc gọi bây giờ trông như thế này

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

1

Sử dụng cấu trúc, nhưng thay vì các trường công khai, có các thuộc tính công khai:

• Mọi người (bao gồm FXCop & Jon Skeet) đồng ý rằng việc phơi bày các lĩnh vực công cộng là xấu.

Jon và FXCop sẽ được bão hòa bởi vì bạn đang phơi bày những thứ không phải là trường.

• Eric Lippert et al nói rằng dựa vào các trường chỉ đọc cho tính bất biến là một lời nói dối.

Eric sẽ được bảo vệ bởi vì sử dụng các thuộc tính, bạn có thể đảm bảo rằng giá trị chỉ được đặt một lần.

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

Một đối tượng bán bất biến (giá trị có thể được đặt nhưng không thay đổi). Hoạt động cho các loại giá trị và tham khảo.


+1 Có thể các trường chỉ đọc riêng và các thuộc tính chỉ dành cho người nhận công khai có thể được kết hợp trong một cấu trúc để cho phép giải pháp ít dài dòng hơn.
Sedat Kapanoglu

Thuộc tính đọc-ghi công khai trên các cấu trúc đột biến thislà xấu xa hơn nhiều so với các lĩnh vực công cộng. Tôi không chắc tại sao bạn nghĩ chỉ đặt giá trị một lần sẽ khiến mọi thứ "ổn"; nếu một bắt đầu với một thể hiện mặc định của một cấu trúc, các vấn đề liên quan đến các thisthuộc tính -mutating vẫn sẽ tồn tại.
supercat

0

Một biến thể của câu trả lời của Samuel mà tôi đã sử dụng trong dự án của mình khi tôi gặp vấn đề tương tự:

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
    {
    }
}

Và sử dụng:

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

Trong trường hợp của tôi, các tham số có thể sửa đổi có chủ ý, bởi vì các phương thức setter không cho phép tất cả các kết hợp có thể và chỉ hiển thị các kết hợp chung của chúng. Đó là bởi vì một số tham số của tôi khá phức tạp và phương pháp viết cho tất cả các trường hợp có thể sẽ khó khăn và không cần thiết (kết hợp điên rồ hiếm khi được sử dụng).


Đây là một lớp đột biến bất kể.
Sedat Kapanoglu

@ssg - Vâng, đúng vậy. Cái DoMagiccó trong hợp đồng của nó mặc dù nó sẽ không sửa đổi đối tượng. Nhưng tôi đoán nó không bảo vệ khỏi những sửa đổi ngẫu nhiên.
Vilx-
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.