Bagaimana seharusnya kelas `Karyawan` dirancang?

11

Saya mencoba membuat program untuk mengelola karyawan. Saya tidak bisa, bagaimanapun, mencari tahu bagaimana merancang Employeekelas. Tujuan saya adalah untuk dapat membuat dan memanipulasi data karyawan pada database menggunakan suatu Employeeobjek.

Implementasi dasar yang saya pikirkan, adalah yang sederhana ini:

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

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

Menggunakan implementasi ini, saya mengalami beberapa masalah.

  1. The IDseorang karyawan diberikan oleh database, jadi jika saya menggunakan objek untuk menggambarkan seorang karyawan baru, tidak akan ada IDuntuk menyimpan lagi, sementara objek yang mewakili karyawan yang ada akan memiliki ID. Jadi saya punya properti yang kadang-kadang menggambarkan objek dan kadang tidak (Yang bisa menunjukkan bahwa kita melanggar SRP ? Karena kita menggunakan kelas yang sama untuk mewakili karyawan baru dan yang sudah ada ...).
  2. The CreateMetode seharusnya membuat seorang karyawan pada database, sementara Updatedan Deleteseharusnya bertindak atas seorang karyawan yang ada (Sekali lagi, SRP ...).
  3. Parameter apa yang harus dimiliki oleh metode 'Buat'? Puluhan parameter untuk semua data karyawan atau mungkin suatu Employeeobjek?
  4. Haruskah kelas tidak berubah?
  5. Bagaimana cara Updatekerjanya? Apakah akan mengambil properti dan memperbarui database? Atau mungkin itu akan mengambil dua objek - yang "lama" dan yang "baru", dan memperbarui database dengan perbedaan di antara mereka? (Saya pikir jawabannya ada hubungannya dengan jawaban tentang mutabilitas kelas).
  6. Apa yang akan menjadi tanggung jawab konstruktor? Apa yang akan menjadi parameter yang dibutuhkan? Apakah itu mengambil data karyawan dari database menggunakan idparameter dan mereka mengisi properti?

Jadi, seperti yang Anda lihat, saya memiliki sedikit kekacauan di kepala saya, dan saya sangat bingung. Bisakah Anda membantu saya memahami bagaimana seharusnya kelas seperti itu?

Harap perhatikan bahwa saya tidak ingin pendapat, hanya untuk memahami bagaimana kelas yang sering digunakan umumnya dirancang.

Sipo
sumber
3
Pelanggaran saat ini Anda terhadap SRP adalah bahwa Anda memiliki kelas yang mewakili entitas dan bertanggung jawab atas logika CRUD. Jika Anda memisahkannya, bahwa operasi CRUD dan struktur entitas akan menjadi kelas yang berbeda, maka 1. dan 2. tidak merusak SRP. 3. harus mengambil Employeeobjek untuk memberikan abstraksi, pertanyaan 4. dan 5. umumnya tidak dapat dijawab, tergantung pada kebutuhan Anda, dan jika Anda memisahkan struktur dan operasi CRUD menjadi dua kelas, maka itu cukup jelas, konstruktor Employeetidak dapat mengambil data dari db lagi, jadi itu jawaban 6.
Andy
@ Davidvider - Terima kasih. Bisakah Anda menjawabnya?
Sipo
5
Jangan, saya ulangi, tidak memiliki ctor Anda menjangkau ke database. Melakukannya dengan erat memasangkan kode ke database dan membuat hal-hal yang sangat sulit untuk diuji (bahkan pengujian manual semakin sulit). Lihatlah ke dalam pola repositori. Pikirkan sejenak, apakah Anda Updateseorang karyawan, atau apakah Anda memperbarui catatan karyawan? Apakah Anda Employee.Delete(), atau tidak Boss.Fire(employee)?
RubberDuck
1
Selain dari apa yang telah disebutkan, apakah masuk akal bagi Anda bahwa Anda membutuhkan seorang karyawan untuk menciptakan seorang karyawan? Dalam catatan aktif, mungkin lebih masuk akal untuk mendapatkan Karyawan baru dan kemudian memanggil Simpan pada objek itu. Meskipun begitu, Anda sekarang memiliki kelas yang bertanggung jawab untuk logika bisnis dan juga kegigihan datanya sendiri.
Mr Cochese

Jawaban:

10

