Bir “Çalışan” sınıfı nasıl tasarlanmalıdır?


11

Çalışanları yönetmek için bir program oluşturmaya çalışıyorum. Ancak Employeesınıfı nasıl tasarlayacağımı anlayamıyorum. Amacım, bir Employeenesne kullanarak veritabanındaki çalışan verilerini oluşturmak ve işlemek .

Düşündüğüm temel uygulama bu kadar basitti:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

Bu uygulamayı kullanarak çeşitli sorunlarla karşılaştım.

  1. IDBir çalışanın yeni bir çalışanı tanımlamak için nesne kullanmak eğer öyleyse, veritabanı tarafından verilir, hiçbir olacak IDvarolan çalışanı temsil eden bir nesne ise, henüz saklamak olacaktır bir var ID. Bu yüzden bazen nesneyi tanımlayan ve bazen de olmayan bir mülküm var (Bu, SRP'yi ihlal ettiğimizi gösterebilir mi? Yeni ve mevcut çalışanları temsil etmek için aynı sınıfı kullandığımız için ...).
  2. Createİse yöntem, veritabanı üzerinde bir çalışan oluşturmak gerekiyordu Updateve Deletevarolan çalışanın (Yine hareket etmeye gerekiyor SRP ...).
  3. 'Create' yönteminin hangi parametreleri olması gerekir? Tüm çalışan verileri veya belki bir Employeenesne için düzinelerce parametre ?
  4. Sınıf değişmez olmalı mı?
  5. İş nasıl olacak Update? Özellikleri alıp veritabanını güncelleyecek mi? Ya da belki "eski" bir "yeni" bir nesne olmak üzere iki nesne alacak ve veritabanını aralarındaki farklarla güncelleyecek? (Bence cevap sınıfın değişebilirliğiyle ilgili cevapla ilgilidir).
  6. Yapıcının sorumluluğu ne olurdu? Alınan parametreler ne olurdu? Bir idparametre kullanarak çalışan verilerini veritabanından alır ve özellikleri doldurur mu?

Gördüğünüz gibi, kafamda biraz karışıklık var ve kafam çok karıştı. Böyle bir sınıfın nasıl olması gerektiğini anlamama yardım eder misiniz?

Sadece sık kullanılan bir sınıfın genel olarak nasıl tasarlandığını anlamak için fikir istemediğimi lütfen unutmayın.


3
SRP'yi şu anda ihlal etmeniz, hem bir varlığı temsil eden hem de CRUD mantığından sorumlu olan bir sınıfınız olmasıdır. Ayırırsanız, bu CRUD işlemleri ve varlık yapısı farklı sınıflar olacaktır, o zaman 1. ve 2. SRP'yi kesmeyin. 3.Employee soyutlama sağlamak için bir nesne almalıdır , 4. ve 5. sorular genellikle cevapsızdır, ihtiyaçlarınıza bağlıdır ve yapı ve CRUD işlemlerini iki sınıfa ayırırsanız, o zaman açıktır, Employeeveri getiremez artık db'den, böylece cevaplar 6.
Andy

@DavidPacker - Teşekkürler. Buna bir cevap verebilir misiniz?
Sipo

5
Tekrar ediyorum, ctor'unuzun veritabanına ulaşmasını istemiyorum . Bunu sıkıca yapmak veritabanına kodu çiftler ve şeylerin tanrıyı test etmesini zorlaştırır (manuel testler bile zorlaşır). Havuz desenine bakın. Bir saniyeliğine düşün, Updatebir çalışan mısın yoksa bir çalışan kaydını mı güncelliyorsun? Yapar mısın Employee.Delete()yoksa yapar Boss.Fire(employee)mısın?
RubberDuck

1
Daha önce bahsedilenlerin yanı sıra, bir çalışan oluşturmak için bir çalışana ihtiyacınız olması sizin için anlamlı mı? Etkin kayıtta, bir Çalışanı yeni kurmak ve daha sonra bu nesne üzerinde Kaydet'i çağırmak daha mantıklı olabilir. O zaman bile, şimdi iş mantığından ve kendi veri kalıcılığından sorumlu bir sınıfınız var.
Bay Cochese

Yanıtlar:


10

Bu, sorunuzun altındaki ilk yorumumun daha iyi oluşturulmuş bir transkripsiyonudur. OP tarafından yöneltilen soruların cevapları bu cevabın alt kısmında bulunabilir. Lütfen aynı yerde bulunan önemli notu da kontrol edin .


Şu anda açıkladığınız şey, Sipo, Aktif kayıt adı verilen bir tasarım modelidir . Her şeyde olduğu gibi, bu bile programcılar arasında yerini buldu, ancak basit bir nedenden dolayı ölçeklenebilirlik için depo ve veri eşleme desenleri lehine atıldı .

Kısacası, aktif bir kayıt bir nesnedir:

  • alan adınızdaki bir nesneyi temsil eder (iş kurallarını içerir, bir kullanıcı adını değiştirip değiştiremeyeceğiniz gibi bir nesne üzerinde nasıl işlem yapılacağını bilir),
  • Varlığın nasıl alınacağını, güncelleneceğini, kaydedileceğini ve silineceğini bilir.

Mevcut tasarımınızla ilgili birçok sorunu ele alıyorsunuz ve tasarımınızın ana problemi son 6'ncı noktaya (son fakat en az değil, sanırım) değiniyor. Bir kurucu tasarladığınız bir sınıfınız olduğunda ve kurucunun ne yapması gerektiğini bile bilmiyorsanız, sınıf muhtemelen yanlış bir şey yapıyor demektir. Bu senin davanda oldu.

Ancak tasarımın düzeltilmesi aslında varlık temsilini ve CRUD mantığını iki (veya daha fazla) sınıfa bölerek oldukça basittir.

Tasarımınız şu şekilde görünüyor:

  • Employee- çalışan yapısı (öznitelikleri) hakkında bilgi içerir ve varlığı nasıl değiştireceğinize ilişkin yöntemler (değişebilir şekilde gitmeye karar verirseniz), Employeevarlık için CRUD mantığı içerir Employee, bir Employeenesne listesi döndürebilir , istediğiniz zaman bir nesneyi kabul edebilir bir çalışanı güncelleyin, tek Employeebir yöntemlegetSingleById(id : string) : Employee

Vay, sınıf çok büyük görünüyor.

Önerilen çözüm bu olacaktır:

  • Employee - çalışan yapısı (öznitelikleri) ve varlığın nasıl değiştirilebileceği hakkında yöntemler (değişebilir yoldan gitmeye karar verirseniz) hakkında bilgi içerir
  • EmployeeRepository- için CRUD mantığı içerir Employeelistesini döndürebilir, varlık Employeebir kabul, nesneler Employeetek bir dönebilir, bir çalışan güncellemek istediğinizde nesne Employeegibi bir yöntemlegetSingleById(id : string) : Employee

Endişelerin ayrıldığını duydunuz mu? Hayır, şimdi yapacaksın. Tek Sorumluluk İlkesinin daha az katı bir versiyonudur ve bir sınıfın aslında sadece bir sorumluluğu olması gerektiğini veya Bob Amca'nın dediği gibi:

Bir modülün değiştirmek için bir ve tek nedeni olmalıdır.

Başlangıç ​​sınıfınızı hala iyi bir yuvarlak arabirime sahip olan ikiye bölebilirsem, ilk sınıf muhtemelen çok fazla şey yapıyormuş gibi görünüyor.

Depo deseni hakkında harika olan şey, sadece veritabanı (orta, katman, dosya, noSQL, SQL, nesne yönelimli olabilir) arasında orta bir katman sağlamak için bir soyutlama işlevi görmez, aynı zamanda somut olması bile gerekmez. sınıf. Birçok OO dilinde, arayüzü gerçek interface(veya C ++ kullanıyorsanız saf sanal yöntemle bir sınıf) olarak tanımlayabilir ve daha sonra birden çok uygulamaya sahip olabilirsiniz.

Bu, bir havuzun gerçek bir uygulaması olup olmadığı kararını tamamen kaldırır ve interfaceanahtar kelimeye sahip bir yapıya güvenerek arayüze güvenirsiniz . Ve depo tam olarak budur, veri katmanı soyutlaması için süslü bir terimdir, yani verileri alan adınızla eşleştirir ve tersi de geçerlidir.

(En azından) iki sınıfa ayırmakla ilgili bir başka harika şey, artık Employeesınıfın kendi verilerini açıkça yönetebilmesi ve çok iyi yapabilmesidir, çünkü diğer zor şeylerle ilgilenmesine gerek yoktur.

Soru 6: Oluşturucu yeni oluşturulan Employeesınıfta ne yapmalı ? Basit. Bağımsız değişkenleri almalı, geçerli olup olmadıklarını kontrol etmeli (örneğin, yaş muhtemelen negatif olmamalı veya ad boş olmamalıdır), veriler geçersiz olduğunda bir hata ortaya koymalı ve doğrulama başarılıysa bağımsız değişkenlere bağımsız değişkenler atamalıdır varlığın. Artık veritabanıyla iletişim kuramıyor çünkü nasıl yapılacağı hakkında hiçbir fikri yok.


Soru 4: Genel olarak değil, cevaplanamaz çünkü cevap büyük ölçüde tam olarak neye ihtiyacınız olduğuna bağlıdır.


Soru 5: Şimdi ikiye şişirilmiş sınıf ayrılmış göre, artık doğrudan çoklu güncelleme yöntemleri olabilir Employeesınıfına gibi changeUsername, markAsDeceasedait verileri işlemek hangi Employeesınıfta sadece RAM içinde ve sonra gibi bir yöntem tanıtmak olabilir registerDirtydan Deponun bu nesnenin özellikleri değiştirdiğini ve commityöntemi çağırdıktan sonra güncelleştirilmesi gerektiğini bildireceği depo sınıfına yönelik Çalışma Birimi deseni .

Açıkçası, bir güncelleme için bir nesnenin bir kimliğe sahip olması ve bu nedenle zaten kaydedilmesi gerekir ve bu, ölçütlere uyulmadığında bunu algılamak ve bir hata oluşturmak için deponun sorumluluğudur.


Soru 3: İş Birimi modeli ile gitmeye karar verirseniz, createyöntem şimdi olacaktır registerNew. Eğer yoksa, muhtemelen çağırır saveyerine. Bir havuzun amacı, etki alanı ve veri katmanı arasında bir soyutlama sağlamaktır, çünkü bu nedenle bu yöntemin (ister registerNewveya ister save) Employeenesneyi kabul etmesini ve özniteliklerin depo arayüzünü uygulayan sınıflara bağlı olmasını öneririm. kuruluştan çıkarılmaya karar verirler. Bir nesnenin tamamını iletmek daha iyidir, bu nedenle birçok isteğe bağlı parametreye sahip olmanız gerekmez.


Soru 2: Artık her iki yöntem de depo arayüzünün bir parçası olacak ve tek sorumluluk ilkesini ihlal etmeyecekler. Deponun sorumluluğu, Employeenesneler için CRUD işlemleri sağlamaktır , yani yaptığı şeydir (Okuma ve Silme yanında, CRUD hem Oluşturma hem de Güncelleme anlamına gelir). Açıkçası, depoyu EmployeeUpdateRepositoryve benzerlerini kullanarak daha da bölebilirsiniz , ancak bu nadiren gereklidir ve tek bir uygulama genellikle tüm CRUD işlemlerini içerebilir.


Soru 1:Employee Artık (diğer niteliklerin yanı sıra) kimliğine sahip olacak basit bir sınıfla sonuçlandınız. Kimliğin dolu veya boş (veya null) olup olmadığı, nesnenin önceden kaydedilmiş olmasına bağlıdır. Bununla birlikte, bir kimlik hala işletmenin sahip olduğu bir niteliktir ve işletmenin sorumluluğu Employeeniteliklerine dikkat etmek ve dolayısıyla kimliğine dikkat etmektir.

Bir varlığın kimliğe sahip olup olmadığı, üzerinde bazı kalıcılık mantığı yapmaya çalışana kadar genellikle önemli değildir. 5. sorunun cevabında belirtildiği gibi, daha önce kaydedilmiş bir varlığı kaydetmeye çalışmadığınızı veya kimliği olmayan bir varlığı güncellemeye çalıştığınızı saptamak deponun sorumluluğundadır.


Önemli Not

Endişelerin ayrılması büyük olsa da, işlevsel bir depo katmanı tasarlamanın oldukça sıkıcı bir iş olduğunu ve benim deneyimime göre, aktif kayıt yaklaşımından daha doğru bir şekilde elde edilmesi biraz daha zor olduğunu lütfen unutmayın. Ancak, çok daha esnek ve ölçeklenebilir bir tasarıma sahip olacaksınız, bu da iyi bir şey olabilir.


Cevabımla aynı hmm, ama 'edgey' tonları koyduğu
Ewan

2
@Ewan Cevabınızı küçümsemedim, ama bazılarının neden olabileceğini görebiliyorum. OP'nin bazı sorularına doğrudan cevap vermez ve bazı önerileriniz asılsız görünür.
Andy

1
Güzel ve kapsamlı bir cevap. Endişe ayracı ile kafasına çivi vurur. Ve mükemmel bir karmaşık tasarım ve hoş bir uzlaşma arasında yapılması gereken önemli seçimi belirten uyarıyı seviyorum.
Christophe

Doğru, cevabınız üstündür
Ewan

ilk olarak yeni bir çalışan nesnesi oluşturduğunuzda, kimliğin değeri olmayacaktır. id alanı null değerle ayrılabilir, ancak çalışan nesnesinin geçersiz durumda olmasına neden olur ????
Susantha7

2

Önce kavramsal çalışanın özelliklerini içeren bir çalışan yapısı oluşturun.

Sonra eşleşen tablo yapısına sahip bir veritabanı oluşturun, örneğin mssql

Sonra bir çalışan havuzu oluşturun Bu veritabanı için EmployeeRepoMsSql için ihtiyacınız olan çeşitli CRUD işlemleri.

Sonra CRUD işlemlerini gösteren bir IEmployeeRepo arabirimi oluşturun

Ardından Çalışan yapınızı IEmployeeRepo'nun inşaat parametresiyle bir sınıfa genişletin. İstediğiniz çeşitli Kaydet / Sil vb yöntemlerini ekleyin ve bunları uygulamak için enjekte edilen EmployeeRepo'yu kullanın.

Id koni olduğunda ben yapıcı kodu ile oluşturulabilir bir GUID kullanmanızı öneririz.

Varolan nesnelerle çalışmak için, kodunuz Güncelleme Yöntemini çağırmadan önce onları veri havuzu aracılığıyla veritabanından alabilir.

Alternatif olarak kaşlarını çattı (ama benim görüşüme göre daha üstün), CRUD yöntemlerini nesnenize eklemediğiniz Anemic Domain Object modelini seçebilir ve nesneyi güncellenecek / kaydedilecek / silinecek repoya geçirebilirsiniz.

Değişmezlik, desenlerinize ve kodlama stilinize bağlı olacak bir tasarım seçimidir. Eğer tüm işlevsel olacak o zaman da değişmez olmaya çalışın. Ancak, değişken bir nesnenin uygulanması muhtemelen daha kolaydır.

Create () yerine Save () ile giderdim. Değişmezlik kavramı ile çalışır, ancak her zaman henüz 'Kaydedilmemiş' bir nesne inşa etmeyi yararlı buluyorum, örneğin bir çalışan nesnesini veya nesnelerini doldurmanıza ve daha sonra bunları bazı kuralları tekrar doğrulamanıza izin veren bazı kullanıcı arayüzünüz var veritabanına kaydetme.

***** örnek kod

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}

