Bagaimana cara menghindari masalah "terlalu banyak parameter" dalam desain API?


160

Saya memiliki fungsi API ini:

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

Saya tidak suka itu. Karena urutan parameter menjadi tidak penting. Semakin sulit untuk menambahkan bidang baru. Lebih sulit untuk melihat apa yang sedang diedarkan. Lebih sulit untuk mengubah metode menjadi bagian-bagian yang lebih kecil karena ini menciptakan overhead lain untuk melewatkan semua parameter dalam sub fungsi. Kode lebih sulit dibaca.

Saya datang dengan ide yang paling jelas: memiliki objek merangkum data dan menyebarkannya alih-alih melewati setiap parameter satu per satu. Inilah yang saya pikirkan:

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

Itu mengurangi deklarasi API saya ke:

public ResultEnum DoSomeAction(DoSomeActionParameters parameters, out Guid code)

Bagus. Terlihat sangat polos tapi kami benar-benar memperkenalkan perubahan besar: kami memperkenalkan kemampuan berubah-ubah. Karena apa yang sebelumnya telah kami lakukan sebenarnya untuk melewatkan objek abadi anonim: parameter fungsi pada stack. Sekarang kami membuat kelas baru yang sangat bisa berubah. Kami menciptakan kemampuan untuk memanipulasi keadaan penelepon . Itu menyebalkan. Sekarang saya ingin objek saya tidak berubah, apa yang harus saya lakukan?

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

Seperti yang Anda lihat, saya benar-benar menciptakan kembali masalah asli saya: terlalu banyak parameter. Sudah jelas bahwa itu bukan cara untuk pergi. Apa yang akan aku lakukan? Opsi terakhir untuk mencapai ketidakberubahan tersebut adalah dengan menggunakan struct "readonly" seperti ini:

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

Itu memungkinkan kita untuk menghindari konstruktor dengan terlalu banyak parameter dan mencapai kekekalan. Sebenarnya itu memperbaiki semua masalah (pemesanan parameter dll). Namun:

Saat itulah saya menjadi bingung dan memutuskan untuk menulis pertanyaan ini: Apa cara paling mudah dalam C # untuk menghindari masalah "terlalu banyak parameter" tanpa memperkenalkan kemampuan berubah-ubah? Apakah mungkin untuk menggunakan struct hanya baca untuk tujuan itu dan belum memiliki desain API yang buruk?

KLARIFIKASI:

  • Harap asumsikan tidak ada pelanggaran terhadap prinsip tanggung jawab tunggal. Dalam kasus asli saya fungsi hanya menulis parameter yang diberikan ke catatan DB tunggal.
  • Saya tidak mencari solusi spesifik untuk fungsi yang diberikan. Saya mencari pendekatan umum untuk masalah seperti itu. Saya secara khusus tertarik untuk memecahkan masalah "terlalu banyak parameter" tanpa memperkenalkan kemampuan berubah-ubah atau desain yang mengerikan.

MEMPERBARUI

Jawaban yang diberikan di sini memiliki kelebihan / kekurangan yang berbeda. Karenanya saya ingin mengonversikan ini ke wiki komunitas. Saya pikir setiap jawaban dengan contoh kode dan Pro / Kontra akan menjadi panduan yang baik untuk masalah yang sama di masa depan. Saya sekarang mencoba mencari tahu bagaimana melakukannya.


Clean Code: A Handbook of Agile Software Craftsmanship oleh Robert C. Martin dan Martin Fowler's Refactoring book membahas ini sedikit
Ian Ringrose

1
Bukankah itu solusi Builder berlebihan jika Anda menggunakan C # 4 di mana Anda memiliki parameter opsional ?
khellang

1
Saya mungkin bodoh, tetapi saya gagal melihat bagaimana ini menjadi masalah, mengingat bahwa itu DoSomeActionParametersadalah objek sekali pakai, yang akan dibuang setelah pemanggilan metode.
Vilx-

