Yapıcıdaki ayarlayıcıları kullanmak neden yaygın bir kalıp haline gelmedi?


23

Erişimciler ve değiştiriciler (aka ayarlayıcılar ve alıcılar) üç ana nedenden dolayı faydalıdır:

  1. Değişkenlere erişimi kısıtlarlar.
    • Örneğin, bir değişkene erişilebilir, ancak değiştirilemez.
  2. Parametreleri doğrularlar.
  3. Bazı yan etkilere neden olabilirler.

İnternetteki üniversiteler, çevrimiçi kurslar, dersler, blog yazıları ve kod örnekleri, erişim sağlayıcıların ve değiştiricilerin önemi konusunda strese giriyor; Böylece kişi, aşağıdaki kod gibi herhangi bir ek değer sağlamadığında bile onları bulabilir.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Buna göre, parametreleri gerçekten doğrulayan ve bir istisna atan veya geçersiz girdi sağlanmışsa, bir boole döndüren, daha yararlı değiştiriciler bulmak çok yaygındır:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Fakat o zaman bile, neredeyse hiçbir zaman değiştiricilerin bir kurucudan çağrıldığını görmüyorum, bu yüzden karşılaştığım basit bir sınıfın en yaygın örneği şudur:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Ancak bu ikinci yaklaşımın çok daha güvenli olduğu düşünülüyor:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Deneyiminizde benzer bir kalıp görüyor musunuz, yoksa sadece şanssız mıyım? Ve eğer yaparsan, o zaman sence buna neden olan nedir? Yapıcılardan değiştiricileri kullanmak için bariz bir dezavantaj var mı, yoksa daha güvenli oldukları mı düşünüldü? Başka bir şey mi var?


1
@ STG, teşekkür ederim, ilginç nokta! Üzerinde genişleyebilir ve belki de olacak kötü bir şey örneği ile bir cevaba koyabilir misiniz?
Vlad Spreys


8
@ stg Aslında, yapıcıda geçersiz kılınabilir yöntemleri kullanmak bir anti-kalıptır ve birçok kod kalitesi aracı bunu bir hata olarak gösterecektir. Geçersiz kılınan sürümün ne yapacağını bilmiyorsunuz ve yapıcı bitmeden önce "bu" kaçışa izin vermek gibi garip şeylere yol açabilecek kötü şeyler yapabilir.
Michał Kosmulski

1
@gnat: Bunun bir kopyası olduğuna inanmıyorum Bir sınıfın yöntemleri kendi alıcılarını ve ayarlayıcılarını mı çağırmalı? çünkü yapıcının doğası gereği, tam olarak oluşturulmamış bir nesneye çağrılmaktadır.
Greg Burghardt,

3
Ayrıca, "onaylama" işleminizin başlatılmamış yaşı (ya da dile bağlı olarak sıfır veya başka bir şey) bıraktığını unutmayın. Yapıcının başarısız olmasını istiyorsanız, dönüş değerini kontrol etmeniz ve hataya bir istisna atmanız gerekir. Bu haliyle, onaylamanız az önce farklı bir hata ortaya koydu.
İşe yaramaz

Yanıtlar:


37

Çok genel felsefi akıl yürütme

Tipik olarak, bir kurucunun (post-koşullar olarak) inşa edilen nesnenin durumu hakkında bazı garantiler sağlamasını istiyoruz.

Genellikle, örnek yöntemlerin çağrıldıklarında bu garantilerin zaten geçerli olduğunu varsaydıklarını (ön koşullar olarak) kabul edebileceklerini ve yalnızca bunları kırmamaya özen göstereceklerini bekleriz.

Yapıcı içinden bir örnek yöntemi çağırmak, bu garantilerin bir kısmının veya tamamının henüz belirlenmemiş olabileceği anlamına gelir; bu, örnek yönteminin ön koşullarının yerine getirilip getirilmediğinin nedenini zorlaştırır. Doğru olsa bile, örneğin karşısında çok kırılgan olabilir. yeniden sipariş verme örneği yöntemi çağrıları veya diğer işlemler.

Diller, yapıcı hala çalışırken temel sınıflardan miras alınan / alt sınıflar tarafından geçersiz kılınan örnek yöntemlere yapılan çağrıları çözümleme biçiminde de değişiklik gösterir. Bu, başka bir karmaşıklık katmanı ekler.

