Apakah ada alasan untuk menggunakan kelas "data lama biasa"?

43

Dalam kode lama saya kadang-kadang melihat kelas yang tidak lain adalah pembungkus untuk data. sesuatu seperti:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

Pemahaman saya tentang OO adalah bahwa kelas adalah struktur untuk data dan metode operasi pada data itu. Ini sepertinya menghalangi objek dari tipe ini. Bagi saya mereka tidak lebih dari structsdan semacam mengalahkan tujuan OO. Saya tidak berpikir itu pasti jahat, meskipun itu mungkin bau kode.

Apakah ada kasus di mana benda seperti itu diperlukan? Jika ini sering digunakan, apakah itu membuat desain curiga?

Michael K.
sumber
1
Ini tidak cukup menjawab pertanyaan Anda tetapi tampaknya relevan: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear
2
Saya pikir istilah yang Anda cari adalah POD (Data Lama Biasa).
Gaurav
9
Ini adalah contoh khas pemrograman terstruktur. Belum tentu buruk, pokoknya tidak berorientasi objek.
Konrad Rudolph
1
bukankah ini seharusnya stack overflow?
Muad'Dib
7
@ Muad'Dib: secara teknis, ini tentang programmer. Kompiler Anda tidak peduli jika Anda menggunakan struktur data lama yang sederhana. CPU Anda mungkin menikmatinya (dalam cara "Saya suka bau data segar dari cache"). Ini orang yang terpaku pada "hal ini membuat metodologi saya kurang murni?" pertanyaan.
Shog9

Jawaban:

67

Jelas bukan kejahatan dan bukan bau kode di pikiranku. Wadah data adalah warga negara OO yang valid. Terkadang Anda ingin merangkum informasi terkait bersama. Jauh lebih baik memiliki metode seperti

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

dari

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

Menggunakan kelas juga memungkinkan Anda untuk menambahkan parameter tambahan ke Botol tanpa mengubah setiap pemanggil dari DoStuffWithBottle. Dan Anda dapat mensubklasifikasikan Botol dan selanjutnya meningkatkan keterbacaan dan pengaturan kode Anda, jika perlu.

Ada juga objek data biasa yang dapat dikembalikan sebagai hasil dari kueri basis data, misalnya. Saya percaya istilah untuk mereka dalam hal ini adalah "Objek Transfer Data".

Dalam beberapa bahasa ada pertimbangan lain juga. Misalnya, dalam C # kelas dan struct berperilaku berbeda, karena struct adalah tipe nilai dan kelas adalah tipe referensi.

Adam Lear
sumber
25
Tidak DoStuffWithharus menjadi metode dari yang Bottlekelas dalam OOP (dan harus paling mungkin berubah, juga). Apa yang Anda tulis di atas bukanlah pola yang baik dalam bahasa OO (kecuali Anda berinteraksi dengan API lama). Ini adalah , bagaimanapun, desain berlaku di lingkungan non-OO.
Konrad Rudolph
11
@ Javier: Maka Java juga bukan jawaban terbaik. Penekanan Java pada OO luar biasa, bahasanya pada dasarnya tidak bagus dalam hal lain.
Konrad Rudolph
11
@ JohnL: Tentu saja saya menyederhanakan. Namun secara umum, objek dalam OO mengenkapsulasi status , bukan data. Ini adalah perbedaan yang baik tetapi penting. Maksud dari OOP adalah tidak memiliki banyak data. Itu mengirim pesan antar negara yang membuat negara baru. Saya gagal melihat bagaimana Anda dapat mengirim pesan ke atau dari objek tanpa metode. (Dan ini adalah definisi asli dari OOP jadi saya tidak akan menganggapnya cacat.)
Konrad Rudolph
13
@Konrad Rudolph: Inilah sebabnya saya secara eksplisit membuat komentar di dalam metode ini. Saya setuju bahwa metode yang memengaruhi instance Bottle harus masuk dalam kelas itu. Tetapi jika objek lain perlu memodifikasi statusnya berdasarkan informasi di Bottle, maka saya pikir desain saya akan cukup valid.
Adam Lear
10
@Konrad, saya tidak setuju bahwa doStuffWithBottle harus masuk dalam kelas botol. Mengapa sebotol botol tahu cara melakukan sesuatu dengan sendirinya? doStuffWithBottle menunjukkan bahwa sesuatu yang lain akan melakukan sesuatu dengan botol. Jika botol itu ada di dalamnya, itu akan menjadi kopling ketat. Namun, jika kelas Botol memiliki metode isFull (), itu akan sangat tepat.
Nemi
25