6
Perhatikan bahwa saya tidak mengatakan bahwa Anda harus menghindari bidang baca hanya dalam struct; struct yang tidak berubah adalah praktik terbaik dan bidang yang hanya bisa dibaca membantu Anda membuat struct yang tidak dapat didokumentasikan sendiri. Maksud saya adalah bahwa Anda tidak harus bergantung pada bidang baca saja yang diamati untuk tidak pernah berubah , karena itu bukan jaminan bahwa bidang hanya baca memberi Anda dalam sebuah struct. Ini adalah kasus spesifik dari saran yang lebih umum bahwa Anda tidak boleh memperlakukan tipe nilai seolah-olah mereka adalah tipe referensi; mereka adalah binatang yang sangat berbeda.
Eric Lippert

7
@sg: Saya juga suka itu. Kami telah menambahkan fitur ke C # yang mempromosikan imutabilitas (seperti LINQ) pada saat yang sama karena kami telah menambahkan fitur yang mempromosikan mutabilitas (seperti inisialisasi objek.) Akan lebih baik jika memiliki sintaks yang lebih baik untuk mempromosikan tipe yang tidak dapat diubah. Kami berpikir keras tentang hal itu dan memiliki beberapa ide menarik, tetapi saya tidak akan mengharapkan hal seperti itu untuk versi berikutnya.
Eric Lippert

Jawaban:


22

Satu gaya yang dianut dalam kerangka kerja biasanya seperti pengelompokan parameter terkait ke dalam kelas terkait (tapi sekali lagi bermasalah dengan mutabilitas):

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

Mengondensasi semua string menjadi array string hanya masuk akal jika semuanya terkait. Dari urutan argumen, sepertinya semuanya tidak sepenuhnya terkait.
icktoofay

Setuju, selain itu, itu hanya akan berfungsi jika Anda memiliki banyak tipe yang sama dengan parameter.
Timo Willemsen

Bagaimana itu berbeda dari contoh pertama saya dalam pertanyaan?
Sedat Kapanoglu

Yah ini adalah gaya merangkul yang biasa bahkan dalam .NET Framework, menunjukkan bahwa contoh pertama Anda cukup benar dalam apa yang dilakukannya bahkan memperkenalkan masalah mutabilitas kecil (meskipun contoh ini dari perpustakaan lain jelas). Pertanyaan yang bagus.
Teoman Soygul

2
Saya telah lebih dekat dengan gaya ini. Tampaknya masalah "terlalu banyak parameter" yang signifikan dapat diselesaikan dengan grup logis dan abstraksi yang baik. Pada akhirnya itu membuat kode lebih mudah dibaca dan lebih termodulasi.
Sedat Kapanoglu

87

Gunakan kombinasi API gaya builder dan bahasa khusus domain - Antarmuka Lancar. API sedikit lebih verbose tetapi dengan intellisense sangat cepat untuk mengetik dan mudah dimengerti.

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 - pendekatan semacam ini (yang biasa saya lihat disebut "antarmuka lancar") adalah persis apa yang ada dalam pikiran saya juga.
Daniel Pryden

+1 - inilah yang saya dapatkan dalam situasi yang sama. Kecuali bahwa hanya ada satu kelas, dan DoSomeActionmerupakan metode untuk itu.
Vilx-

+1 Saya juga memikirkan hal ini, tetapi karena saya baru mengenal antarmuka yang lancar, saya tidak tahu apakah itu cocok di sini. Terima kasih telah memvalidasi intuisi saya sendiri.
Matt

Saya belum pernah mendengar tentang pola Builder sebelum saya mengajukan pertanyaan ini, jadi ini merupakan pengalaman yang mendalam bagi saya. Saya bertanya-tanya seberapa umum itu? Karena pengembang mana pun yang belum pernah mendengar polanya mungkin akan kesulitan memahami penggunaan tanpa dokumentasi.
Sedat Kapanoglu

1
@ssg, ini biasa terjadi dalam skenario ini. BCL memiliki kelas pembangun string koneksi, misalnya.
Samuel Neff

10

Apa yang Anda miliki di sana adalah indikasi yang cukup yakin bahwa kelas tersebut melanggar Prinsip Tanggung Jawab Tunggal karena memiliki terlalu banyak ketergantungan. Cari cara untuk memperbaiki ketergantungan tersebut ke dalam kelompok Dependensi Fasad .


