Antarmuka terpisah untuk metode mutasi

11

Saya telah bekerja pada refactoring beberapa kode, dan saya pikir saya mungkin telah mengambil langkah pertama ke lubang kelinci. Saya menulis contoh di Jawa, tapi saya kira itu bisa menjadi agnostik.

Saya memiliki antarmuka yang Foodidefinisikan sebagai

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

Dan implementasi sebagai

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

Saya juga memiliki antarmuka MutableFooyang menyediakan mutator yang cocok

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

Ada beberapa implementasi MutableFooyang bisa ada (saya belum mengimplementasikannya). Salah satunya adalah

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

Alasan saya membaginya adalah karena keduanya sama-sama memungkinkan untuk digunakan. Artinya, kemungkinan besar seseorang yang menggunakan kelas-kelas ini akan menginginkan instance yang tidak dapat diubah, karena mereka akan menginginkan yang bisa berubah.

Kasus penggunaan utama yang saya miliki adalah sebuah antarmuka yang disebut StatSetyang mewakili detail pertempuran tertentu untuk sebuah game (hitpoint, serangan, pertahanan). Namun, statistik "efektif", atau statistik aktual, adalah hasil dari statistik dasar, yang tidak pernah dapat diubah, dan statistik terlatih, yang dapat ditingkatkan. Keduanya terkait dengan

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

Statistik terlatih meningkat setelah setiap pertempuran seperti

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

tetapi mereka tidak bertambah setelah pertempuran. Menggunakan barang-barang tertentu, menggunakan taktik tertentu, penggunaan medan perang yang cerdas semua bisa memberikan pengalaman yang berbeda.

Sekarang untuk pertanyaan saya:

  1. Apakah ada nama untuk memisahkan antarmuka oleh pengakses dan mutator menjadi antarmuka terpisah?
  2. Apakah membelah mereka dengan cara ini sebagai pendekatan yang 'benar' jika mereka sama-sama cenderung digunakan, atau apakah ada pola yang berbeda, lebih diterima yang harus saya gunakan sebagai gantinya (mis. Foo foo = FooFactory.createImmutableFoo();Yang bisa kembali DefaultFooatau DefaultMutableFootetapi disembunyikan karena createImmutableFookembali Foo)?
  3. Apakah ada kelemahan langsung untuk menggunakan pola ini, kecuali untuk memperumit hierarki antarmuka?

Alasan saya mulai mendesain dengan cara ini adalah karena saya memiliki pola pikir bahwa semua pelaksana antarmuka harus mematuhi antarmuka yang paling sederhana yang mungkin, dan tidak memberikan apa-apa lagi. Dengan menambahkan setter ke antarmuka, sekarang statistik efektif dapat dimodifikasi secara terpisah dari bagian-bagiannya.

Membuat kelas baru untuk EffectiveStatSetitu tidak masuk akal karena kami tidak memperluas fungsionalitas dengan cara apa pun. Kita dapat mengubah implementasinya, dan membuat EffectiveStatSetgabungan dua yang berbeda StatSets, tetapi saya merasa itu bukan solusi yang tepat;

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}
Zymus
sumber
1
Saya pikir masalahnya adalah objek Anda bisa berubah bahkan jika Anda mengaksesnya antarmuka saya 'tidak berubah'. Lebih baik memiliki objek yang bisa berubah dengan Player.TrainStats ()
Ewan
@gnat apakah Anda memiliki referensi ke tempat saya dapat mempelajari lebih lanjut tentang itu? Berdasarkan pertanyaan yang diedit, saya tidak yakin bagaimana atau di mana saya bisa menerapkannya.
Zymus
@gnat: tautan Anda (dan tautan tingkat dua dan tiga) sangat berguna, tetapi ungkapan bijak yang menarik tidak banyak membantu. Itu hanya menarik kesalahpahaman dan menghina.
rwong

Jawaban:

6

Bagi saya sepertinya Anda punya solusi mencari masalah.