1
Anemik Alan Modeli'nin CRUD mantığıyla çok az ilgisi vardır. Etki alanı katmanına ait olmasına rağmen, hiçbir işlevselliğe sahip olmayan ve tüm işlevlerin, bu etki alanı modelinin parametre olarak iletildiği hizmetler aracılığıyla sunulan bir modeldir.
Andy

Tam olarak, bu durumda repo hizmettir ve işlevler CRUD işlemidir.
Ewan

@DavidPacker, Anemik Alan Modeli'nin iyi bir şey olduğunu mu söylüyorsunuz?
candied_orange

1
@CandiedOrange Yorumdaki fikrimi ifade etmedim, ancak hayır, başvurunuzu bir katmanın sadece iş mantığından sorumlu olduğu katmanlara daldırmaya karar verirseniz, anemik bir etki alanı modeli olan Bay Fowler ile beraberim aslında bir anti-kalıptır. Neden yöntemi doğrudan sınıfa ekleyebileceğim zaman neden UserUpdatebir changeUsername(User user, string newUsername)yöntemle bir hizmete ihtiyacım var . Bunun için bir hizmet yaratmak mantıklı değil. changeUsernameUser
Andy

1
Bu durumda CRUD mantığını Model'e uygun hale getirmek için repo enjekte etmek gerektiğini düşünüyorum.
Ewan