1
Saya masih refactor dengan memperkenalkan satu atau lebih objek parameter, tetapi jelas, jika Anda memindahkan semua argumen ke tipe tunggal yang tidak dapat diubah, Anda tidak mencapai apa pun. Caranya adalah dengan mencari kluster argumen yang lebih dekat hubungannya, dan kemudian refactor masing-masing kluster ke objek parameter terpisah.
Mark Seemann

2
Anda dapat mengubah setiap grup menjadi objek yang tidak dapat diubah dengan sendirinya. Setiap objek hanya perlu mengambil beberapa parameter, jadi sementara jumlah aktual argumen tetap sama, jumlah argumen yang digunakan dalam setiap konstruktor tunggal akan berkurang.
Mark Seemann

2
+1 @ssg: Setiap kali saya berhasil meyakinkan diri saya tentang sesuatu seperti ini, saya telah membuktikan diri saya salah dari waktu ke waktu dengan mengusir abstraksi yang berguna dari metode besar yang membutuhkan tingkat parameter ini. Buku DDD karya Evans mungkin memberi Anda beberapa ide tentang cara berpikir tentang hal ini (meskipun sistem Anda sepertinya berpotensi jauh dari tempat yang relevan dengan penerapan pola-pola seperti itu) - (dan itu hanya buku yang bagus juga).
Ruben Bartelink

3
@ Ruben: Tidak ada buku waras yang mengatakan "dalam desain OO yang bagus, sebuah kelas tidak boleh memiliki lebih dari 5 properti". Kelas dikelompokkan secara logis dan kontekstualisasi semacam itu tidak dapat didasarkan pada kuantitas. Namun dukungan ketidakmampuan C # mulai menciptakan masalah di sejumlah properti sebelum kami mulai melanggar prinsip-prinsip desain OO yang baik.
Sedat Kapanoglu

3
@ Ruben: Saya tidak menilai pengetahuan, sikap, atau emosi Anda. Saya berharap Anda melakukan hal yang sama. Saya tidak mengatakan rekomendasi Anda tidak baik. Saya mengatakan bahwa masalah saya dapat muncul bahkan pada desain yang paling sempurna dan itu tampaknya menjadi poin yang diabaikan di sini. Meskipun saya mengerti mengapa orang yang berpengalaman mengajukan pertanyaan mendasar tentang kesalahan yang paling umum, menjadi kurang menyenangkan untuk mengklarifikasi setelah beberapa putaran. Saya harus mengatakan lagi bahwa sangat mungkin untuk memiliki masalah dengan desain yang sempurna. Dan terima kasih atas upvote!
Sedat Kapanoglu

10

Cukup ubah struktur data parameter Anda dari a classke structdan Anda siap melakukannya.

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) 

Metode sekarang akan mendapatkan salinan strukturnya sendiri. Perubahan yang dilakukan pada variabel argumen tidak dapat diamati oleh metode, dan perubahan metode membuat ke variabel tidak dapat diamati oleh pemanggil. Isolasi dicapai tanpa kekekalan.

Pro:

  • Paling mudah diimplementasikan
  • Sedikit perubahan perilaku dalam mekanika yang mendasarinya

Cons:

  • Kekekalan tidak jelas, membutuhkan perhatian pengembang.
  • Penyalinan yang tidak perlu untuk mempertahankan keabadian
  • Menempati ruang stack

+1 Itu benar tetapi tidak menyelesaikan bagian tentang "mengekspos bidang secara langsung adalah jahat". Saya tidak yakin bagaimana hal buruk bisa menjadi lebih buruk jika saya memilih untuk menggunakan bidang alih-alih properti di API itu.
Sedat Kapanoglu

@ssg - Jadikan properti publik sebagai ganti bidang. Jika Anda memperlakukan ini sebagai struct yang tidak akan pernah memiliki kode, maka itu tidak membuat banyak perbedaan apakah Anda menggunakan properti atau bidang. Jika Anda memutuskan untuk memberikannya kode (seperti validasi atau sesuatu), maka Anda pasti ingin membuatnya properti. Setidaknya dengan bidang publik, tidak ada yang akan memiliki ilusi tentang invarian yang ada di struktur ini. Itu harus divalidasi oleh metode persis seperti parameter seharusnya.
Jeffrey L Whitledge