Apakah ada nama untuk memisahkan antarmuka oleh pengakses dan mutator menjadi antarmuka terpisah?

Ini mungkin sedikit memprovokasi, tetapi sebenarnya saya akan menyebutnya "overdesigning things" atau "overcomplicating things". Dengan menawarkan varian yang dapat berubah dan tidak berubah dari kelas yang sama, Anda menawarkan dua solusi yang setara secara fungsional untuk masalah yang sama, yang hanya berbeda dalam aspek non-fungsional seperti perilaku kinerja, API, dan keamanan terhadap efek samping. Saya kira itu karena Anda takut membuat keputusan mana yang lebih disukai, atau karena Anda mencoba menerapkan fitur "const" dari C ++ di C #. Saya kira dalam 99% dari semua kasus itu tidak akan membuat perbedaan besar jika pengguna memilih varian yang dapat berubah atau tidak berubah, ia dapat menyelesaikan masalahnya dengan yang lain atau yang lain. Dengan demikian "kemungkinan kelas untuk digunakan"

Pengecualiannya adalah ketika Anda mendesain bahasa pemrograman baru atau kerangka kerja multi guna yang akan digunakan oleh sekitar sepuluh ribu programmer atau lebih. Maka itu memang dapat skala yang lebih baik ketika Anda menawarkan varian tipe data tujuan umum yang tidak berubah dan bisa berubah. Tapi ini adalah situasi di mana Anda akan memiliki ribuan skenario penggunaan yang berbeda - yang mungkin bukan masalah yang Anda hadapi, saya kira?

Apakah memecah mereka dengan cara ini sebagai pendekatan yang 'tepat' jika mereka sama-sama cenderung digunakan atau apakah ada pola yang berbeda dan lebih diterima

"Pola yang lebih diterima" disebut KISS - jadikan itu sederhana dan bodoh. Buat keputusan untuk atau tidak berubah-ubah untuk kelas / antarmuka tertentu di perpustakaan Anda. Misalnya, jika "StatSet" Anda memiliki selusin atribut atau lebih, dan sebagian besar diubah secara individual, saya lebih suka varian yang bisa berubah dan tidak mengubah statistik dasar yang tidak boleh dimodifikasi. Untuk sesuatu seperti Fookelas dengan atribut X, Y, Z (vektor tiga dimensi), saya mungkin lebih suka varian yang tidak berubah.

Apakah ada kelemahan langsung untuk menggunakan pola ini, kecuali untuk memperumit hierarki antarmuka?

Desain yang terlalu rumit membuat perangkat lunak lebih sulit untuk diuji, lebih sulit untuk dipertahankan, lebih sulit untuk berkembang.

Doc Brown
sumber
1
Saya pikir maksud Anda "KISS - Keep It Simple, Stupid"
Epicblood
2

Apakah ada nama untuk memisahkan antarmuka oleh pengakses dan mutator menjadi antarmuka terpisah?

Mungkin ada nama untuk ini jika pemisahan ini bermanfaat dan memberikan manfaat yang tidak saya lihat . Jika pemisahan tidak memberikan manfaat, dua pertanyaan lainnya tidak masuk akal.

Bisakah Anda memberi tahu kami kasus penggunaan bisnis di mana dua Antarmuka terpisah memberi kami manfaat atau apakah pertanyaan ini masalah akademis (YAGNI)?

Saya bisa memikirkan berbelanja dengan kereta yang bisa berubah-ubah (Anda bisa memasukkan lebih banyak artikel) yang dapat menjadi pesanan di mana artikel tidak dapat diubah lagi oleh pelanggan. Status pesanan masih bisa berubah.

Implementasi yang saya lihat tidak memisahkan antarmuka

  • tidak ada antarmuka ReadOnlyList di java

Versi ReadOnly menggunakan "WritableInterface" dan melemparkan pengecualian jika metode penulisan uesed