Ini adalah transkripsi yang lebih baik dari komentar awal saya di bawah pertanyaan Anda. Jawaban atas pertanyaan yang diajukan oleh OP dapat ditemukan di bagian bawah jawaban ini. Periksa juga catatan penting yang terletak di tempat yang sama.


Apa yang saat ini Anda gambarkan, Sipo, adalah pola desain yang disebut Rekaman aktif . Seperti halnya segala sesuatu, bahkan yang ini telah menemukan tempatnya di antara programmer, tetapi telah dibuang demi repositori dan pola data mapper karena satu alasan sederhana, skalabilitas.

Singkatnya, catatan aktif adalah objek, yang:

  • mewakili objek dalam domain Anda (termasuk aturan bisnis, tahu cara menangani operasi tertentu pada objek, seperti jika Anda dapat atau tidak dapat mengubah nama pengguna dan sebagainya),
  • tahu cara mengambil, memperbarui, menyimpan, dan menghapus entitas.

Anda mengatasi beberapa masalah dengan desain Anda saat ini dan masalah utama desain Anda dibahas pada poin terakhir, ke-6 (terakhir tapi tidak kalah penting, saya kira). Ketika Anda memiliki kelas yang Anda merancang konstruktor dan Anda bahkan tidak tahu apa yang harus dilakukan konstruktor, kelas mungkin melakukan sesuatu yang salah. Itu terjadi dalam kasus Anda.

Tetapi memperbaiki desain sebenarnya cukup sederhana dengan memecah representasi entitas dan logika CRUD menjadi dua (atau lebih) kelas.

Seperti inilah desain Anda sekarang:

  • Employee- berisi informasi tentang struktur karyawan (atributnya) dan metode bagaimana memodifikasi entitas (jika Anda memutuskan untuk pergi dengan cara yang bisa berubah), berisi logika CRUD untuk Employeeentitas, dapat mengembalikan daftar Employeeobjek, menerima Employeeobjek saat Anda ingin memperbarui karyawan, dapat mengembalikan satu Employeemelalui metode sepertigetSingleById(id : string) : Employee

Wow, kelasnya tampak besar.

Ini akan menjadi solusi yang diusulkan:

  • Employee - berisi informasi tentang struktur karyawan (atributnya) dan metode bagaimana memodifikasi entitas (jika Anda memutuskan untuk pergi dengan cara yang bisa berubah)
  • EmployeeRepository- berisi logika CRUD untuk Employeeentitas, dapat mengembalikan daftar Employeeobjek, menerima Employeeobjek saat Anda ingin memperbarui karyawan, dapat mengembalikan satu Employeemelalui metode sepertigetSingleById(id : string) : Employee

Pernahkah Anda mendengar tentang pemisahan kekhawatiran ? Tidak, sekarang akan. Ini adalah versi Prinsip Tanggung Jawab Tunggal yang tidak terlalu ketat, yang mengatakan bahwa kelas seharusnya hanya memiliki satu tanggung jawab, atau seperti yang dikatakan Paman Bob:

Modul harus memiliki satu dan hanya satu alasan untuk berubah.

Cukup jelas bahwa jika saya dapat dengan jelas membagi kelas awal Anda menjadi dua yang masih memiliki antarmuka yang baik, kelas awal mungkin melakukan terlalu banyak, dan memang begitu.

Apa yang hebat tentang pola repositori, itu tidak hanya bertindak sebagai abstraksi untuk menyediakan lapisan tengah antara database (yang bisa berupa apa saja, file, noSQL, SQL, berorientasi objek), tetapi bahkan tidak perlu menjadi beton kelas. Dalam banyak bahasa OO, Anda dapat mendefinisikan antarmuka sebagai aktual interface(atau kelas dengan metode virtual murni jika Anda menggunakan C ++) dan kemudian memiliki beberapa implementasi.

Ini sepenuhnya mengangkat keputusan apakah repositori adalah implementasi aktual Anda hanya mengandalkan antarmuka dengan benar-benar mengandalkan struktur dengan interfacekata kunci. Dan repositori persis seperti itu, itu adalah istilah mewah untuk abstraksi lapisan data, yaitu memetakan data ke domain Anda dan sebaliknya.