Oh itu benar. Luar biasa! Terima kasih.
Sedat Kapanoglu

8
Saya berpikir bahwa aturan mengekspos bidang-is-jahat berlaku untuk objek dalam desain berorientasi objek. Struct yang saya usulkan hanyalah wadah parameter yang kosong. Karena instingmu adalah untuk pergi dengan sesuatu yang abadi, aku pikir wadah dasar seperti itu mungkin cocok untuk kesempatan ini.
Jeffrey L Whitledge

1
@ JeffreyLWhitledge: Saya benar-benar tidak menyukai gagasan bahwa struct bidang yang terbuka itu jahat. Pernyataan seperti itu menurut saya setara dengan mengatakan bahwa obeng itu jahat dan orang harus menggunakan palu karena ujung obeng adalah kepala paku. Jika seseorang perlu menggerakkan paku, orang harus menggunakan palu, tetapi jika orang perlu menggerakkan sekrup, ia harus menggunakan obeng. Ada banyak situasi di mana struct bidang terbuka adalah alat yang tepat untuk pekerjaan itu. Kebetulan, ada jauh lebih sedikit di mana struct dengan get / set properties adalah alat yang tepat (dalam kebanyakan kasus di mana ...
supercat

6

Bagaimana dengan membuat kelas builder di dalam kelas data Anda. Kelas data akan memiliki semua setter sebagai pribadi dan hanya pembangun yang dapat mengaturnya.

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 Itu adalah solusi yang benar-benar valid tetapi saya pikir itu terlalu banyak perancah untuk mempertahankan keputusan desain yang sederhana. Terutama ketika solusi "readonly struct" adalah "sangat" mendekati ideal.
Sedat Kapanoglu

2
Bagaimana cara mengatasi masalah "terlalu banyak parameter"? Sintaksnya mungkin berbeda, tetapi masalahnya terlihat sama. Ini bukan kritik, saya hanya ingin tahu karena saya tidak terbiasa dengan pola ini.
alexD

1
@alexD ini memecahkan masalah dengan memiliki terlalu banyak parameter fungsi dan menjaga objek tetap. Hanya kelas builder yang dapat mengatur properti pribadi dan setelah Anda mendapatkan objek parameter Anda tidak dapat mengubahnya. Masalahnya adalah itu membutuhkan banyak kode perancah.
marto

5
Objek parameter dalam solusi Anda tidak berubah. Seseorang yang menyimpan pembuat dapat mengedit parameter bahkan setelah membangun
astef

1
Untuk titik @ astef, saat ini sedang ditulis sebuah DoSomeActionParameters.Builderinstance dapat digunakan untuk membuat dan mengkonfigurasi tepat satu DoSomeActionParametersinstance. Setelah memanggil Build()perubahan selanjutnya pada Builderproperti akan terus memodifikasi properti instance asli DoSomeActionParameters , dan panggilan selanjutnya untuk Build()akan terus mengembalikan DoSomeActionParametersinstance yang sama . Seharusnya memang begitu public DoSomeActionParameters Build() { var oldObj = obj; obj = new DoSomeActionParameters(); return oldObj; }.
BACON

6

Saya bukan programmer C # tetapi saya percaya C # mendukung argumen bernama: (F # tidak dan C # sebagian besar fitur yang kompatibel untuk hal semacam itu) Itu: http://msdn.microsoft.com/en-us/library/dd264739 .aspx # Y342

Jadi memanggil kode asli Anda menjadi:

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

ini tidak memerlukan lebih banyak ruang / pemikiran bahwa pembuatan objek Anda dan memiliki semua manfaat, dari fakta bahwa Anda belum mengubah apa yang terjadi dalam sistem yang sama sekali tidak ada. Anda bahkan tidak perlu melakukan pengkodean ulang apa pun untuk menunjukkan argumen yang disebutkan

Sunting: di sini ada yang artis yang saya temukan tentang itu. http://www.globalnerdy.com/2009/03/12/default-and-named-parameters-in-c-40-sith-lord-in-training/ Saya harus menyebutkan C # 4.0 mendukung argumen bernama, 3.0 tidak


Perbaikan memang. Tetapi ini hanya memecahkan keterbacaan kode dan itu hanya terjadi ketika pengembang memilih untuk menggunakan parameter bernama. Sangat mudah bagi seseorang untuk lupa menentukan nama dan memberikan manfaatnya. Itu tidak membantu ketika menulis kode itu sendiri. misalnya ketika refactoring fungsi menjadi yang lebih kecil dan meneruskan data sekitar dalam satu paket berisi.
Sedat Kapanoglu

6

Mengapa tidak membuat antarmuka yang menegakkan imutabilitas (yaitu hanya getter)?

Ini pada dasarnya solusi pertama Anda, tetapi Anda memaksa fungsi untuk menggunakan antarmuka untuk mengakses parameter.

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

dan deklarasi fungsi menjadi:

public ResultEnum DoSomeAction(IDoSomeActionParameters parameters, out Guid code)

Pro:

  • Tidak memiliki masalah ruang stack seperti structsolusi
  • Solusi alami menggunakan semantik bahasa
  • Kekekalan sudah jelas
  • Fleksibel (konsumen dapat menggunakan kelas yang berbeda jika dia mau)

Cons:

  • Beberapa karya berulang (deklarasi yang sama dalam dua entitas yang berbeda)
  • Pengembang harus menebak itu DoSomeActionParametersadalah kelas yang bisa dipetakanIDoSomeActionParameters

+1 Saya tidak tahu mengapa saya tidak memikirkan itu? :) Saya kira saya pikir objek masih akan menderita konstruktor dengan terlalu banyak parameter tetapi bukan itu masalahnya. Ya itu solusi yang sangat valid juga. Satu-satunya masalah yang dapat saya pikirkan adalah itu tidak benar-benar mudah bagi pengguna API untuk menemukan nama kelas yang tepat yang mendukung antarmuka yang diberikan. Solusi yang membutuhkan dokumentasi paling sedikit lebih baik.
Sedat Kapanoglu