k3b
sumber
Saya telah memodifikasi OP untuk menjelaskan kasus penggunaan utama.
Zymus
2
"tidak ada antarmuka ReadOnlyList di java" - terus terang, seharusnya ada. Jika Anda memiliki sepotong kode yang menerima Daftar <T> sebagai parameter, Anda tidak dapat dengan mudah mengetahui apakah itu akan bekerja dengan daftar yang tidak dapat dimodifikasi. Kode yang mengembalikan daftar, tidak ada cara untuk mengetahui apakah Anda dapat memodifikasinya dengan aman. Satu-satunya pilihan adalah mengandalkan dokumentasi yang berpotensi tidak lengkap atau tidak akurat, atau membuat salinan defensif untuk berjaga-jaga. Jenis koleksi hanya baca akan membuat ini lebih sederhana.
Jules
@ Jules: memang, cara Java tampaknya semua di atas: untuk memastikan dokumentasi lengkap dan akurat, serta membuat salinan defensif berjaga-jaga. Itu pasti berskala ke proyek-proyek perusahaan yang sangat besar.
rwong
1

Pemisahan antarmuka koleksi yang dapat diubah dan antarmuka koleksi hanya-baca-kontrak adalah contoh prinsip pemisahan antarmuka. Saya tidak berpikir ada nama khusus yang diberikan untuk setiap penerapan prinsip.

Perhatikan beberapa kata di sini: "read-only-contract" dan "collection".

Kontrak hanya-baca berarti bahwa kelas Amemberi kelas Bakses hanya-baca, tetapi tidak menyiratkan bahwa objek koleksi yang mendasarinya sebenarnya tidak dapat diubah. Abadi berarti bahwa itu tidak akan pernah berubah di masa depan, tidak peduli oleh agen apa pun. Kontrak hanya baca hanya mengatakan bahwa penerima tidak diperbolehkan mengubahnya; orang lain (khususnya, kelas A) diizinkan untuk mengubahnya.

Untuk membuat suatu objek tidak berubah, ia harus benar-benar tidak dapat diubah - ia harus menolak upaya untuk mengubah datanya terlepas dari agen yang memintanya.

Pola ini kemungkinan besar diamati pada objek yang mewakili kumpulan data - daftar, urutan, file (aliran), dll.


Kata "immutable" menjadi iseng, tetapi konsepnya bukan hal baru. Dan ada banyak cara menggunakan kekekalan, serta banyak cara untuk mencapai desain yang lebih baik menggunakan sesuatu yang lain (yaitu para pesaingnya).


Inilah pendekatan saya (tidak didasarkan pada keabadian).

  1. Tentukan DTO (objek transfer data), juga dikenal sebagai nilai tuple.
    • DTO ini akan berisi tiga bidang: hitpoints, attack, defense.
    • Hanya bidang: publik, siapa pun dapat menulis, tanpa perlindungan.
    • Namun, DTO harus menjadi objek sekali pakai: jika kelas Aperlu meneruskan DTO ke kelas B, ia membuat salinannya dan meneruskan salinannya. Jadi, Bdapat menggunakan DTO namun suka (menulis untuk itu), tanpa mempengaruhi DTO yang Aberpegang pada.
  2. The grantExperiencefungsi yang akan dipecah menjadi dua:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats akan mengambil input dari dua DTO, satu mewakili statistik pemain dan satu lagi mewakili statistik musuh, dan melakukan perhitungan.
    • Untuk input, penelepon harus memilih di antara pangkalan, statistik terlatih atau efektif, sesuai dengan kebutuhan Anda.
    • Hasilnya akan menjadi DTO baru di mana masing-masing bidang ( hitpoints, attack, defense) toko jumlah yang akan bertambah karena kemampuannya itu.
    • DTO "jumlah yang ditambahkan" yang baru tidak menyangkut batas atas (batas maksimum yang dikenakan) untuk nilai-nilai itu.
  4. increaseStats adalah metode pada pemain (bukan pada DTO) yang mengambil "jumlah untuk meningkatkan" DTO, dan menerapkan kenaikan pada DTO yang dimiliki oleh pemain dan mewakili DTO yang dapat dilatih pemain.
    • Jika ada nilai maksimum yang berlaku untuk statistik ini, mereka diberlakukan di sini.