Belirli örnekler

  1. Bunun nasıl görünmesi gerektiğini düşündüğünüze dair kendi örneğiniz yanlış:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    bu dönüş değerini kontrol etmez setAge. Anlaşılan belirleyiciyi aramak, sonuçta doğruluk garantisi değil.

  2. Başlatma sırasına bağlı olarak olduğu gibi çok kolay hatalar:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    geçici kayıtlarımın her şeyi kırdığı yer. Tüh!

Yapıcıdan bir ayarlayıcıyı çağırmak israf varsayılan bir başlatma anlamına gelen C ++ gibi diller de vardır (bazı üye değişkenleri için en azından kaçınılması gereken)

Basit bir teklif

Çoğu kodun böyle yazılmadığı doğrudur , ancak yapıcınızı temiz ve öngörülebilir tutmak ve yine de durum öncesi ve sonrası mantığınızı yeniden kullanmak istiyorsanız, en iyi çözüm şudur:

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

veya mümkünse daha da iyisi: özellik türündeki kısıtlamayı kodlayın ve atamadaki kendi değerini doğrulamasını isteyin:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

Ve son olarak, tam bir bütünlük için, C ++ 'da kendi kendini doğrulayan bir sarıcı. Hala sıkıcı doğrulama yapıyor olmasına rağmen, bu sınıf başka hiçbir şey yapmadığından , kontrol etmenin nispeten kolay olduğunu unutmayın.

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

Tamam, tamamlanmadı, çeşitli kopyalar bıraktım ve yapıcıları ve ödevleri taşıdım.


4
Ağır undervoted cevap! Ekleyeyim, çünkü OP bir ders kitabındaki tanımlanmış durumu gördüğü için bunu iyi bir çözüm yapmaz. Sınıfta bir öznitelik ayarlamak için iki yol varsa (yapıcı ve ayarlayıcıya göre) ve biri bu öznitelikte bir kısıtlama uygulamak isterse, her iki yol da kısıtlamayı kontrol etmelidir, aksi takdirde bu bir tasarım hatasıdır .
Doktor Brown,

Yani bir yapıcı kötü pratikte setter yöntemleri kullanarak dikkate alacağını eğer orada ayarlayıcıları 1) hiçbir mantığı, ya ayarlayıcıları 2) mantık devletin bağımsızdır? Sık sık yapmıyorum ama şahsen ben tam olarak ikinci nedenden dolayı bir
kurucuda

Zorunluluk her zaman geçerli şartıyla bir tür varsa, o ise setter mantık. Bu mantığı üyede kodlamanın mümkün olduğunu kesinlikle düşünüyorum , bu yüzden kendi içinde yer almak zorunda ve sınıfın geri kalanına bağımlılık kazanamıyor.
Yararsız

13

Bir nesne inşa edilirken, tanımı gereği tam olarak oluşturulmamıştır. Belirleyicinin nasıl verdiğini düşünün:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Doğrulamanın bir kısmı , nesnenin yalnızca yaşının artmasını sağlamak setAge()için agemülke karşı bir kontrol içeriyorsa ne olur ? Bu durum şöyle görünebilir:

if (age > this.age && age < 25) {
    //...

Bu oldukça masum bir değişime benziyor, çünkü kediler zaman içerisinde yalnızca bir yönde hareket edebiliyor. Tek sorun, this.agebunun this.agezaten geçerli bir değere sahip olduğunu varsaydığı için başlangıç ​​değerini ayarlamak için kullanamamanızdır .

Yapıcılardaki ayarlayıcılardan kaçınmak, yapıcının yaptığı tek şeyin örnek değişkenini ayarlamak ve ayarlayıcıda gerçekleşen diğer davranışları atlamak olduğunu açıkça belirtir .


Tamam ... ama nesne yapısını gerçekten sınamak istemiyorsanız ve potansiyel hataları göstermiyorsanız, her iki durumda da potansiyel hatalar vardır . Ve şimdi iki iki probleminiz var: Bu gizli potansiyel böceğiniz var ve yaş aralığı kontrollerini iki yerde zorlamanız gerekiyor.
svidgen,

Java / C # 'da olduğu gibi, hiçbir sorun yapıcı tarafından çağrılmadan önce sıfırlanır.
Deduplicator

2
Evet, kuruculardan örnek çağırma yöntemlerini çağırmak zor olabilir ve genellikle tercih edilmez. Şimdi, doğrulama mantığınızı özel, son sınıf / statik bir yönteme taşımak ve hem kurucu hem de ayarlayıcıdan çağırmak istiyorsanız, iyi olurdu.
İşe yaramaz

1
Hayır, örnek olmayan yöntemler, örnek olarak oluşturulmuş bir örneğe dokunmadıkları için örnek yöntemlerin kandırıcılığından kaçınırlar. Elbette, inşaatın başarılı olup olmadığına karar vermek için doğrulama sonucunu yine de kontrol etmeniz gerekiyor.
İşe yaramaz

1
Bu cevap IMHO'nun noktasını kaçırıyor. "Bir nesne inşa edilirken, tanım gereği tam olarak oluşturulmaz" - yapım sürecinin ortasında, elbette, ancak yapımcı yapıldığında, nitelikler üzerindeki amaçlanan herhangi bir kısıtlama tutmalı, aksi halde bu kısıtlamayı aşabilir. Buna ciddi bir tasarım hatası diyebilirim.
Doktor Brown

6

Burada zaten bazı iyi cevaplar var, fakat şu ana kadar kimse buradaki karışıklıklara neden olan iki şeyi sorduğunuzu fark etmiş görünüyor. IMHO gönderiniz en iyi iki ayrı soru olarak görülüyor :

  1. bir pasifte bir kısıtlama geçerliliği varsa, atlanmayacağından emin olmak için sınıfın kurucusunda da olmamalı mı?

  2. Neden ayarlayıcıyı doğrudan bu amaç için yapıcıdan çağırmıyorsunuz?

İlk soruların cevabı açıkça "evet" tir. Bir istisna atamadığında, yapıcı nesneyi geçerli bir durumda bırakmalıdır ve belirli nitelikler için belirli değerler yasaklanmışsa, o zaman traktörü bu durumu atlatması kesinlikle mantıklı değildir.

Bununla birlikte, ikinci sorunun cevabını tipik olarak "hayır" dır, belirleyiciyi türetilmiş bir sınıfta geçersiz kılmamak için önlem almadığınız sürece. Bu nedenle, daha iyi alternatif, kısıtlama doğrulamasının, ayarlayıcıdan ve yapıcıdan yeniden kullanılabilecek özel bir yöntemde uygulanmasıdır. Buraya tam olarak bu tasarımı gösteren @ Yararsız örneği tekrar etmeye gelmiyorum.


Yapıcının kullanması için neden mantığın belirleyiciden çıkarılmasının daha iyi olduğunu anlamadım. ... Bu sadece ayarlayıcıları makul bir sıra ile çağırmaktan gerçekten farklı mı? Özellikle, eğer ayarlayıcılarınız "olması" gerektiği kadar basitse,
kurucunuzun

2
@svidgen: bkz. JimmyJames örneği: kısacası, bir serviste geçersiz kılınabilecek yöntemleri kullanmak iyi bir fikir değildir, bu sorunlara neden olacaktır. Son halini aldıysa ve başka bir geçersiz kılınabilir metodu çağırmıyorsa, ayarlayıcıyı ctor'da kullanabilirsiniz. Dahası, onaylama işleminin başarısız olduğu zaman başa çıkma yönteminden ayrılması, size depoda ve hazırlayıcıda farklı şekilde ele alma imkanı sunar.
Doktor Brown,

Şimdi daha az ikna oldum! Ama, beni diğer cevabı işaret ettiğin için teşekkürler ...
svidgen

2
By makul sırayla Şunu bir kırılgan olan, görünmez sipariş bağımlılık kolayca masum görünüşlü değişiklikler kırılabilir sağ?
İşe yaramaz

@ useless iyi, hayır ... Demek istediğim, kodunuzun yürütüldüğü sıranın önemli olmamasını önermeniz komik. ama, asıl mesele bu değildi. Tartışmalı örneklerin ötesinde, deneyimimde bir düzenleyicide mülkler arası doğrulamalar yapmanın iyi bir fikir olduğunu düşündüğüm bir örneği hatırlayamıyorum - bu tür bir şey mutlaka "görünmez" başlatma sırası gereksinimlerine yol açar . Bu
istilaların bir kurucuda

2

İşte yapıcınızda son olmayan yöntemleri kullanarak karşılaşabileceğiniz sorunların türünü gösteren aptalca bir Java kodu:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Bunu çalıştırdığımda, NextProblem yapıcısında bir NPE alıyorum. Bu elbette önemsiz bir örnektir, ancak birden fazla kalıtım seviyeniz varsa, işler hızla karışabilir.

Bunun yaygınlaşmamasının en büyük sebebi, kodu daha iyi anlaşılmasını zorlaştırması. Şahsen, neredeyse hiçbir zaman belirleyici yöntemlere sahip değilim ve üye değişkenlerim neredeyse her zaman değişmez (kesin amaçlanan) final. Bu nedenle, kurucuda değerlerin ayarlanması gerekir. Değişmez nesneler kullanıyorsanız (ve bunu yapmanın birçok iyi nedeni var), soru tartışılır.

Her durumda, doğrulamayı veya diğer mantığı yeniden kullanmak iyi bir amaçtır ve bunu statik bir yönteme koyabilir ve hem yapıcı hem de ayarlayıcıdan başlatabilirsiniz.


Bu, nasıl yapıcı olmayan bir kalıtım problemi yerine, kurucuda belirleyici kullanmanın bir sorunu? Başka bir deyişle, temel kurucuyu etkin bir şekilde geçersiz kılıyorsanız, neden onu çağırıyorsunuz?
svidgen,

1
Bence asıl mesele, bir pasörü geçersiz kılmak için, kurucuyu geçersiz kılmaması gerektiğidir.
Chris Wohlert

@ChrisWohlert Tamam ... ancak ayarlayıcı mantığınızı yapıcı mantığınızla çakışması için değiştirirseniz, yapıcınızın ayarlayıcıyı kullanıp kullanmadığına bakılmaksızın bu bir problemdir. Ya inşaat zamanında başarısız olur, sonra başarısız olur ya da görünmez bir şekilde başarısız olur (b / c, iş kurallarınızı atladığınızdan).
svidgen 13

Sık sık, temel sınıfın Oluşturucusunun nasıl uygulandığını bilmiyorsunuzdur (bir yerde bir 3. parti kütüphanesinde olabilir). Geçersiz kılınabilir bir yöntemi geçersiz kılmak, temel uygulamanın sözleşmesini ihlal etmemelidir, ancak (ve belki de bu örnek namealanı ayarlayarak bunu yapmaz ).
Hulk

Svidgen, buradaki sorun, yapıcıdan geçersiz kılma yöntemlerini çağırmanın, bunları geçersiz kılmanın yararını azalttığıdır - geçersiz kılınan sürümlerde alt sınıfın hiçbir alanını kullanamazsınız, çünkü henüz başlatılmamış olabilirler. Daha da kötüsü, thisbaşka bir yönteme cevap vermemelisiniz, çünkü henüz tam olarak başlatıldığından emin olamazsınız.
Hulk

1

Değişir!

Basit bir onaylama yapıyorsanız veya ayarlayıcıda her özellik belirlendiğinde vurulması gereken yaramaz yan etkiler çıkarıyorsanız: ayarlayıcıyı kullanın. Alternatif, genellikle ayarlayıcı mantığını ayarlayıcıdan çıkarmak ve ardından yeni ayarlayıcıyı çağırmak, ardından da ayarlayıcıdan ve yapıcıdan asıl kümeyi izlemektir - ki bu da ayarlayıcıyı kullanmakla aynı şekilde etkilidir! (Şimdi hariç, kuşkusuz küçük, iki satırlı ayarlayıcınızı iki yerde tutuyorsunuz.)

Ancak , nesneyi belirli bir düzenleyicinin (veya iki!) Başarıyla yürütmeden önce düzenlemek için karmaşık işlemler yapmanız gerekirse , ayarlayıcıyı kullanmayın.

Ve her iki durumda da, test durumları var . Hangi rotaya gideceğinize bakmaksızın, birim testleriniz varsa, her iki seçenekle ilişkili tuzakları açığa vurma şansınız yüksektir.


Ayrıca şunu da ekleyeceğim, eğer o kadar uzaktaki hatıralarım uzaktan bile doğru olsa, bu da üniversitede öğretildiğim bir tür akıl yürütme. Ve üzerinde çalışabileceğim başarılı projelerle aynı çizgide değil.

Tahmin etmem gerekiyorsa, bir kurucuda ayarlayıcı kullanmaktan kaynaklanan bazı tipik hataların tespit edilebildiği bir noktada "temiz kod" notunu kaçırdım. Ama, benim açımdan, böyle böceklerin hiç kurbanı olmadım ...

Dolayısıyla, bu özel kararın kapsamlı ve dogmatik olması gerekmediğini savunuyorum.

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.