Kelas data valid dalam beberapa kasus. DTO adalah salah satu contoh bagus yang disebutkan oleh Anna Lear. Namun secara umum, Anda harus menganggap mereka sebagai benih kelas yang metodenya belum tumbuh. Dan jika Anda menemukan banyak dari mereka dalam kode lama, perlakukan mereka sebagai bau kode yang kuat. Mereka sering digunakan oleh programmer C / C ++ lama yang tidak pernah membuat transisi ke pemrograman OO dan merupakan tanda pemrograman prosedural. Mengandalkan getter dan setter setiap saat (atau lebih buruk lagi, pada akses langsung anggota non-pribadi) dapat membuat Anda mendapat masalah sebelum Anda menyadarinya. Pertimbangkan contoh metode eksternal yang membutuhkan informasi Bottle.

Berikut Bottleini adalah kelas data):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Di sini kami telah memberikan Bottlebeberapa perilaku):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Metode pertama melanggar prinsip Tell-Don't-Ask, dan dengan menjaga Bottlekebodohan kita telah membiarkan pengetahuan implisit tentang botol, seperti apa yang membuat seseorang rapuh Cap, tergelincir ke dalam logika yang berada di luar Bottlekelas. Anda harus waspada untuk mencegah 'kebocoran' semacam ini ketika Anda terbiasa mengandalkan getter.

Metode kedua Bottlehanya meminta apa yang dibutuhkan untuk melakukan tugasnya, dan pergi Bottleuntuk memutuskan apakah itu rapuh, atau lebih besar dari ukuran yang diberikan. Hasilnya adalah kopling yang jauh lebih longgar antara metode dan implementasi Botol. Efek samping yang menyenangkan adalah metode ini lebih bersih dan lebih ekspresif.

Anda jarang akan menggunakan banyak bidang objek ini tanpa menulis beberapa logika yang harus berada di kelas dengan bidang tersebut. Cari tahu apa logika itu dan kemudian pindahkan ke tempat yang seharusnya.

Mike E
sumber
1
Tidak percaya jawaban ini tidak memiliki suara (ya, Anda punya sekarang). Ini mungkin contoh sederhana, tetapi ketika OO cukup disalahgunakan Anda mendapatkan kelas layanan yang berubah menjadi mimpi buruk yang mengandung banyak fungsi yang seharusnya dienkapsulasi di kelas.
Alb
"oleh programmer C / C ++ lama yang belum pernah membuat transisi (sic) ke OO"? Pemrogram C ++ biasanya cukup OO, karena ini adalah bahasa OO, yaitu seluruh titik menggunakan C ++ alih-alih C.
nappyfalcon
7

Jika ini adalah jenis hal yang Anda butuhkan, itu tidak masalah, tapi tolong, tolong, tolong lakukan

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

bukannya sesuatu seperti

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Silahkan.

compman
sumber
14
Satu-satunya masalah dengan ini adalah tidak ada validasi. Logika apa pun mengenai Botol yang valid harus ada di kelas Botol. Namun, dengan menggunakan implementasi yang Anda usulkan, saya dapat memiliki botol dengan tinggi dan diameter negatif - tidak ada cara untuk menegakkan aturan bisnis apa pun pada objek tersebut tanpa memvalidasi setiap kali objek tersebut digunakan. Dengan menggunakan metode kedua, saya dapat memastikan bahwa jika saya memiliki objek Botol, itu adalah, adalah, dan selalu akan menjadi objek Botol yang valid sesuai dengan kontrak saya.
Thomas Owens
6
Itu salah satu area di mana .NET memiliki sedikit keunggulan atas properti, karena Anda dapat menambahkan accessor properti dengan logika validasi, dengan sintaksis yang sama seperti jika Anda mengakses bidang. Anda juga bisa mendefinisikan properti tempat kelas bisa mendapatkan nilai properti, tetapi tidak menyetelnya.
JohnL
3
@ user9521 Jika Anda yakin bahwa kode Anda tidak akan menyebabkan kesalahan fatal dengan nilai "buruk", lanjutkan ke metode Anda. Namun, jika Anda memerlukan validasi lebih lanjut, atau kemampuan untuk menggunakan pemuatan malas, atau pemeriksaan lainnya saat data dibaca atau ditulis, maka gunakan pengambil dan penyetel secara eksplisit. Secara pribadi saya cenderung menjaga variabel saya tetap pribadi dan menggunakan getter dan setter untuk konsistensi. Dengan cara ini semua variabel saya diperlakukan sama, terlepas dari validasi dan / atau teknik "lanjutan" lainnya.
Jonathan
2
Keuntungan menggunakan konstruktor adalah membuatnya lebih mudah untuk membuat kelas tidak berubah. Ini penting jika Anda ingin menulis kode multi-utas.
Fortyrunner
7
Saya akan membuat ladang final jika memungkinkan. IMHO Saya lebih suka bidang yang final dan memiliki kata kunci untuk bidang bisa berubah. misalnya var
Peter Lawrey
6