Dalam kasus calculateNewStatsditemukan tidak bergantung pada informasi pemain atau musuh lain (di luar nilai-nilai dalam DTO dua input), metode ini dapat berpotensi berada di mana saja dalam proyek.

Jika calculateNewStatsditemukan memiliki ketergantungan penuh pada pemain dan objek musuh (yaitu, pemain masa depan dan objek musuh mungkin memiliki properti baru, dan calculateNewStatsharus diperbarui untuk mengkonsumsi sebanyak mungkin properti baru mereka), maka calculateNewStatsharus menerima kedua objek, bukan hanya DTO. Namun, hasil perhitungannya akan tetap menjadi DTO kenaikan, atau objek transfer data sederhana apa pun yang membawa informasi yang digunakan untuk melakukan peningkatan / peningkatan.

rwong
sumber
1

Ada masalah besar di sini: hanya karena struktur data tidak berubah, tidak berarti kita tidak perlu versi modifikasi itu . The nyata versi berubah dari struktur data tidak menyediakan setX, setY, dan setZmetode - mereka hanya mengembalikan baru struktur bukan memodifikasi objek yang memanggil mereka pada.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

Jadi, bagaimana Anda memberikan satu bagian dari sistem Anda kemampuan untuk mengubahnya sambil membatasi bagian lain? Dengan objek yang bisa berubah yang berisi referensi ke instance dari struktur yang tidak dapat diubah. Pada dasarnya, alih-alih kelas pemain Anda dapat mengubah objek statistiknya sembari memberikan setiap kelas lainnya pandangan tentang statistiknya, statistiknya tidak dapat diubah dan pemain dapat berubah. Dari pada:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Anda akan memiliki:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

Lihat perbedaannya? Objek stats benar-benar tidak berubah, tetapi statistik pemain saat ini adalah referensi yang bisa berubah ke objek yang tidak bisa diubah. Tidak perlu membuat dua antarmuka sama sekali - cukup buat struktur data benar-benar tidak dapat diubah dan kelola referensi yang dapat berubah ke tempat Anda menggunakannya untuk menyimpan status.

Ini berarti objek lain di sistem Anda tidak dapat bergantung pada referensi ke objek statistik - objek statistik yang mereka miliki tidak akan menjadi objek statistik yang sama dengan yang dimiliki pemain setelah pemain memperbarui statistik mereka.

Ini lebih masuk akal, karena secara konseptual bukan statistik yang berubah, ini statistik pemain saat ini . Bukan objek statistik. The referensi untuk statistik objek objek pemain memiliki. Jadi bagian lain dari sistem Anda tergantung pada referensi yang semuanya harus secara eksplisit merujuk player.currentStats()atau semacam itu alih-alih mendapatkan benda statistik yang mendasari pemain, menyimpannya di suatu tempat, dan bergantung padanya memperbarui melalui mutasi.

Mendongkrak
sumber
1

Wow ... ini benar-benar membawaku kembali. Saya mencoba ide yang sama ini beberapa kali. Saya terlalu keras kepala untuk menyerah karena saya pikir akan ada sesuatu yang baik untuk dipelajari. Saya telah melihat orang lain mencoba ini dalam kode juga. Sebagian besar implementasi yang saya lihat disebut antarmuka Read-Only seperti milik Anda FooVieweratau ReadOnlyFoodan antarmuka Write-Only FooEditoratau WriteableFooatau di Jawa saya pikir saya FooMutatorpernah melihatnya . Saya tidak yakin ada kosa kata resmi atau bahkan umum untuk melakukan hal-hal seperti ini.

