Mengapa / kapan Anda harus menggunakan kelas bersarang di .net? Atau tidak?


93

Dalam posting blog Kathleen Dollard tahun 2008 , dia menyajikan alasan menarik untuk menggunakan kelas bersarang di .net. Namun, dia juga menyebutkan bahwa FxCop tidak menyukai kelas bersarang. Saya berasumsi bahwa orang yang menulis aturan FxCop tidak bodoh, jadi pasti ada alasan di balik posisi itu, tetapi saya belum dapat menemukannya.



Seperti yang ditunjukkan oleh nawfal , teman kita Eric Lippert menjawab duplikat dari pertanyaan ini di sini , jawaban yang dimaksud dimulai dengan, " Gunakan kelas bersarang saat Anda membutuhkan kelas pembantu yang tidak ada artinya di luar kelas; terutama saat kelas bertingkat dapat menggunakan detail implementasi privat kelas luar.¶ Argumen Anda bahwa kelas bersarang tidak berguna juga merupakan argumen bahwa metode privat tidak berguna ... "
ruffin

Jawaban:


96

Gunakan kelas bertingkat jika kelas yang Anda susun hanya berguna untuk kelas yang melingkupi. Misalnya, kelas bertingkat memungkinkan Anda menulis sesuatu seperti (disederhanakan):

public class SortedMap {
    private class TreeNode {
        TreeNode left;
        TreeNode right;
    }
}

Anda dapat membuat definisi lengkap tentang kelas Anda di satu tempat, Anda tidak perlu melewati rintangan PIMPL apa pun untuk menentukan cara kerja kelas Anda, dan dunia luar tidak perlu melihat apa pun dari penerapan Anda.

Jika kelas TreeNode adalah eksternal, Anda harus membuat semua bidang publicatau membuat banyak get/setmetode untuk menggunakannya. Dunia luar akan memiliki kelas lain yang mencemari akal sehat mereka.


44
Untuk menambahkan ini: Anda juga dapat menggunakan kelas parsial dalam file terpisah untuk mengelola kode Anda sedikit lebih baik. Letakkan kelas dalam dalam file terpisah (SortedMap.TreeNode.cs dalam kasus ini). Ini harus menjaga kode Anda tetap bersih, sekaligus menjaga kode Anda tetap terpisah :)
Erik van Brakel

1
Akan ada kasus di mana Anda perlu membuat kelas bersarang menjadi publik atau internal jika kelas itu digunakan dalam jenis kembalian dari API publik atau properti publik dari kelas penampung. Saya tidak yakin apakah ini praktik yang baik. Kasus seperti itu mungkin lebih masuk akal untuk mengeluarkan kelas bersarang di luar kelas kontainer. Kelas System.Windows.Forms.ListViewItem.ListViewSubItem dalam kerangka .Net adalah salah satu contohnya.
RBT

16

Dari Tutorial Java Sun:

Mengapa Menggunakan Kelas Bersarang? Ada beberapa alasan kuat untuk menggunakan kelas bersarang, di antaranya:

  • Ini adalah cara pengelompokan kelas secara logis yang hanya digunakan di satu tempat.
  • Ini meningkatkan enkapsulasi.
  • Kelas bertingkat dapat menghasilkan kode yang lebih mudah dibaca dan dipelihara.

Pengelompokan kelas secara logis — Jika sebuah kelas hanya berguna untuk satu kelas lain, maka logis untuk menyematkannya di kelas tersebut dan menjaga keduanya bersama-sama. Menyusun "kelas pembantu" seperti itu membuat paket mereka lebih efisien.

Enkapsulasi yang ditingkatkan — Pertimbangkan dua kelas tingkat atas, A dan B, di mana B memerlukan akses ke anggota A yang dinyatakan sebagai pribadi. Dengan menyembunyikan kelas B di dalam kelas A, anggota A dapat dinyatakan sebagai pribadi dan B dapat mengaksesnya. Selain itu, B sendiri bisa disembunyikan dari dunia luar.<- Ini tidak berlaku untuk implementasi C # kelas bersarang, ini hanya berlaku untuk Java.

Kode yang lebih mudah dibaca dan dapat dipelihara — Menyusun kelas kecil dalam kelas tingkat atas akan menempatkan kode lebih dekat ke tempatnya digunakan.


