Mutasyon yöntemleri için ayrı arayüz


11

Bazı kodları yeniden düzenleme üzerinde çalışıyorum ve sanırım tavşan deliğinden aşağı ilk adımı atmış olabilirim. Örneği Java ile yazıyorum, ancak sanırım agnostik olabilir.

FooOlarak tanımlanan bir arayüzüm var

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

Ve bir uygulama olarak

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

Ben de MutableFooeşleşen mutasyonlar sağlayan bir arayüz var

/**
 * 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);
}

MutableFooBunun olabileceği birkaç uygulama var (henüz uygulamadım). Onlardan biri

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

Bunları ayırmamın nedeni, her birinin kullanılmasının eşit derecede olası olmasıdır. Yani, bu sınıfları kullanan birisinin değişmez bir örnek isteyeceği gibi, muhtemelen değişebilir bir örnek isteyeceklerdir.

Sahip olduğum birincil kullanım durumu, StatSetbir oyun için belirli savaş detaylarını (isabet noktaları, saldırı, savunma) temsil eden bir arayüz . Bununla birlikte, "etkili" istatistikler veya gerçek istatistikler, asla değiştirilemeyen temel istatistiklerin ve artırılabilecek eğitimli istatistiklerin bir sonucudur. Bu ikisi

/**
 * 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);
}

eğitimli Stajyerler, her savaştan sonra

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

ama savaştan hemen sonra artmıyorlar. Belirli eşyaları kullanmak, belirli taktikleri kullanmak, savaş alanının akıllıca kullanılması farklı deneyim kazandırabilir.

Şimdi sorularım için:

  1. Arabirimleri erişimciler ve mutasyoncular tarafından ayrı arabirimlere bölmek için bir ad var mı?
  2. Onları eşit şekilde kullanılmaları muhtemelse bu şekilde 'doğru' yaklaşım mı bölmek, yoksa bunun yerine kullanmam gereken daha farklı, daha kabul edilmiş bir kalıp var mıdır (örneğin Foo foo = FooFactory.createImmutableFoo();, geri dönebilir DefaultFooveya geri döndüğü DefaultMutableFooiçin gizlidir )?createImmutableFooFoo
  3. Arayüz hiyerarşisini karmaşıklaştırmak için bu kalıbı kullanmanın hemen öngörülebilir dezavantajları var mı?

Bu şekilde tasarlamaya başlamamın nedeni, bir arabirimin tüm uygulayıcılarının mümkün olan en basit arabirime uyması ve başka bir şey sağlamaması gerektiği fikrindeyim. Arayüze ayarlayıcılar ekleyerek, etkili istatistikler artık bölümlerinden bağımsız olarak değiştirilebilir.

Bunun için yeni bir sınıf oluşturmak EffectiveStatSet, işlevselliği hiçbir şekilde genişletmediğimiz için pek mantıklı değil. Uygulamayı değiştirebilir ve EffectiveStatSetiki farklı bileşimi oluşturabiliriz StatSets, ancak bunun doğru çözüm olmadığını hissediyorum;

public class EffectiveStatSet implements StatSet {

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

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}


1
Sanırım sorun, 'değişmez' arayüzüme erişiyor olsanız bile nesnenizin değişebilir olması. Player.TrainStats () ile değiştirilebilir bir nesneye sahip olmak daha iyi
Ewan

@gnat Bununla ilgili daha fazla bilgiyi nerede bulabileceğime dair referanslarınız var mı? Düzenlenen soruya dayanarak, bunu nasıl veya nerede uygulayabileceğimden emin değilim.
Zymus

@gnat: bağlantılarınız (ve ikinci derece ve üçüncü derece bağlantıları) çok faydalıdır, ancak bilgeliğin akılda kalan cümleleri çok az yardımcı olur. Sadece yanlış anlama ve hor görme çeker.
rwong

Yanıtlar:


6

Bana göre sorun arayan bir çözümünüz var.

Arabirimleri erişimciler ve mutasyoncular tarafından ayrı arabirimlere bölmek için bir ad var mı?

Bu biraz kışkırtıcı olabilir, ama aslında "şeyleri aşırı tasarlamak" veya "aşırı karmaşık şeyler" olarak adlandırırım. Aynı sınıfın değişken ve değişmez bir varyantını sunarak, aynı sorun için yalnızca performans davranışı, API ve yan etkilere karşı güvenlik gibi işlevsel olmayan yönlerde farklılık gösteren iki işlevsel olarak eşdeğer çözüm sunarsınız. Sanırım hangisini tercih edeceğinize karar vermekten korkuyorsunuz veya C ++ 'da "const" özelliğini uygulamaya çalışıyorsunuz. Sanırım tüm vakaların% 99'unda, kullanıcı değişebilir veya değişmez varyantı seçerse büyük bir fark yaratmayacak, problemlerini biri veya diğeri ile çözebilir. Böylece "bir sınıfın kullanılma olasılığı"

Bunun istisnası, onbinlerce veya daha fazla programcı tarafından kullanılacak yeni bir programlama dili veya çok amaçlı bir çerçeve tasarlamanızdır. Daha sonra, genel amaçlı veri türlerinin değişmez ve değiştirilebilir değişkenlerini sunduğunuzda gerçekten daha iyi ölçeklenebilir. Ancak bu, binlerce farklı kullanım senaryosuna sahip olacağınız bir durumdur - sanırım karşılaştığınız sorun bu değil mi?

Aynı şekilde kullanılmaları muhtemelse veya farklı, daha kabul edilmiş bir kalıp varsa, onları bu şekilde 'doğru' yaklaşımla bölmek

"Daha kabul edilen patern" KISS olarak adlandırılır - basit ve aptalca tutun. Kitaplığınızdaki belirli bir sınıf / arabirim için değişiklik yapma veya buna karşı karar verme. Örneğin, "StatSet" inizin bir düzine veya daha fazla özelliği varsa ve çoğunlukla tek tek değiştirildiyse, değiştirilebilir değişkeni tercih ederim ve değiştirilmemeleri gereken temel istatistikleri değiştirmem. FooX, Y, Z (üç boyutlu bir vektör) özelliklerine sahip bir sınıf gibi bir şey için , muhtemelen değişmez değişkeni tercih ederim.

Arayüz hiyerarşisini karmaşıklaştırmak için bu kalıbı kullanmanın hemen öngörülebilir dezavantajları var mı?

Çok karmaşık tasarımlar yazılımı test etmeyi, bakımını zorlaştırmayı, geliştirmeyi zorlaştırır.


1
Sanırım "
ÖPÜCÜK

2

Arabirimleri erişimciler ve mutasyoncular tarafından ayrı arabirimlere bölmek için bir ad var mı?

Bu ayrılık yararlıdır ve bir sağlıyorsa bunun için bir isim var olabilir fayda gördüğüm bağışta bulunan kimse . Ayırıcılar fayda sağlamazsa, diğer iki soru anlamsızdır.

İki ayrı Arayüzün bize fayda sağladığı herhangi bir ticari kullanım durumunu anlatabilir misiniz veya bu soru akademik bir sorundur (YAGNI)?

Değişken bir alışveriş sepeti (daha fazla makale koyabilirsiniz) ile alışveriş yapmayı düşünebilirim. Sipariş durumu hala değiştirilebilir.

Gördüğüm uygulamalar arayüzleri ayırmıyor

  • Java'da ReadOnlyList arabirimi yok

ReadOnly sürümü "WritableInterface" kullanır ve bir yazma yöntemi kullanılırsa bir özel durum atar


OP'yi birincil kullanım durumunu açıklayacak şekilde değiştirdim.
Zymus

2
"Java ReadOnlyList arayüzü yok" - açıkçası, olması gerekir. Bir List <T> parametresini parametre olarak kabul eden bir kod parçanız varsa, değiştirilemez bir listeyle çalışıp çalışmayacağını kolayca söyleyemezsiniz. Kodları listeler döndürür, bunları güvenle değiştirip değiştiremeyeceğinizi bilmenin bir yolu yoktur. Tek seçenek, potansiyel olarak eksik veya yanlış belgelere güvenmek ya da her durumda savunma amaçlı bir kopya oluşturmaktır. Salt okunur bir koleksiyon türü bunu daha basit hale getirecektir.
Jules

@Jules: gerçekten, Java yolu yukarıdakilerin hepsi gibi görünüyor: belgelerin eksiksiz ve doğru olduğundan emin olmak ve her durumda savunma amaçlı bir kopya oluşturmak. Kesinlikle çok büyük kurumsal projelere ölçeklendirilir.
rwong

1

Değişken bir toplama ara yüzünün ve salt okunur bir sözleşme toplama ara yüzünün ayrılması, arayüz ayırma ilkesinin bir örneğidir. İlkenin her bir uygulamasına verilen özel isimler olduğunu düşünmüyorum.

Burada birkaç kelimeye dikkat edin: "salt okunur sözleşme" ve "tahsilat".

Salt okunur bir sözleşme, sınıfın salt okunur erişim Asağladığı anlamına gelir B, ancak temel alınan toplama nesnesinin aslında değişmez olduğu anlamına gelmez. Değişmez, herhangi bir ajan ne olursa olsun gelecekte değişmeyeceği anlamına gelir. Salt okunur bir sözleşme yalnızca alıcının değiştirmesine izin verilmediğini belirtir; başkasının (özellikle sınıfın A) değiştirmesine izin verilir.

Bir nesneyi değişmez kılmak için, gerçekten değişmez olmalıdır - talep eden aracıya bakılmaksızın verilerini değiştirme girişimlerini reddetmelidir.

Örüntü büyük olasılıkla bir veri koleksiyonunu temsil eden nesnelerde görülür - listeler, sekanslar, dosyalar (akışlar) vb.


"Değişmez" kelimesi soluklaşır, ancak konsept yeni değildir. Ve değişmezliği kullanmanın birçok yolu ve başka bir şey (yani rakipleri) kullanarak daha iyi tasarım elde etmenin birçok yolu vardır.


İşte yaklaşımım (değişmezliğe dayalı değil).

  1. Değer kümesi olarak da bilinen bir DTO (veri aktarım nesnesi) tanımlayın.
    • Bu DTO üç alan içerecektir: hitpoints, attack, defense.
    • Sadece alanlar: kamu, herkes yazabilir, koruma yok.
    • Ancak, bir DTO bir bırakma nesnesi olmalıdır: sınıfın bir DTO'yu sınıfa Ageçirmesi gerekiyorsa B, bunun bir kopyasını oluşturur ve bunun yerine kopyasını geçirir. Bu nedenle, BDTO'yu, Atuttuğu DTO'yu etkilemeden (buna yazmayı) sever .
  2. grantExperienceFonksiyon ikiye bölünebilir için:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats girişleri oyuncu istatistiklerini, diğeri düşman istatistiklerini temsil eden iki DTO'dan alır ve hesaplamaları yapar.
    • Giriş için, arayan kişi ihtiyaçlarınıza göre temel, eğitimli veya etkili istatistikler arasından seçim yapmalıdır.
    • Sonuç her alan (yeni DTO olacak hitpoints, attack, defense) saklar miktar artış olacak o yeteneği için.
    • Yeni "artırılacak miktar" DTO, bu değerler için üst sınır (uygulanan maksimum sınır) ile ilgili değildir.
  4. increaseStats oyuncuda (DTO'da değil) "DTO'da artış" gerektiren bir yöntemdir ve oyuncunun sahip olduğu ve oyuncunun eğitilebilir DTO'sunu temsil eden bu DTO'ya yapılan artışı uygular.
    • Bu istatistikler için geçerli maksimum değerler varsa, burada uygulanır.

Durumda calculateNewStatsbu yöntem, potansiyel yerde projede yer alabilir, (iki giriş DTO değerlerin ötesinde) başka bir oyuncu ya da düşman bilgilere bağlı değildir bulunmuştur.

calculateNewStatsOyuncu ve düşman nesnelerine tam bir bağımlılığı olduğu tespit edilirse (yani, gelecekteki oyuncu ve düşman nesneleri yeni özelliklere sahip olabilir calculateNewStatsve yeni özelliklerinin mümkün olduğunca fazla tüketilmesi için güncellenmesi gerekir), calculateNewStatsbu ikisini kabul etmelidir nesneler, sadece DTO değil. Ancak, hesaplama sonucu hala artımlı DTO veya bir artış / yükseltme gerçekleştirmek için kullanılan bilgileri taşıyan basit veri aktarım nesneleri olacaktır.


1

Burada büyük bir sorun var: bir veri yapısının değişmez olması, değiştirilmiş sürümlerine ihtiyacımız olmadığı anlamına gelmiyor . Gerçek bir veri yapısı değişmez sürümü sağlamaz setX, setYve setZyöntemler - onlar sadece bir dönüş yeni , sizin aradığınız nesneyi değiştirerek yapısını yerine.

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

Peki sisteminizin bir bölümüne diğer bölümleri kısıtlarken onu değiştirme yeteneğini nasıl verirsiniz? Değişmez yapının bir örneğine referans içeren değişken bir nesne ile. Temelde, senin çalar sınıfı her sınıf kendi istatistiklerini de değişmez bir görünüm verirken onun istatistik nesneyi mutasyona edememek yerine, onun istatistikler vardır değişmez ve oyuncu değişkendir. Onun yerine:

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

Şunlara sahip olursunuz:

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

Farkı gör? Stats nesnesi tamamen değiştirilemez, ancak oyuncunun mevcut istatistikleri değişmez nesneye değiştirilebilir bir referanstır . Hiç iki arayüz yapmaya gerek yok - sadece veri yapısını tamamen değiştirilemez hale getirin ve durumu depolamak için kullandığınız yerde değiştirilebilir bir referansı yönetin.

Bu, sisteminizdeki diğer nesnelerin istatistik nesnesine yapılan bir referansa güvenemeyeceği anlamına gelir - sahip oldukları istatistik nesnesi, oyuncu istatistiklerini güncelledikten sonra oynatıcının sahip olduğu istatistik nesnesi olmayacaktır.

Bu daha mantıklı, çünkü kavramsal olarak değişen istatistikler değil , oyuncunun mevcut istatistikleri . İstatistikler nesnesi değil. Referans istatistiklerine oyuncu nesne vardır nesnesi. Bu nedenle, sisteminizin bu referansa bağlı diğer bölümleri player.currentStats(), oyuncunun temelindeki istatistik nesnesini ele geçirmek, bir yerde saklamak ve mutasyonlar yoluyla güncellenmesine güvenmek yerine açık bir şekilde referans vermelidir .


1

Vay be ... bu beni gerçekten geri götürüyor. Aynı fikri birkaç kez denedim. Bundan vazgeçemeyecek kadar inatçıydım çünkü öğrenecek iyi bir şey olacağını düşündüm. Başkalarının da kodda denemek gördüm. Gördüğüm uygulamaların çoğu sizinki gibi Salt Okunur arabirimleri FooViewerveya Salt ReadOnlyFooOkunur arabirimi olarak FooEditorveya WriteableFooJava'da bir FooMutatorkez gördüğümü düşünüyorum . İşleri bu şekilde yapmak için resmi veya hatta ortak bir kelime dağarcığı olduğundan emin değilim.

Bu, denediğim yerlerde benim için hiç yararlı bir şey yapmadı. Tamamen bundan kaçınırdım. Diğerlerinin önerdiği gibi bir adım geri atarım ve bu fikre gerçekten daha büyük fikrinizde ihtiyacınız olup olmadığını düşünürdüm. Aslında bunu denerken üretilen herhangi bir kod tutmadım çünkü bunu yapmak için doğru bir yol olduğundan emin değilim. Her seferinde hatırı sayılır bir çabadan ve basitleştirilmiş şeylerden sonra geri çekildim. Ve bununla, başkalarının YAGNI ve KISS ve DRY hakkında söylediklerinin çizgisinde bir şey kastediyorum.

Olası bir dezavantaj: tekrarlama. Potansiyel olarak birçok sınıf için bu arayüzleri oluşturmanız gerekecek ve hatta sadece bir sınıf için her yöntemi adlandırmanız ve her imzayı en az iki kez tanımlamanız gerekir. Kodlamada beni yakan her şeyden, birden fazla dosyayı bu şekilde değiştirmek zorunda kalmam beni en çok sıkıntıya sokar. Sonunda bir yerde veya başka yerde değişiklikler yapmayı unutuyorum. Foo adlı bir sınıfınız ve FooViewer ve FooEditor olarak adlandırılan arayüzleriniz varsa, Bar olarak adlandırırsanız daha iyi olacağına karar verirseniz, gerçekten şaşırtıcı derecede akıllı bir IDE'niz yoksa üç kez yeniden adlandırmanız gerekir. Hatta ben bir kod üreteci gelen kod önemli miktarda vardı durum olarak bulundu.

Şahsen ben de sadece tek bir uygulaması olan şeyler için arayüz oluşturmayı sevmiyorum. Bu kodda dolaştığım zaman sadece doğrudan uygulamaya atlayamıyorum, arayüze atlamak ve sonra arayüzün uygulanmasına atlamak ya da en azından oraya ulaşmak için birkaç ekstra tuşa basmak zorundayım. Bu şeyler toplanıyor.

Ve sonra bahsettiğiniz bir komplikasyon var. Şimdiye kadar kodumla bu şekilde tekrar gideceğimden şüpheliyim. Hatta bu en iyi kod için genel planı (ben inşa ORM) içine en uygun yer için bu çekti ve kod daha kolay bir şey ile değiştirildi.

Bahsettiğiniz kompozisyon için gerçekten daha fazla düşünürdüm. Bunun neden kötü bir fikir olduğunu düşündüğünü merak ediyorum. Ben EffectiveStatsbeste ve değişmez olması beklenirdi Statsve StatModifiershangi gibi StatModifieristatistikleri (geçici efektler, öğeleri, bazı geliştirme alanında konumu, yorgunluk) ne değiştirebilir temsil eden bazı s kümesi oluşturacak gibi bir şey olurdu ama sizin EffectiveStatsanlamak gerekir StatModifiersolmaz çünkü bu istatistiklerin ne olduğunu ve ne kadar etkili olduğunu ve hangi istatistikler üzerinde ne tür bir etkisinin olacağını yönetin. StatModifierfarklı şeyler için bir arayüz olabilir ve "bölgede miyim", "ilaç ne zaman yıpranır", vb. Sadece şu anda hangi statün ve nasıl değiştirildiğini söylemeliydi. Daha iyisi StatModifier, uygun şekilde değiştirilmiş olduğu için farklı olacak Statsbaşka bir Statsdeğişime dayanarak yeni bir değişmez üretmek için bir yöntemi ortaya çıkarabilir . Sonra böyle bir şey yapabilirsiniz currentStats = statModifier2.modify(statModifier1.modify(baseStats))ve tüm Statsdeğişmez olabilir. Hatta doğrudan kodlamak olmaz, muhtemelen tüm değiştiriciler arasında döngü ve her ile başlayan önceki değiştiricilerin sonucuna uygulamak baseStats.

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.