Seperti yang dikatakan @Anna, jelas tidak jahat. Tentu Anda bisa memasukkan operasi (metode) ke dalam kelas, tetapi hanya jika Anda mau . Anda tidak harus melakukannya.

Izinkan saya sedikit keluhan tentang gagasan bahwa Anda harus memasukkan operasi ke dalam kelas, bersama dengan gagasan bahwa kelas adalah abstraksi. Dalam praktiknya, ini mendorong programmer untuk

  1. Buat lebih banyak kelas daripada yang mereka butuhkan (struktur data yang berlebihan). Ketika struktur data mengandung lebih banyak komponen daripada yang diperlukan seminimal mungkin, itu tidak dinormalisasi, dan karena itu mengandung keadaan tidak konsisten. Dengan kata lain, ketika diubah, itu perlu diubah di lebih dari satu tempat agar tetap konsisten. Kegagalan untuk melakukan semua perubahan terkoordinasi membuatnya tidak konsisten, dan merupakan bug.

  2. Selesaikan masalah 1 dengan memasukkan metode pemberitahuan , sehingga jika bagian A dimodifikasi, ia mencoba untuk menyebarkan perubahan yang diperlukan ke bagian B dan C. Ini adalah alasan utama mengapa disarankan untuk memiliki metode aksesor get-and-set. Karena ini adalah praktik yang disarankan, tampaknya memaafkan masalah 1, menyebabkan lebih banyak masalah 1 dan lebih banyak solusi 2. Hasil ini tidak hanya pada bug karena mengimplementasikan notifikasi yang tidak lengkap, tetapi juga pada masalah pengisapan kinerja notifikasi runaway. Ini bukan perhitungan yang tak terbatas, hanya yang sangat panjang.

Konsep-konsep ini diajarkan sebagai hal yang baik, umumnya oleh para guru yang tidak harus bekerja dalam jutaan aplikasi monster yang penuh dengan masalah ini.

Inilah yang saya coba lakukan:

  1. Pertahankan data dinormalisasi mungkin, sehingga ketika perubahan dilakukan pada data, hal itu dilakukan pada titik kode sesedikit mungkin, untuk meminimalkan kemungkinan memasuki keadaan yang tidak konsisten.

  2. Ketika data harus dinormalisasi, dan redundansi tidak dapat dihindarkan, jangan gunakan pemberitahuan dalam upaya untuk membuatnya konsisten. Sebaliknya, toleransilah terhadap inkonsistensi sementara. Atasi ketidakkonsistenan dengan sapuan berkala melalui data dengan proses yang hanya melakukan itu. Ini memusatkan tanggung jawab untuk menjaga konsistensi, sambil menghindari masalah kinerja dan kebenaran yang rentan terhadap pemberitahuan. Ini menghasilkan kode yang jauh lebih kecil, bebas kesalahan, dan efisien.

Mike Dunlavey
sumber
3

Kelas semacam ini cukup berguna ketika Anda berurusan dengan aplikasi ukuran menengah / besar, untuk beberapa alasan:

  • cukup mudah untuk membuat beberapa test case dan memastikan bahwa datanya konsisten.
  • itu memegang semua jenis perilaku yang melibatkan informasi itu, sehingga waktu pelacakan bug data berkurang
  • Menggunakannya harus menjaga metode args tetap ringan.
  • Saat menggunakan ORM, kelas ini memberikan fleksibilitas dan konsistensi. Menambahkan atribut kompleks yang dihitung berdasarkan informasi sederhana yang sudah ada di kelas, resutl secara tertulis satu metode sederhana. Ini cukup lincah dan produktif sehingga harus memeriksa database Anda dan memastikan semua basis data ditambal dengan modifikasi baru.

Singkatnya, menurut pengalaman saya, mereka biasanya lebih bermanfaat daripada menyebalkan.

guiman
sumber
3

Dengan desain game, overhead 1000 fungsi panggilan, dan pendengar acara kadang-kadang membuatnya layak untuk memiliki kelas yang hanya menyimpan data, dan memiliki kelas lain yang mengulang semua data hanya kelas untuk melakukan logika.