Hal hebat lainnya tentang memisahkannya ke dalam (setidaknya) dua kelas adalah bahwa sekarang Employeekelas dapat dengan jelas mengelola datanya sendiri dan melakukannya dengan sangat baik, karena ia tidak perlu mengurus hal-hal sulit lainnya.

Pertanyaan 6: Jadi apa yang harus dilakukan oleh konstruktor di kelas yang baru dibuat Employee? Sederhana saja. Seharusnya mengambil argumen, memeriksa apakah mereka valid (seperti usia seharusnya tidak boleh negatif atau nama tidak boleh kosong), menimbulkan kesalahan ketika data tidak valid dan jika validasi yang dilewatkan menetapkan argumen ke variabel pribadi entitas. Sekarang tidak dapat berkomunikasi dengan database, karena ia tidak tahu bagaimana melakukannya.


Pertanyaan 4: Tidak dapat dijawab sama sekali, tidak secara umum, karena jawabannya sangat tergantung pada apa yang sebenarnya Anda butuhkan.


Pertanyaan 5: Sekarang bahwa Anda telah memisahkan kelas membengkak menjadi dua, Anda dapat memiliki beberapa metode pembaruan langsung pada Employeekelas, seperti changeUsername, markAsDeceased, yang akan memanipulasi data dari Employeekelas hanya dalam RAM dan kemudian Anda bisa memperkenalkan metode seperti registerDirtydari Pola Unit Kerja ke kelas repositori, di mana Anda akan membiarkan repositori tahu bahwa objek ini telah mengubah properti dan perlu diperbarui setelah Anda memanggil commitmetode.

Jelas, untuk pembaruan suatu objek harus memiliki id dan karenanya sudah disimpan, dan itu adalah tanggung jawab repositori untuk mendeteksi ini dan meningkatkan kesalahan ketika kriteria tidak terpenuhi.


Pertanyaan 3: Jika Anda memutuskan untuk mengikuti pola Unit Kerja, createmetode sekarang akan menjadi registerNew. Jika tidak, saya mungkin akan menyebutnya sebagai savegantinya. Tujuan dari repositori adalah untuk memberikan abstraksi antara domain dan lapisan data, karena ini saya akan merekomendasikan Anda bahwa metode ini (baik itu registerNewatau save) menerima Employeeobyek dan terserah kepada kelas menerapkan antarmuka repositori, yang atribut mereka memutuskan untuk mengeluarkan entitas tersebut. Melewati seluruh objek lebih baik sehingga Anda tidak perlu memiliki banyak parameter opsional.


Pertanyaan 2: Kedua metode sekarang akan menjadi bagian dari antarmuka repositori dan mereka tidak melanggar prinsip tanggung jawab tunggal. Tanggung jawab repositori adalah untuk menyediakan operasi CRUD untuk Employeeobjek, itulah yang dilakukannya (selain Baca dan Hapus, CRUD diterjemahkan menjadi Buat dan Perbarui). Jelas, Anda dapat membagi repositori lebih jauh dengan memiliki EmployeeUpdateRepositorydan sebagainya, tetapi itu jarang diperlukan dan implementasi tunggal biasanya dapat berisi semua operasi CRUD.


Pertanyaan 1: Anda berakhir dengan Employeekelas sederhana yang sekarang (di antara atribut lainnya) memiliki id. Apakah id diisi atau kosong (atau null) tergantung pada apakah objek telah disimpan. Meskipun demikian, id masih merupakan atribut yang dimiliki entitas dan tanggung jawab Employeeentitas adalah untuk menjaga atributnya, maka dari itu menjaga idnya.

Entah suatu entitas memiliki atau tidak memiliki id, biasanya tidak penting sebelum Anda mencoba melakukan beberapa logika kegigihan padanya. Seperti disebutkan dalam jawaban untuk pertanyaan 5, itu adalah tanggung jawab repositori untuk mendeteksi Anda tidak mencoba menyelamatkan entitas yang sudah disimpan atau mencoba memperbarui entitas tanpa id.


Catatan penting

Perlu diketahui bahwa meskipun pemisahan kekhawatiran itu hebat, sebenarnya merancang lapisan repositori fungsional merupakan pekerjaan yang sangat membosankan dan menurut pengalaman saya sedikit lebih sulit untuk dilakukan dengan benar daripada pendekatan rekaman aktif. Tetapi Anda akan berakhir dengan desain yang jauh lebih fleksibel dan terukur, yang mungkin merupakan hal yang baik.