Saya suka yang ini, duplikasi dan pengetahuan peta ditangani dengan resharper, dan saya bisa memberikan nilai default menggunakan konstruktor default kelas beton
Anthony Johnston

2
Saya tidak suka pendekatan itu. Seseorang yang memiliki referensi untuk implementasi sewenang-wenang IDoSomeActionParametersdan ingin menangkap nilai-nilai di dalamnya tidak memiliki cara untuk mengetahui apakah memegang referensi akan cukup, atau jika harus menyalin nilai-nilai ke objek lain. Antarmuka yang dapat dibaca bermanfaat dalam beberapa konteks, tetapi bukan sebagai alat untuk membuat hal-hal yang tidak berubah.
supercat

"Parameter IDoSomeActionParameters" dapat dilemparkan ke DoSomeActionParameters dan diubah. Pengembang mungkin bahkan tidak menyadari bahwa mereka menghindari upaya untuk membuat parameter dapat diganti
Joseph Simpson

3

Saya tahu ini adalah pertanyaan lama tapi saya pikir saya akan mengemukakan saran saya karena saya baru saja menyelesaikan masalah yang sama. Sekarang, dapat diterima masalah saya sedikit berbeda dengan Anda karena saya memiliki persyaratan tambahan karena tidak ingin pengguna dapat membuat objek ini sendiri (semua hidrasi data berasal dari database, sehingga saya dapat mem-jailbreak semua konstruksi secara internal). Ini memungkinkan saya untuk menggunakan konstruktor pribadi dan pola berikut;

    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

Anda bisa menggunakan pendekatan gaya Builder, meskipun tergantung pada kompleksitas DoSomeActionmetode Anda , ini mungkin merupakan sentuhan kelas berat. Sesuatu di sepanjang garis ini:

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 mengalahkan saya ke saran pembangun!
Chris White

2

Selain respons manji - Anda mungkin juga ingin membagi satu operasi menjadi beberapa yang lebih kecil. Membandingkan:

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