SerangHobo
sumber
3

Setuju dengan Anna Lear,

Jelas bukan kejahatan dan bukan bau kode di pikiranku. Wadah data adalah warga negara OO yang valid. Terkadang Anda ingin merangkum informasi terkait bersama. Jauh lebih baik memiliki metode seperti ...

Kadang-kadang orang lupa membaca Konvensi Java Coding 1999 yang membuatnya sangat jelas bahwa pemrograman semacam ini baik-baik saja. Bahkan jika Anda menghindarinya, maka kode Anda akan tercium! (terlalu banyak getter / setter)

Dari Konvensi Kode Java 1999: Salah satu contoh variabel instance publik yang sesuai adalah kasus di mana kelas pada dasarnya adalah struktur data, tanpa perilaku. Dengan kata lain, jika Anda akan menggunakan struct bukan kelas (jika Java didukung struct), maka itu tepat untuk membuat variabel instance kelas publik. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

Ketika digunakan dengan benar, POD (struktur data lama biasa) lebih baik daripada POJO seperti POJO sering lebih baik daripada EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

Richard Faber
sumber
3

Bangunan memiliki tempat mereka, bahkan di Jawa. Anda hanya boleh menggunakannya jika dua hal berikut ini benar:

  • Anda hanya perlu mengumpulkan data yang tidak memiliki perilaku apa pun, misalnya untuk meneruskan sebagai parameter
  • Tidak masalah sedikit pun seperti apa nilai yang dimiliki data agregat

Jika ini masalahnya, maka Anda harus membuat bidang publik dan melewati getter / setter. Lagipula Getters dan setter kikuk, dan Java konyol karena tidak memiliki properti seperti bahasa yang berguna. Karena objek struct-like Anda seharusnya tidak memiliki metode apa pun, bidang publik paling masuk akal.

Namun, jika salah satu dari itu tidak berlaku, Anda sedang berhadapan dengan kelas nyata. Itu berarti semua bidang harus bersifat pribadi. (Jika Anda benar-benar membutuhkan bidang pada cakupan yang lebih mudah diakses, gunakan pengambil / penyetel.)

Untuk memeriksa apakah seharusnya struktur Anda memiliki perilaku, lihat ketika bidang digunakan. Jika tampaknya melanggar katakan, jangan tanya , maka Anda harus memindahkan perilaku itu ke dalam kelas Anda.

Jika beberapa data Anda tidak boleh berubah, maka Anda harus membuat semua bidang itu final. Anda mungkin mempertimbangkan untuk membuat kelas Anda tidak berubah . Jika Anda perlu memvalidasi data Anda, maka berikan validasi di setter dan konstruktor. (Trik yang berguna adalah mendefinisikan setter pribadi dan memodifikasi bidang Anda di dalam kelas Anda hanya menggunakan setter itu.)

Contoh Botol Anda kemungkinan besar akan gagal dalam kedua pengujian. Anda dapat memiliki (buat) kode yang terlihat seperti ini:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Sebaliknya seharusnya

double volume = bottle.calculateVolumeAsCylinder();

Jika Anda mengubah tinggi dan diameter, apakah itu botol yang sama? Mungkin tidak. Itu harus final. Apakah nilai negatif ok untuk diameter? Haruskah Botol Anda lebih tinggi dari lebar? Bisakah Cap menjadi nol? Tidak? Bagaimana Anda memvalidasi ini? Anggaplah klien itu bodoh atau jahat. ( Tidak mungkin untuk mengatakan perbedaannya. ) Anda perlu memeriksa nilai-nilai ini.

Seperti inilah bentuk kelas Botol Anda yang baru:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
Eva
sumber
0

IMHO sering tidak ada kelas yang cukup seperti ini dalam sistem berorientasi objek. Saya perlu hati-hati untuk memenuhi syarat itu.

Tentu saja jika bidang data memiliki cakupan dan jarak pandang yang luas, itu bisa sangat tidak diinginkan jika ada ratusan atau ribuan tempat di basis kode Anda yang merusak data tersebut. Itu meminta masalah dan kesulitan mempertahankan invarian. Namun pada saat yang sama, itu tidak berarti bahwa setiap kelas di seluruh basis kode mendapat manfaat dari penyembunyian informasi.