1

Tasarımınızın gözden geçirilmesi

Sizin Employeegerçekte veritabanında ısrarla yönetilen bir nesne için vekil bir türüdür.

Bu nedenle, ID'yi veritabanı nesnenize bir başvuru gibi düşünmenizi öneririm. Bu mantığı göz önünde bulundurarak, geleneksel olmayan kompozisyon mantığını uygulamanıza izin veren kimlik, veritabanı olmayan nesneler için yaptığınız gibi devam edebilirsiniz:

  • Kimlik ayarlanırsa, karşılık gelen bir veritabanı nesneniz vardır.
  • Kimlik ayarlanmamışsa, karşılık gelen bir veritabanı nesnesi yoktur: Employeehenüz oluşturulmamış olabilir veya silinmiş olabilir.
  • Henüz bellekte yüklü olmayan eski çalışanlar ve mevcut veri tabanı kayıtları arasındaki ilişkiyi başlatmak için bazı mekanizmalara ihtiyacınız vardır.

Nesnenin durumunu da yönetmeniz gerekir. Örneğin:

  • bir Çalışan henüz oluşturma veya veri alma yoluyla bir DB nesnesine bağlı değilse, güncelleme veya silme işlemi gerçekleştiremezsiniz.
  • Nesnedeki Çalışan verileri veritabanı ile senkronize mi veya yapılan değişiklikler var mı?