1
Ini tidak benar-benar berlaku, karena Anda tidak dapat mengakses variabel instan dari kelas penutup di C # seperti yang Anda bisa di Java. Hanya anggota statis yang dapat diakses.
Ben Baron

5
Namun, jika Anda meneruskan sebuah instance dari kelas yang melingkupi ke dalam kelas yang disarangkan, kelas yang disarangkan memiliki akses penuh ke semua anggota melalui variabel instance itu ... jadi sungguh, ini seperti Java membuat variabel instance tersirat, sedangkan di C # Anda harus buat itu eksplisit.
Alex

@Alex Tidak, di Java kelas bertingkat benar-benar menangkap instance kelas induk saat dibuat instance-nya - antara lain hal ini berarti mencegah induk dari pengumpulan sampah. Ini juga berarti kelas bersarang tidak dapat dibuat instance-nya tanpa kelas induk. Jadi tidak, ini sama sekali tidak sama.
Tomáš Zato - Kembalikan Monica

2
@ TomášZato Deskripsi saya cukup tepat, sungguh. Secara efektif ada variabel contoh induk implisit dalam kelas bersarang di Java, sementara di C #, Anda harus secara eksplisit menyerahkan kelas dalam contoh. Konsekuensi dari ini, seperti yang Anda katakan, adalah bahwa kelas-kelas dalam Java harus memiliki instance induk, sedangkan C # tidak. Poin utama saya dalam hal apapun adalah bahwa kelas dalam C # juga dapat mengakses ke bidang dan properti pribadi orang tuanya, tetapi itu harus melewati contoh induk secara eksplisit untuk dapat melakukannya.
Alex

9

Pola tunggal yang sepenuhnya Malas dan aman untuk benang

public sealed class Singleton
{
    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return Nested.instance;
        }
    }

    class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}

sumber: http://www.yoda.arachsys.com/csharp/singleton.html


5

Itu tergantung pada penggunaannya. Saya jarang akan menggunakan kelas bersarang Publik tetapi menggunakan kelas bersarang Pribadi sepanjang waktu. Kelas bertingkat privat bisa digunakan untuk sub-objek yang dimaksudkan untuk digunakan hanya di dalam induk. Contohnya adalah jika kelas HashTable berisi objek Entri pribadi untuk menyimpan data hanya secara internal.

Jika kelas dimaksudkan untuk digunakan oleh pemanggil (secara eksternal), saya biasanya suka membuatnya menjadi kelas mandiri yang terpisah.


5

Selain alasan lain yang tercantum di atas, ada satu alasan lagi yang dapat saya pikirkan tidak hanya untuk menggunakan kelas bersarang, tetapi pada kenyataannya kelas bersarang publik. Bagi mereka yang bekerja dengan beberapa kelas generik yang berbagi parameter tipe generik yang sama, kemampuan untuk mendeklarasikan namespace generik akan sangat berguna. Sayangnya, .Net (atau setidaknya C #) tidak mendukung gagasan ruang nama generik. Jadi untuk mencapai tujuan yang sama, kita dapat menggunakan kelas generik untuk memenuhi tujuan yang sama. Ambil contoh kelas berikut yang terkait dengan entitas logis:

public  class       BaseDataObject
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  class       BaseDataObjectList
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
:   
                    CollectionBase<tDataObject>
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  interface   IBaseBusiness
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  interface   IBaseDataAccess
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

Kita dapat menyederhanakan tanda tangan dari kelas-kelas ini dengan menggunakan namespace generik (diimplementasikan melalui kelas-kelas bersarang):

public
partial class   Entity
                <
                    tDataObject, 
                    tDataObjectList, 
                    tBusiness, 
                    tDataAccess
                >
        where   tDataObject     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
        where   tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
        where   tBusiness       : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
        where   tDataAccess     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{

    public  class       BaseDataObject {}

    public  class       BaseDataObjectList : CollectionBase<tDataObject> {}

    public  interface   IBaseBusiness {}

    public  interface   IBaseDataAccess {}

}

Kemudian, melalui penggunaan kelas parsial seperti yang disarankan oleh Erik van Brakel dalam komentar sebelumnya, Anda dapat memisahkan kelas menjadi file bersarang yang terpisah. Saya merekomendasikan menggunakan ekstensi Visual Studio seperti NestIn untuk mendukung penumpukan file kelas parsial. Hal ini memungkinkan file kelas "namespace" juga digunakan untuk mengatur file kelas bersarang dalam folder seperti cara.

Sebagai contoh:

Entity.cs

public
partial class   Entity
                <
                    tDataObject, 
                    tDataObjectList, 
                    tBusiness, 
                    tDataAccess
                >
        where   tDataObject     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
        where   tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
        where   tBusiness       : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
        where   tDataAccess     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{
}

Entity.BaseDataObject.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  class   BaseDataObject
    {

        public  DataTimeOffset  CreatedDateTime     { get; set; }
        public  Guid            CreatedById         { get; set; }
        public  Guid            Id                  { get; set; }
        public  DataTimeOffset  LastUpdateDateTime  { get; set; }
        public  Guid            LastUpdatedById     { get; set; }

        public
        static
        implicit    operator    tDataObjectList(DataObject dataObject)
        {
            var returnList  = new tDataObjectList();
            returnList.Add((tDataObject) this);
            return returnList;
        }

    }

}