Ini tidak pernah melakukan sesuatu yang berguna bagi saya di tempat saya mencobanya. Saya akan menghindarinya sama sekali. Saya akan melakukan apa yang orang lain sarankan dan mengambil langkah mundur dan mempertimbangkan apakah Anda benar-benar membutuhkan gagasan ini dalam ide Anda yang lebih besar. Saya tidak yakin ada cara yang tepat untuk melakukan ini karena saya tidak pernah benar-benar menyimpan kode yang saya buat saat mencoba ini. Setiap kali saya mundur setelah banyak upaya dan hal-hal yang disederhanakan. Dan maksud saya adalah sesuatu yang sejalan dengan apa yang orang lain katakan tentang YAGNI dan KISS dan DRY.

Satu kemungkinan downside: pengulangan. Anda harus membuat antarmuka ini untuk banyak kelas yang berpotensi dan bahkan hanya untuk satu kelas Anda harus memberi nama setiap metode dan menggambarkan setiap tanda tangan setidaknya dua kali. Dari semua hal yang membakar saya dalam pengkodean, harus mengubah banyak file dengan cara ini membuat saya dalam kesulitan terbesar. Saya akhirnya lupa membuat perubahan di satu tempat atau di tempat lain. Setelah Anda memiliki kelas yang disebut Foo dan antarmuka yang disebut FooViewer dan FooEditor, jika Anda memutuskan akan lebih baik jika Anda menyebutnya Bar alih-alih Anda harus refactor-> ganti nama tiga kali kecuali Anda memiliki IDE yang benar-benar pintar. Saya bahkan menemukan ini menjadi kasus ketika saya memiliki sejumlah besar kode yang berasal dari generator kode.

Secara pribadi, saya tidak suka membuat antarmuka untuk hal-hal yang hanya memiliki satu implementasi juga. Itu berarti ketika saya melakukan perjalanan dalam kode saya tidak bisa hanya melompat ke satu - satunya implementasi secara langsung, saya harus melompat ke antarmuka dan kemudian ke implementasi antarmuka atau setidaknya tekan beberapa tombol tambahan untuk sampai ke sana. Semua itu bertambah.

Dan kemudian ada komplikasi yang Anda sebutkan. Saya ragu saya akan pernah menggunakan cara ini dengan kode saya lagi. Bahkan untuk tempat ini paling sesuai dengan keseluruhan rencana saya untuk kode (ORM yang saya buat) saya menarik ini dan menggantinya dengan sesuatu yang lebih mudah untuk dikodekan.

Saya benar-benar akan lebih memikirkan komposisi yang Anda sebutkan. Saya ingin tahu mengapa Anda merasa itu adalah ide yang buruk. Saya akan mengharapkan untuk EffectiveStatsmembuat dan berubah Statsdan sesuatu seperti StatModifiersyang selanjutnya akan menyusun beberapa set StatModifieryang mewakili apa pun yang dapat mengubah statistik (efek sementara, item, lokasi di beberapa bidang peningkatan, kelelahan) tetapi Anda EffectiveStatstidak perlu memahami karena StatModifiersakan kelola apa saja hal-hal itu dan seberapa banyak efek dan apa yang akan mereka miliki pada statistik mana. ItuStatModifierakan menjadi antarmuka untuk hal-hal yang berbeda dan dapat mengetahui hal-hal seperti "apakah saya di zona", "kapan obatnya habis", dll ... tetapi tidak harus membiarkan benda lain mengetahui hal-hal seperti itu. Itu hanya perlu mengatakan stat mana dan bagaimana mengubahnya sekarang. Lebih baik lagi StatModifierdapat dengan mudah memaparkan metode untuk menghasilkan sesuatu yang tidak berubah Statsberdasarkan pada yang lain Statsyang akan berbeda karena telah diubah dengan tepat. Anda kemudian bisa melakukan sesuatu seperti currentStats = statModifier2.modify(statModifier1.modify(baseStats))dan semua Statsbisa berubah. Saya bahkan tidak akan kode itu secara langsung, saya mungkin akan mengulangi semua pengubah dan menerapkan masing-masing ke hasil pengubah sebelumnya dimulai dengan baseStats.

Jason Kell
sumber