Bunu göz önünde bulundurarak şunları seçebiliriz:

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

Nesne durumunuzu güvenilir bir şekilde yönetebilmek için, özellikleri özel hale getirerek daha iyi bir kapsülleme sağlamalısınız ve yalnızca güncelleme durumunu ayarlayan alıcılar ve ayarlayıcılar aracılığıyla erişim vermelisiniz.

Sorularınız

  1. Bence ID özelliği SRP'yi ihlal etmiyor. Tek sorumluluğu bir veritabanı nesnesine başvurmaktır.

  2. Çalışanınız bir bütün olarak SRP ile uyumlu değildir, çünkü veritabanı ile olan bağlantıdan değil, aynı zamanda geçici değişikliklerin yapılmasından ve o nesneye yapılan tüm işlemlerden de sorumludur.

    Başka bir tasarım, değiştirilebilir alanların yalnızca alanlara erişilmesi gerektiğinde yüklenecek başka bir nesnede tutulması olabilir.

    Veritabanı işlemlerini Çalışan üzerinde komut desenini kullanarak uygulayabilirsiniz . Bu tür tasarım, veritabanına özgü deyimler ve API'ları yalıtarak iş nesneleriniz (Çalışan) ve temel veritabanı sisteminiz arasındaki ayrıştırmayı da kolaylaştıracaktır.

  3. Ben düzinelerce parametre eklemek olmaz Create(), çünkü iş nesneleri gelişebilir ve tüm bu bakımı çok zorlaştırabilir. Ve kod okunamaz hale gelecekti. Burada 2 seçeneğiniz var: ya veritabanında bir çalışan oluşturmak ve güncelleme yoluyla kalan değişiklikleri gerçekleştirmek için kesinlikle gerekli olan minimalist bir parametre kümesini (4'ten fazla değil) geçirerek VEYA bir nesneyi iletirsiniz. Bu arada, tasarımınızda ben önceden seçmiş anlıyoruz: my_employee.Create().

  4. Sınıf değişmez olmalı mı? Yukarıdaki tartışmaya bakınız: orijinal tasarım no. Değişmez bir kimlik seçerdim ama değişmez bir Çalışan değil. Bir Çalışan gerçek hayatta gelişir (yeni iş pozisyonu, yeni adres, yeni evlilik durumu, hatta yeni isimler ...). En azından iş mantığı katmanında bu gerçekliği göz önünde bulundurmanın daha kolay ve doğal olacağını düşünüyorum.

  5. Güncelleme için bir komut ve istenen değişiklikleri tutmak için (GUI?) İçin ayrı bir nesne kullanmayı düşünüyorsanız, eski / yeni yaklaşımı tercih edebilirsiniz. Diğer tüm durumlarda, değiştirilebilir bir nesneyi güncellemeyi tercih ederim. Uyarı: güncelleştirme veritabanı kodunu tetikleyebilir, böylece bir güncellemeden sonra nesnenin DB ile gerçekten senkronize olduğundan emin olmalısınız.

  6. Yapıcıda DB'den bir çalışan getirmenin iyi bir fikir olmadığını düşünüyorum, çünkü getirme yanlış gidebilir ve birçok dilde başarısız inşaatla başa çıkmak zor. Yapıcı, nesneyi (özellikle ID) ve durumunu başlatmalıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.