Entity.BaseDataObjectList.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  class   BaseDataObjectList : CollectionBase<tDataObject>
    {

        public  tDataObjectList ShallowClone() 
        {
            var returnList  = new tDataObjectList();
            returnList.AddRange(this);
            return returnList;
        }

    }

}

Entity.IBaseBusiness.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  interface   IBaseBusiness
    {
        tDataObjectList Load();
        void            Delete();
        void            Save(tDataObjectList data);
    }

}

Entity.IBaseDataAccess.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  interface   IBaseDataAccess
    {
        tDataObjectList Load();
        void            Delete();
        void            Save(tDataObjectList data);
    }

}

File di penjelajah solusi studio visual kemudian akan diatur sebagai berikut:

Entity.cs
+   Entity.BaseDataObject.cs
+   Entity.BaseDataObjectList.cs
+   Entity.IBaseBusiness.cs
+   Entity.IBaseDataAccess.cs

Dan Anda akan menerapkan namespace generik seperti berikut:

User.cs

public
partial class   User
:
                Entity
                <
                    User.DataObject, 
                    User.DataObjectList, 
                    User.IBusiness, 
                    User.IDataAccess
                >
{
}

User.DataObject.cs

partial class   User
{

    public  class   DataObject : BaseDataObject 
    {
        public  string  UserName            { get; set; }
        public  byte[]  PasswordHash        { get; set; }
        public  bool    AccountIsEnabled    { get; set; }
    }

}

User.DataObjectList.cs

partial class   User
{

    public  class   DataObjectList : BaseDataObjectList {}

}

User.IBusiness.cs

partial class   User
{

    public  interface   IBusiness : IBaseBusiness {}

}

User.IDataAccess.cs

partial class   User
{

    public  interface   IDataAccess : IBaseDataAccess {}

}

Dan file akan diatur dalam penjelajah solusi sebagai berikut:

User.cs
+   User.DataObject.cs
+   User.DataObjectList.cs
+   User.IBusiness.cs
+   User.IDataAccess.cs

Di atas adalah contoh sederhana menggunakan kelas luar sebagai namespace generik. Saya telah membangun "ruang nama umum" yang berisi 9 atau lebih parameter tipe di masa lalu. Harus menjaga agar parameter jenis tersebut tetap tersinkronisasi di sembilan jenis yang semuanya diperlukan untuk mengetahui parameter jenis itu membosankan, terutama saat menambahkan parameter baru. Penggunaan namespace generik membuat kode itu jauh lebih mudah diatur dan dibaca.


3

Jika saya memahami artikel Katheleen dengan benar, dia mengusulkan untuk menggunakan kelas bertingkat agar dapat menulis SomeEntity.Collection daripada EntityCollection <SomeEntity>. Menurut saya ini cara kontroversial untuk menghemat beberapa pengetikan. Saya cukup yakin bahwa dalam koleksi aplikasi dunia nyata akan memiliki beberapa perbedaan dalam implementasi, jadi Anda tetap perlu membuat kelas terpisah. Menurut saya, menggunakan nama kelas untuk membatasi ruang lingkup kelas lainnya bukanlah ide yang baik. Ini mencemari intellisense dan memperkuat ketergantungan antar kelas. Menggunakan ruang nama adalah cara standar untuk mengontrol ruang lingkup kelas. Namun saya menemukan bahwa penggunaan kelas bersarang seperti di komentar @hazzen dapat diterima kecuali Anda memiliki banyak kelas bersarang yang merupakan tanda desain yang buruk.