Andy
sumber
Hmm sama dengan jawaban saya, tapi tidak seperti 'edgey' memakai nuansa
Ewan
2
@ Ewan saya tidak mengecilkan jawaban Anda, tetapi saya bisa melihat mengapa beberapa mungkin. Itu tidak secara langsung menjawab beberapa pertanyaan OP dan beberapa saran Anda tampaknya tidak berdasar.
Andy
1
Jawaban yang bagus dan lengkap. Memukul paku di kepala dengan pemisahan perhatian. Dan saya suka peringatan yang menunjukkan pilihan penting antara desain kompleks yang sempurna dan kompromi yang bagus.
Christophe
Benar, jawaban Anda lebih unggul
Ewan
saat pertama kali membuat objek karyawan baru, tidak akan ada nilai untuk ID. bidang id dapat pergi dengan nilai nol tetapi itu akan menyebabkan objek karyawan dalam keadaan tidak valid ????
Susantha7
2

Pertama buat struktur karyawan yang berisi properti karyawan konseptual.

Kemudian buat database dengan struktur tabel yang cocok, katakan misalnya mssql

Kemudian buat repositori karyawan Untuk basis data itu, EmployeeRepoMsSql dengan berbagai operasi CRUD yang Anda butuhkan.

Kemudian buat antarmuka IEmployeeRepo yang mengekspos operasi CRUD

Kemudian rentangkan struct Karyawan Anda ke kelas dengan parameter konstruksi IEmployeeRepo. Tambahkan berbagai metode Simpan / Hapus dll yang Anda perlukan dan gunakan EmployeeRepo yang disuntikkan untuk menerapkannya.

Ketika cone ke Id saya sarankan Anda menggunakan GUID yang dapat dihasilkan melalui kode di konstruktor.

Untuk bekerja dengan objek yang ada, kode Anda dapat mengambilnya dari database melalui repositori sebelum memanggil Metode Pembaruan.

Atau Anda dapat memilih model objek Anemic Domain yang disukai (tetapi dalam pandangan saya superior) di mana Anda tidak menambahkan metode CRUD ke objek Anda, dan hanya meneruskan objek ke repo untuk diperbarui / disimpan / dihapus

Kekekalan adalah pilihan desain yang akan tergantung pada pola dan gaya penulisan Anda. Jika Anda semua fungsional maka cobalah untuk menjadi abadi juga. Tetapi jika Anda tidak yakin objek yang bisa berubah mungkin lebih mudah diimplementasikan.

Alih-alih Buat () saya akan pergi dengan Simpan (). Buat karya dengan konsep immutability, tetapi saya selalu merasa berguna untuk dapat membangun objek yang belum 'Disimpan' misalnya Anda memiliki beberapa UI yang memungkinkan Anda untuk mengisi objek atau objek karyawan dan kemudian memverifikasi lagi beberapa aturan sebelum menyimpan ke database.

***** contoh kode

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

    }
}
Ewan
sumber
1
Model Domain Anemik sangat sedikit hubungannya dengan logika CRUD. Ini adalah model yang, meskipun milik lapisan domain, tidak memiliki fungsionalitas dan semua fungsionalitas dilayani melalui layanan, yang model domain ini diteruskan sebagai parameter.
Andy
Tepatnya, dalam hal ini repo adalah layanan dan fungsinya adalah operasi CRUD.
Ewan
@ Davidvider apakah Anda mengatakan Model Domain Anemik adalah hal yang baik?
candied_orange
1
@CandiedOrange Saya belum menyatakan pendapat saya dalam komentar, tetapi tidak, jika Anda memutuskan untuk menyelam aplikasi Anda ke lapisan di mana satu lapisan hanya bertanggung jawab untuk logika bisnis, saya dengan Tn. Fowler bahwa model domain anemik sebenarnya merupakan anti-pola. Mengapa saya perlu UserUpdatelayanan dengan changeUsername(User user, string newUsername)metode, padahal saya bisa juga menambahkan changeUsernamemetode ke kelas Usersecara langsung. Menciptakan layanan untuk itu tidak masuk akal.
Andy
1
Saya pikir dalam hal ini menyuntikkan repo hanya untuk menempatkan logika CRUD pada Model tidak optimal.
Ewan
1