Tetapi ada banyak kasus di mana bidang data seperti itu akan memiliki cakupan yang sangat sempit. Contoh yang sangat mudah adalah Nodekelas privat dari struktur data. Sering dapat menyederhanakan kode banyak dengan mengurangi jumlah interaksi objek yang terjadi jika dikatakan Nodehanya bisa terdiri dari data mentah. Itu berfungsi sebagai mekanisme decoupling karena versi alternatif mungkin memerlukan kopling dua arah dari, katakanlah, Tree->Nodedan Node->Treebukan hanya Tree->Node Data.

Contoh yang lebih kompleks adalah sistem entitas-komponen seperti yang sering digunakan dalam mesin game. Dalam kasus tersebut, komponen seringkali hanya berupa data mentah dan kelas seperti yang Anda tunjukkan. Namun, ruang lingkup / visibilitas mereka cenderung terbatas karena biasanya hanya ada satu atau dua sistem yang dapat mengakses jenis komponen tertentu. Akibatnya, Anda masih cenderung menemukan cukup mudah untuk mempertahankan invarian dalam sistem tersebut dan, apalagi, sistem seperti itu memiliki sedikit object->objectinteraksi, sehingga sangat mudah untuk memahami apa yang terjadi pada level mata burung.

Dalam kasus seperti itu, Anda dapat berakhir dengan sesuatu yang lebih seperti ini sejauh interaksi berjalan (diagram ini menunjukkan interaksi, bukan kopling, karena diagram kopling mungkin menyertakan antarmuka abstrak untuk gambar kedua di bawah):

masukkan deskripsi gambar di sini

... berbeda dengan ini:

masukkan deskripsi gambar di sini

... dan jenis sistem yang sebelumnya cenderung lebih mudah untuk dipelihara dan beralasan dalam hal kebenaran terlepas dari kenyataan bahwa dependensi sebenarnya mengalir ke data. Anda mendapatkan lebih sedikit kopling terutama karena banyak hal dapat diubah menjadi data mentah, bukan objek yang berinteraksi satu sama lain membentuk grafik interaksi yang sangat kompleks.


sumber
-1

Bahkan ada banyak makhluk aneh yang ada di dunia OOP. Saya pernah membuat kelas, yang abstrak, dan tidak mengandung apa-apa, itu hanya untuk menjadi induk dari dua kelas lainnya ... ini adalah kelas Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Jadi SceneMember adalah kelas dasar, ia melakukan semua pekerjaan, yang diperlukan untuk muncul di tempat kejadian: menambah, menghapus, mengatur ID, dll. Pesan adalah koneksi antar port, ia memiliki kehidupannya sendiri. InputPort dan OutputPort berisi fungsi spesifik i / o sendiri.

Port kosong. Ini hanya digunakan untuk mengelompokkan InputPort dan OutputPort bersama untuk, katakanlah, sebuah portlist. Itu kosong karena SceneMember berisi semua hal untuk bertindak sebagai anggota, dan InputPort dan OutputPort berisi tugas yang ditentukan port. InputPort dan OutputPort sangat berbeda, sehingga tidak memiliki fungsi umum (kecuali SceneMember). Jadi, Port kosong. Tapi ini legal.

Mungkin ini adalah antipemutaran , tetapi saya menyukainya.

(Ini seperti kata "jodoh", yang digunakan untuk "istri" dan "suami". Anda tidak pernah menggunakan kata "jodoh" untuk orang konkret, karena Anda tahu jenis kelaminnya, dan itu tidak masalah jika dia sudah menikah atau belum, maka Anda menggunakan "seseorang" sebagai gantinya, tetapi itu masih ada, dan digunakan dalam situasi yang jarang dan abstrak, misalnya teks hukum.)

ern0
sumber
1
Mengapa port Anda perlu memperpanjang SceneMember? mengapa tidak membuat kelas port untuk beroperasi di SceneMembers?
Michael K
1
Jadi mengapa tidak menggunakan pola antarmuka penanda standar? Pada dasarnya identik dengan kelas dasar abstrak kosong Anda, tetapi itu adalah ungkapan yang jauh lebih umum.
TMN
1
@Michael: Hanya untuk alasan teoretis, saya memesan Port untuk digunakan di masa depan, tetapi masa depan ini belum tiba, dan mungkin tidak akan pernah. Saya tidak menyadari bahwa mereka tidak memiliki kesamaan sama sekali, tidak seperti namanya. Saya akan membayar kompensasi untuk semua orang yang menderita kerugian yang terjadi oleh kelas kosong itu ...
ern0
1
@TMN: Jenis SceneMember (turunan) memiliki metode getMemberType (), ada beberapa kasus ketika Scene dipindai untuk subset objek SceneMember.
ern0
4
Ini tidak menjawab pertanyaan.
Eva