1

Saya sering menggunakan kelas bersarang untuk menyembunyikan detail implementasi. Contoh dari jawaban Eric Lippert di sini:

abstract public class BankAccount
{
    private BankAccount() { }
    // Now no one else can extend BankAccount because a derived class
    // must be able to call a constructor, but all the constructors are
    // private!
    private sealed class ChequingAccount : BankAccount { ... }
    public static BankAccount MakeChequingAccount() { return new ChequingAccount(); }
    private sealed class SavingsAccount : BankAccount { ... }
}

Pola ini menjadi lebih baik dengan penggunaan obat generik. Lihat pertanyaan ini untuk dua contoh keren. Jadi saya akhirnya menulis

Equality<Person>.CreateComparer(p => p.Id);

dari pada

new EqualityComparer<Person, int>(p => p.Id);

Saya juga dapat memiliki daftar umum Equality<Person>tetapi tidakEqualityComparer<Person, int>

var l = new List<Equality<Person>> 
        { 
         Equality<Person>.CreateComparer(p => p.Id),
         Equality<Person>.CreateComparer(p => p.Name) 
        }

dimana

var l = new List<EqualityComparer<Person, ??>>> 
        { 
         new EqualityComparer<Person, int>>(p => p.Id),
         new EqualityComparer<Person, string>>(p => p.Name) 
        }

itu tidak mungkin. Itulah keuntungan dari kelas bersarang yang diturunkan dari kelas induk.

Kasus lain (dengan sifat yang sama - menyembunyikan implementasi) adalah ketika Anda ingin membuat anggota kelas (bidang, properti, dll.) Hanya dapat diakses untuk satu kelas:

public class Outer 
{
   class Inner //private class
   {
       public int Field; //public field
   }

   static inner = new Inner { Field = -1 }; // Field is accessible here, but in no other class
}

1

Penggunaan lain yang belum disebutkan untuk kelas bersarang adalah segregasi tipe generik. Misalnya, seseorang ingin memiliki beberapa keluarga generik kelas statis yang dapat menggunakan metode dengan berbagai jumlah parameter, bersama dengan nilai untuk beberapa parameter tersebut, dan menghasilkan delegasi dengan lebih sedikit parameter. Misalnya, seseorang ingin memiliki metode statis yang dapat mengambil Action<string, int, double>dan menghasilkan a String<string, int>yang akan memanggil tindakan yang diberikan lewat 3.5 sebagai double; seseorang mungkin juga ingin memiliki metode statis yang dapat mengambil Action<string, int, double>dan menghasilkan Action<string>, lewat 7sebagaiint dan 5.3sebagai double. Menggunakan kelas bertingkat generik, seseorang dapat mengatur agar pemanggilan metode menjadi seperti:

MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5);
MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);

atau, karena tipe terakhir di setiap ekspresi dapat disimpulkan meskipun ekspresi sebelumnya tidak dapat:

MakeDelegate<string,int>.WithParams(theDelegate, 3.5);
MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);

Menggunakan tipe generik bersarang memungkinkan untuk mengetahui delegasi mana yang berlaku untuk bagian mana dari keseluruhan deskripsi tipe.


1

Kelas bersarang dapat digunakan untuk kebutuhan berikut:

  1. Klasifikasi data
  2. Ketika logika kelas utama rumit dan Anda merasa membutuhkan objek bawahan untuk mengelola kelas
  3. Ketika Anda menyatakan bahwa status dan keberadaan kelas sepenuhnya bergantung pada kelas yang melingkupinya


0

Saya suka mengelompokkan pengecualian yang unik untuk satu kelas, yaitu. yang tidak pernah terlempar dari tempat lain.

Sebagai contoh:

public class MyClass
{
    void DoStuff()
    {
        if (!someArbitraryCondition)
        {
            // This is the only class from which OhNoException is thrown
            throw new OhNoException(
                "Oh no! Some arbitrary condition was not satisfied!");
        }
        // Do other stuff
    }

    public class OhNoException : Exception
    {
        // Constructors calling base()
    }
}

Ini membantu menjaga file proyek Anda tetap rapi dan tidak penuh dengan seratus kelas pengecualian kecil yang gemuk.


0

Ingatlah bahwa Anda harus menguji kelas bertingkat. Jika bersifat pribadi, Anda tidak akan dapat mengujinya secara terpisah.

Anda bisa membuatnya internal, sehubungan dengan InternalsVisibleToatributnya . Namun, ini akan sama dengan membuat bidang pribadi internal hanya untuk tujuan pengujian, yang saya anggap sebagai dokumentasi diri yang buruk.

Jadi, Anda mungkin hanya ingin mengimplementasikan kelas bertingkat privat yang melibatkan kompleksitas rendah.


0

ya untuk kasus ini:

class Join_Operator
{

    class Departamento
    {
        public int idDepto { get; set; }
        public string nombreDepto { get; set; }
    }

    class Empleado
    {
        public int idDepto { get; set; }
        public string nombreEmpleado { get; set; }
    }

    public void JoinTables()
    {
        List<Departamento> departamentos = new List<Departamento>();
        departamentos.Add(new Departamento { idDepto = 1, nombreDepto = "Arquitectura" });
        departamentos.Add(new Departamento { idDepto = 2, nombreDepto = "Programación" });

        List<Empleado> empleados = new List<Empleado>();
        empleados.Add(new Empleado { idDepto = 1, nombreEmpleado = "John Doe." });
        empleados.Add(new Empleado { idDepto = 2, nombreEmpleado = "Jim Bell" });

        var joinList = (from e in empleados
                        join d in departamentos on
                        e.idDepto equals d.idDepto
                        select new
                        {
                            nombreEmpleado = e.nombreEmpleado,
                            nombreDepto = d.nombreDepto
                        });
        foreach (var dato in joinList)
        {
            Console.WriteLine("{0} es empleado del departamento de {1}", dato.nombreEmpleado, dato.nombreDepto);
        }
    }
}

Mengapa? Tambahkan beberapa konteks ke kode dalam solusi Anda untuk membantu pembaca di masa mendatang memahami alasan di balik jawaban Anda.
Grant Miller

0

Berdasarkan pemahaman saya tentang konsep ini, kita dapat menggunakan fitur ini ketika kelas terkait satu sama lain secara konseptual. Maksud saya beberapa dari mereka adalah satu Item lengkap dalam bisnis kami seperti entitas yang ada di dunia DDD yang membantu objek akar agregat untuk melengkapi logika bisnisnya.

Untuk memperjelas, saya akan menunjukkan ini melalui contoh:

Bayangkan kita memiliki dua kelas seperti Order dan OrderItem. Di kelas pesanan, kami akan mengelola semua orderItems dan di OrderItem kami menyimpan data tentang satu pesanan untuk klarifikasi, Anda dapat melihat kelas di bawah ini:

 class Order
    {
        private List<OrderItem> _orderItems = new List<OrderItem>();

        public void AddOrderItem(OrderItem line)
        {
            _orderItems.Add(line);
        }

        public double OrderTotal()
        {
            double total = 0;
            foreach (OrderItem item in _orderItems)
            {
                total += item.TotalPrice();
            }

            return total;
        }

        // Nested class
        public class OrderItem
        {
            public int ProductId { get; set; }
            public int Quantity { get; set; }
            public double Price { get; set; }
            public double TotalPrice() => Price * Quantity;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            Order order = new Order();

            Order.OrderItem orderItem1 = new Order.OrderItem();
            orderItem1.ProductId = 1;
            orderItem1.Quantity = 5;
            orderItem1.Price = 1.99;
            order.AddOrderItem(orderItem1);

            Order.OrderItem orderItem2 = new Order.OrderItem();
            orderItem2.ProductId = 2;
            orderItem2.Quantity = 12;
            orderItem2.Price = 0.35;
            order.AddOrderItem(orderItem2);

            Console.WriteLine(order.OrderTotal());
            ReadLine();
        }


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