Tinjau desain Anda

EmployeePada kenyataannya, Anda adalah semacam proksi untuk objek yang dikelola terus-menerus dalam database.

Karena itu saya menyarankan untuk berpikir ke ID seolah-olah itu referensi ke objek database Anda. Dengan mengingat logika ini, Anda dapat melanjutkan desain seperti yang akan Anda lakukan untuk objek non basis data, ID yang memungkinkan Anda menerapkan logika komposisi tradisional:

  • Jika ID diatur, Anda memiliki objek database yang sesuai.
  • Jika ID tidak disetel, tidak ada objek basis data yang sesuai: Employeemungkin belum dibuat, atau hanya bisa dihapus.
  • Anda memerlukan beberapa mekanisme untuk memulai hubungan untuk karyawan yang ada dan catatan database yang ada yang belum dimuat dalam memori.

Anda juga perlu mengelola status objek. Sebagai contoh:

  • ketika seorang Karyawan belum ditautkan dengan objek DB baik melalui buat atau pengambilan data, Anda seharusnya tidak dapat melakukan pembaruan atau menghapus
  • Apakah data Karyawan di objek disinkronkan dengan database atau apakah ada perubahan yang dilakukan?

Dengan mengingat hal ini, kita dapat memilih:

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
}

Agar dapat mengelola status objek Anda dengan cara yang dapat diandalkan, Anda harus memastikan enkapsulasi yang lebih baik dengan menjadikan properti pribadi dan memberikan akses hanya melalui getter dan setter yang mengatur status pembaruan.

Pertanyaan Anda

  1. Saya pikir properti ID tidak melanggar SRP. Tanggung jawab tunggal adalah merujuk ke objek database.

  2. Karyawan Anda secara keseluruhan tidak patuh dengan SRP, karena bertanggung jawab atas tautan dengan database, tetapi juga untuk menahan perubahan sementara, dan untuk semua transaksi yang terjadi pada objek tersebut.

    Desain lain bisa untuk menjaga bidang yang dapat diubah di objek lain yang akan dimuat hanya ketika bidang perlu diakses.

    Anda bisa menerapkan transaksi basis data pada Karyawan menggunakan pola perintah . Desain semacam ini juga akan memudahkan pemisahan antara objek bisnis Anda (Karyawan) dan sistem basis data Anda, dengan mengisolasi idiom dan API khusus basis data.

  3. Saya tidak akan menambahkan selusin parameter Create(), karena objek bisnis dapat berkembang dan membuat semua ini sangat sulit untuk dipertahankan. Dan kode akan menjadi tidak terbaca. Anda memiliki 2 pilihan di sini: melewati serangkaian parameter minimalis (tidak lebih dari 4) yang benar-benar diperlukan untuk membuat karyawan di basis data dan melakukan perubahan yang tersisa melalui pembaruan, ATAU Anda melewatkan objek. By the way, dalam desain Anda Saya memahami bahwa Anda sudah dipilih: my_employee.Create().

  4. Haruskah kelas tidak berubah? Lihat diskusi di atas: dalam desain asli Anda no. Saya akan memilih ID yang tidak dapat diubah tetapi bukan Karyawan yang tidak dapat diubah. Seorang Karyawan berkembang dalam kehidupan nyata (posisi pekerjaan baru, alamat baru, situasi perkawinan baru, bahkan nama baru ...). Saya pikir akan lebih mudah dan lebih alami untuk bekerja dengan kenyataan ini dalam pikiran, setidaknya di lapisan logika bisnis.

  5. Jika Anda mempertimbangkan untuk menggunakan perintah untuk pembaruan dan objek berbeda untuk (GUI?) Untuk menahan perubahan yang diinginkan, Anda dapat memilih pendekatan lama / baru. Dalam semua kasus lain, saya akan memilih untuk memperbarui objek yang bisa berubah. Perhatian: pembaruan dapat memicu kode database sehingga Anda harus memastikan setelah pembaruan objek masih benar-benar selaras dengan DB.

  6. Saya pikir bahwa mengambil karyawan dari DB di konstruktor bukanlah ide yang baik, karena mengambil bisa salah, dan dalam banyak bahasa, sulit untuk mengatasi konstruksi yang gagal. Konstruktor harus menginisialisasi objek (terutama ID) dan statusnya.

Christophe
sumber