dan

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

Bagi mereka yang tidak tahu POSIX penciptaan anak bisa semudah:

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

Setiap pilihan desain mewakili trade-off atas operasi apa yang mungkin dilakukan.

PS. Ya - mirip dengan builder - hanya secara terbalik (yaitu di sisi callee dan bukan penelepon). Mungkin atau mungkin tidak lebih baik daripada pembangun dalam kasus khusus ini.


Ini adalah operasi kecil tapi itu rekomendasi yang bagus untuk siapa saja yang melanggar prinsip tanggung jawab tunggal. Pertanyaan saya terjadi tepat setelah semua perbaikan desain lainnya dilakukan.
Sedat Kapanoglu

UPS. Maaf - Saya siap pertanyaan sebelum edit Anda dan mempostingnya nanti - karenanya saya tidak menyadarinya.
Maciej Piechotka

2

di sini ada yang sedikit berbeda dari Mikeys tetapi apa yang saya coba lakukan adalah membuat semuanya sesedikit mungkin untuk menulis

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 tidak dapat diubah karena dapat dan tidak dapat dibuat secara langsung karena konstruktor defaultnya adalah pribadi

Penginisialisasi tidak berubah, tetapi hanya transportasi

Penggunaan mengambil keuntungan dari initializer pada Initializer (jika Anda mengerti maksud saya) Dan saya dapat memiliki default di konstruktor default Initializer

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

Parameter akan menjadi opsional di sini, jika Anda ingin beberapa diperlukan Anda bisa meletakkannya di konstruktor default Initializer

Dan validasi bisa masuk dalam metode Buat

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

Jadi sekarang sepertinya

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

Masih sedikit kooki yang saya tahu, tetapi saya akan tetap mencobanya

Sunting: memindahkan metode buat ke statis di objek parameter dan menambahkan delegasi yang melewati initializer mengambil beberapa kookieness keluar dari panggilan

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

Jadi panggilannya sekarang terlihat seperti ini

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

1

Gunakan struktur, tetapi alih-alih bidang publik, memiliki properti publik:

• Semua orang (termasuk FXCop & Jon Skeet) setuju bahwa mengekspos bidang publik adalah buruk.

Jon dan FXCop akan puas karena Anda mengekspos pantasnya bukan bidang.

• Eric Lippert et al mengatakan mengandalkan bidang yang hanya bisa dibaca untuk ketidakberubahan adalah bohong.

Eric akan puas karena menggunakan properti, Anda dapat memastikan bahwa nilainya hanya ditetapkan sekali.

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

Satu objek semi-permanen (nilai dapat diatur tetapi tidak diubah). Bekerja untuk nilai dan tipe Referensi.


+1 Mungkin bidang hanya baca pribadi dan properti pengambil-publik saja dapat digabungkan dalam struct untuk memungkinkan solusi yang kurang bertele-tele.
Sedat Kapanoglu

Properti baca-tulis publik pada struktur yang bermutasi thisjauh lebih jahat daripada bidang publik. Saya tidak yakin mengapa Anda berpikir hanya menetapkan nilai sekali membuat sesuatu "oke"; jika seseorang memulai dengan instance default dari struct, masalah yang terkait dengan thisproperti -mutating masih ada.
supercat

0

Varian dari jawaban Samuel yang saya gunakan dalam proyek saya ketika saya memiliki masalah yang sama:

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

Dan untuk menggunakan:

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

Dalam kasus saya, parameter sengaja dimodifikasi, karena metode setter tidak memungkinkan untuk semua kemungkinan kombinasi, dan hanya mengekspos kombinasi umum dari mereka. Itu karena beberapa parameter saya cukup kompleks dan metode penulisan untuk semua kasus yang mungkin akan menjadi sulit dan tidak perlu (kombinasi gila jarang digunakan).


Ini adalah kelas yang tidak bisa diubah.
Sedat Kapanoglu

@sg - Ya, benar. The DoMagictelah di kontrak meskipun itu tidak akan mengubah objek. Tapi saya rasa itu tidak melindungi dari modifikasi yang tidak disengaja.
Vilx-
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.