SOLID ve statik yöntemler


11

Sık sık karşılaştığım bir sorun var: Ürün sınıfı olan bir web mağazası projesi olsun. Kullanıcıların bir ürüne yorum göndermesine olanak tanıyan bir özellik eklemek istiyorum. Bu yüzden bir ürünü referans alan bir Review sınıfım var. Şimdi bir ürün için tüm değerlendirmeleri listeleyen bir yönteme ihtiyacım var. İki olasılık vardır:

(A)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

Koda bakarak, (A) seçerim: Statik değildir ve bir parametreye ihtiyaç duymaz. Ancak, (A) 'nın Tek Sorumluluk İlkesini (SRP) ve Açık-Kapalı İlkesini (OCP) ihlal ettiğini;

  • (SRP) Bir ürün için incelemelerin toplanma şeklini değiştirmek istediğimde, Ürün sınıfını değiştirmem gerekiyor. Ancak Ürün sınıfını değiştirmenin tek bir nedeni olmalı. Ve bu kesinlikle değerlendirme değil. Ürün'teki ürünlerle ilgili her özelliği paketlersem, yakında işlenecektir.

  • (OCP) Bu özellik ile genişletmek için Product sınıfını değiştirmem gerekiyor. Bence bu ilkenin 'Değişim için kapalı' kısmını ihlal ediyor. Müşterinin yorumları uygulamak için isteğini almadan önce, Ürünü bitmiş olarak kabul ettim ve "kapattım".

Daha da önemlisi: SOLID ilkelerini takip etmek veya daha basit bir arayüze sahip olmak?

Yoksa burada yanlış bir şey mi yapıyorum?

Sonuç

Vay, tüm harika cevapların için teşekkürler! Resmi cevap olarak birini seçmek zor.

Cevaplardan ana argümanları özetleyeyim:

  • pro (A): OCP, yasaların da okunabilirliği ve yasa değildir.
  • pro (A): varlık ilişkisi gezilebilir olmalıdır. Her iki sınıf da bu ilişkiyi bilebilir.
  • pro (A) + (B): her ikisini de yapın ve (A) ila (B) arasında temsilci seçin, böylece Ürünün tekrar değiştirilmesi daha az olasıdır.
  • pro (C): bulucu yöntemlerini statik olmadığı üçüncü sınıfa (hizmet) koyun.
  • kontra (B): testlerde alay etmeyi engeller.

İş hayatındaki kolejlerimin katkıda bulunduğu birkaç şey daha:

  • pro (B): ORM çerçevemiz otomatik olarak (B) kodunu oluşturabilir.
  • pro (A): ORM çerçevemizin teknik nedenlerinden ötürü, bazı durumlarda "kapalı" varlığı, bulucunun gittiği yerden bağımsız olarak değiştirmek gerekecektir. Bu yüzden her zaman SOLID'e bağlı kalamam.
  • kontra (C): çok karışıklık ;-)

Sonuç

Mevcut projem için delegasyonla her iki (A) + (B) kullanıyorum. Ancak hizmet odaklı bir ortamda (C) ile devam edeceğim.


2
Statik bir değişken olmadığı sürece her şey iyidir. Statik yöntemler test edilmesi basit, izlemesi basit.
Kodlayıcı

3
Neden sadece bir ProductsReviews sınıfı yok? Ardından Ürün ve İnceleme aynı kalır. Ya da belki yanlış anlarım.
ElGringoGrande

2
@Coder "Statik yöntemleri test etmek kolaydır", gerçekten mi? Alay edilemezler , daha fazla bilgi için bkz . Googletesting.blogspot.com/2008/12/… .
StuperUser

1
@StuperUser: Alay edecek bir şey yok. Assert(5 = Math.Abs(-5));
Kodlayıcı

2
Test Abs()etmek sorun değil, ona bağlı bir şeyi test etmek. Bir sahte kullanmak için Bağımlı Test Altında Kodunu (CUT) izole etmek için bir dikişiniz yok. Bu, onu bir atom birimi olarak test edemeyeceğiniz anlamına gelir ve tüm testleriniz, birim mantığını test eden entegrasyon testleri haline gelir. Bir testteki başarısızlık CUT'ta veya Abs()(veya bağımlı kodunda) olabilir ve birim testlerin teşhis faydalarını ortadan kaldırır.
StuperUser

Yanıtlar:


7

Daha da önemlisi: SOLID ilkelerini takip etmek veya daha basit bir arayüze sahip olmak?

SOLID ile Arayüz

Bunlar birbirini dışlayan değil. Arayüz, iş modelinizin doğasını ideal olarak iş modeli açısından ifade etmelidir. SOLID ilkeleri, Nesneye Dayalı kod sürdürülebilirliğini en üst düzeye çıkarmak için bir Koan'dır (ve en geniş anlamda "sürdürülebilirlik" anlamına gelir). İlki iş modelinizin kullanımını ve manipülasyonunu destekler ve ikincisi kod bakımını optimize eder.

Açık / Kapalı Prensibi

"Ona dokunma!" çok basit bir yorumdur. Ve "sınıf" demek istediğimizi varsayarsak, keyfi ve her zaman doğru değil. Daha ziyade, OCP, kodunuzu, davranışını değiştirmenin mevcut, çalışan kodu doğrudan değiştirmenizi gerektirmeyecek (olmamalıdır) tasarladığınız anlamına gelir. Ayrıca, ilk etapta koda dokunmamak, mevcut arayüzlerin bütünlüğünü korumanın ideal yoludur; bence bu OCP'nin önemli bir sonucu.

Son olarak OCP'yi mevcut tasarım kalitesinin bir göstergesi olarak görüyorum. Kendimi açık sınıfları (veya yöntemleri) çok sık çatlıyor ve / veya gerçekten sağlam (ha, ha) nedenler olmadan bulursam, bu bana kötü bir tasarımım olduğunu söylüyor olabilir (ve / veya yok OO kodlamayı biliyorum).

En iyi atışını yap, yanında duran bir doktor ekibimiz var

Gereksinim analiziniz, Ürün-Gözden Geçirme ilişkisini her iki açıdan da ifade etmeniz gerektiğini söylüyorsa, bunu yapın.

Bu nedenle Wolfgang, mevcut sınıfları değiştirmek için iyi bir nedeniniz olabilir. Yeni gereksinimler göz önüne alındığında, bir İnceleme artık bir Ürünün temel bir parçasıysa, Ürünün her uzantısının İncelemeye ihtiyacı varsa, bunu yapmak müşteri kodunu uygun şekilde ifade ederse, Ürüne entegre edin.


1
OCP'nin herhangi bir kod için hızlı bir kural değil, kaliteli bir gösterge olduğunu belirtmek için +1. OCP'yi doğru bir şekilde takip etmenin zor veya imkansız olduğunu fark ederseniz, daha esnek bir yeniden kullanıma izin vermek için modelinizin yeniden düzenlenmesi gereken bir işarettir.
CodexArcanum

Ürün'ün projenin temel bir varlığı olduğunu ve İnceleme'nin yalnızca bir eklenti olduğunu belirtmek için Ürün ve İnceleme örneğini seçtim. Dolayısıyla Ürün zaten var, bitmiş ve Gözden Geçirme daha sonra geliyor ve mevcut kodu açmadan tanıtılmalıdır (Ürün dahil).
Wolfgang

10

SOLID yönergelerdir, bu nedenle dikte etmek yerine karar vermeyi etkileyin.

Statik yöntemler kullanırken bilinmesi gereken bir şey, test edilebilirlik üzerindeki etkisidir .


Test etmek forProduct(Product product)sorun olmayacak.

Buna bağlı bir şeyi test etmek olacak.

Bir sahte kullanmak için bağımlı Test Altında Kodunu (CUT) izole etmek için bir dikişiniz olmayacaktır, çünkü uygulama çalışırken statik yöntemler mutlaka vardır.

CUT()Bu çağrılar denilen bir metoda sahip olalımforProduct()

Eğer forProduct()bir statictest edemez CUT()atom birim olarak ve testlerin tüm test ünitesi mantığı bu entegrasyon testleri olurlar.

CUT testindeki bir başarısızlık , birim testlerin teşhis faydalarını ortadan kaldıran CUT()veya içindeki forProduct()(veya bağımlı kodlarından herhangi birinde) bir sorundan kaynaklanabilir .

Daha ayrıntılı bilgi için bu mükemmel blog yayınına bakın: http://googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


Bu, başarısız testlerle hayal kırıklığına ve onları çevreleyen iyi uygulamaların ve faydaların terk edilmesine yol açabilir.


1
Bu çok iyi bir nokta. Genel olarak, benim için olmasa da. ;-) Birden fazla sınıfı kapsayan birim testleri yazma konusunda çok sıkı değilim. Hepsi veritabanına iniyor, bu benim için sorun değil. Bu yüzden iş nesnesiyle veya onun bulucusuyla alay etmeyeceğim.
Wolfgang

+1, testler için kırmızı bayrağı ayarladığınız için teşekkürler! @Wolfgang'a katılıyorum, genellikle çok katı değilim ama bu tür testlere ihtiyacım olduğunda statik yöntemlerden gerçekten nefret ediyorum. Genellikle bir statik yöntem parametreleriyle çok fazla etkileşime giriyorsa veya başka bir statik yöntemle etkileşime giriyorsa bunu bir örnek yöntem yapmayı tercih ederim.
Adriano Repetti

Bu tamamen kullanılan dile bağlı değil mi? OP bir Java örneği kullandı, ancak soruda bir dilden hiç bahsetmedi veya etiketlerde belirtilmedi.
Mart'ta Izkata

1
@StuperUser If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.- Javascript ve Python'un statik yöntemlerin geçersiz kılınmasına / alay edilmesine izin verdiğine inanıyorum. Yine de% 100 emin değilim.
Mart'ta Izkata

1
@Izkata JS dinamik olarak yazılmıştır, bu yüzden yoktur static, bir kapatma ve tekli desenle simüle edersiniz. Python'da okuma yaparken (özellikle stackoverflow.com/questions/893015/… ), miras almanız ve uzatmanız gerekir. Geçersiz kılma alay konusu değildir; kodu atom birimi olarak test etmek için hala bir dikişiniz yok gibi görünüyor.
StuperUser

4

Ürünün, Ürün İncelemelerini bulmak için doğru yer olduğunu düşünüyorsanız, Ürüne her zaman işi yapmak için bir yardım sınıfı verebilirsiniz. (İşletmenizin ürün dışında bir inceleme hakkında asla konuşmayacağını çünkü anlatabilirsiniz).

Örneğin, bir inceleme geri almak rolünü oynayan bir şey enjekte etmek cazip olurdu. Muhtemelen arabirimi verirdim IRetrieveReviews. Bunu ürünün yapıcısına (Bağımlılık Enjeksiyonu) koyabilirsiniz. İncelemelerin nasıl alınacağını değiştirmek isterseniz, farklı bir ortak çalışanı (a TwitterReviewRetrieverveya bir AmazonReviewRetrieverveya MultipleSourceReviewRetrieverveya başka bir şey) enjekte ederek kolayca yapabilirsiniz .

Artık her ikisinin de tek bir sorumluluğu var (sırasıyla ürünle ilgili her şeye gitmek ve incelemeleri almak) ve ileride ürünün incelemelere ilişkin davranışı, ürünü gerçekten değiştirmeden değiştirilebilir (genişletebilirsiniz) bir şekilde ProductWithReviews) gerçekten KATI ilkeler hakkında bilgiçlik olmak istedim, ama bu benim için yeterince iyi olurdu.


Servis / bileşen odaklı yazılımda çok yaygın olan DAO modeli gibi görünüyor. Bu fikri seviyorum çünkü nesneleri almanın bu nesnelerin sorumluluğu olmadığını ifade ediyor. Ancak, hizmet odaklı nesneye yönelik bir yol tercih çünkü.
Wolfgang

1
Gibi bir arayüz kullanmak IRetrieveReviews, servis odaklı olmasını engeller - incelemeleri neyin, nasıl veya ne zaman alacağını belirlemez. Belki de bu gibi şeyler için birçok yöntem içeren bir hizmettir. Belki de bu bir şeyi yapan bir sınıftır. Belki bir havuzdur veya bir sunucuya HTTP isteği yapar. Bilmiyorsun. Bilmemelisin. Mesele bu.
Lunivore

Evet, bu yüzden strateji modelinin bir uygulaması olacaktır. Bu üçüncü sınıf için bir argüman olurdu. (A) ve (B) bunu desteklemez. Bulucu kesinlikle bir ORM kullanacaktır, bu nedenle algoritmayı değiştirmek için hiçbir neden yoktur. Sorumda bu konuda net olmadığım için üzgünüm.
Wolfgang

3

Bir ProductsReview sınıfı olurdu. İncelemenin zaten yeni olduğunu söylüyorsun. Bu sadece bir şey olabileceği anlamına gelmez. Değişmek için hala tek bir nedeni olmalı. Herhangi bir nedenle değerlendirmeleri nasıl alacağınızı değiştirirseniz, Review sınıfını değiştirmeniz gerekir.

Bu doğru değil.

Statik yöntemi Review sınıfına koyuyorsunuz çünkü ... neden? Mücadele ettiğin şey bu değil mi? Bütün sorun bu değil mi?

Öyleyse yapma. Sorumluluğun sadece ürün incelemelerini aldığı bir sınıf oluşturun. Daha sonra bunu ProductReviewsByStartRating değerine alt sınıflandırabilirsiniz. Veya bir ürün sınıfı için yorumlar almak üzere alt sınıflara ayırın.


İnceleme yöntemine sahip olmanın SRP'yi ihlal edeceğini kabul etmiyorum. Ürün üzerinde, ama İnceleme değil. Benim sorunum statik olmasıydı. Eğer yöntemi üçüncü sınıfa taşımış olsaydım hala durağan olurdu ve product parametresine sahip olurdu.
Wolfgang

Yani Review sınıfının tek sorumluluğunun, değişmesinin tek sebebinin forProduct statik yönteminin değişmesi gerekip gerekmediğini söylüyorsunuz? Review sınıfında başka işlev yok mu?
ElGringoGrande

İncelemede bir sürü şey var. Ancak bir forProduct (Ürün) 'un güzelce uygun olacağını düşünüyorum. İnceleme Bulma, İncelemenin niteliklerine ve yapısına (hangi benzersiz tanımlayıcılar, hangi kapsamı değiştiren nitelikler?) Bağlıdır. Bununla birlikte, Ürün İncelemeleri bilmez ve bilmemelidir.
Wolfgang

3

ProductSınıfta veya sınıfta 'Ürün için İnceleme Al' işlevini Reviewkoymazdım ...

Ürünlerinizi alabileceğiniz bir yeriniz var, değil mi? Bir şey GetProductById(int productId)ve belki GetProductsByCategory(int categoryId)vb.

Benzer şekilde, Yorumlarınızı almak için a GetReviewbyId(int reviewId)ve belki de a ile bir yeriniz olmalıdır GetReviewsForProduct(int productId).

Bir ürün için incelemelerin toplanma şeklini değiştirmek istediğimde, Ürün sınıfını değiştirmek zorundayım.

Veri erişiminizi alan adı sınıflarınızdan ayırırsanız , incelemelerin toplanma şeklini değiştirdiğinizde her iki alan sınıfını da değiştirmeniz gerekmez .


Bu şekilde halledeceğim ve yazıma kadar bu cevabın eksikliğinden kafam karıştı (ve kendi fikirlerimin uygun olup olmadığı konusunda endişeliyim). Açıkçası ne bir raporu temsil eden sınıf ne de bir ürünün temsili diğerini almaktan sorumlu olmamalıdır. Bir tür veri sağlama hizmeti bunu ele almalıdır.
CodexArcanum

@CodexArcanum Eh, yalnız değilsin. :-)
Eric King

2

Desenler ve ilkeler, taşta yazılı kurallar değil, kılavuzdur. Benim içinde görüş o KATI ilkelerini takip etmek veya daha basit bir arayüz tutmak daha iyidir eğer soru değil. Kendinize sormanız gereken şey, çoğu insan için daha okunabilir ve anlaşılabilir olan şeydir . Bu genellikle alan adına olabildiğince yakın olması gerektiği anlamına gelir.

Bu durumda (B) çözümünü tercih ederim çünkü benim için başlangıç ​​noktası İnceleme değil, Ürün'tür, ancak incelemeleri yönetmek için bir yazılım yazdığınızı hayal edin. Bu durumda merkez İnceleme'dir, böylece çözüm (A) tercih edilebilir.

Böyle bir çok yöntem (sınıflar arasında "bağlantılar") olduğunda ben hepsini dışarıda şerit ve onları düzenlemek için bir (veya daha fazla) yeni statik sınıf oluşturmak. Genellikle bunları sorgu veya bir tür depo olarak görebilirsiniz.


Ayrıca her iki ucun anlambilimi ve hangisinin "daha güçlü" olduğu fikrini de düşünüyordum, bu yüzden bulucuyu iddia edecektir. Bu yüzden (B) ile geldim. Aynı zamanda işletme sınıflarının “soyutlama” hiyerarşisinin yaratılmasına yardımcı olacaktır. Ürün, İncelemenin daha yüksek olduğu basit bir nesneydi. Bu nedenle, İnceleme Ürüne referans verebilir ancak tam tersi olamaz. Bu şekilde, listemizde başka bir sorun çözülecek olan işletme sınıfları arasındaki referans döngülerinden kaçınılabilir.
Wolfgang

Küçük bir sınıf seti için (A) ve (B) BOTH yöntemleri sağlamak bile güzel olabilir. Bu mantığı yazarken şu anda çok fazla kullanıcı arayüzü bilinmiyor.
Adriano Repetti

1

Ürününüz sadece statik Gözden Geçirme yönteminize yetki verebilir, bu durumda doğal bir konumda (Product.getReviews) kullanışlı bir arayüz sağlarsınız ancak uygulama ayrıntılarınız Review.getForProduct'tedir.

SOLID yönergelerdir ve basit, mantıklı bir arayüzle sonuçlanmalıdır. Alternatif olarak, SOLID'i basit, mantıklı arayüzlerden türetebilirsiniz. Her şey kod içindeki bağımlılık yönetimi ile ilgilidir. Amaç, sürtünme yaratan ve kaçınılmaz değişime engel oluşturan bağımlılıkları en aza indirmektir.


Bunu isteyeceğime emin değilim. Bu şekilde, her iki yerde de iyi bir yöntem var çünkü diğer geliştiricilerin nereye koyduğumu görmek için her iki sınıfa da bakmaları gerekmiyor. Öte yandan, hem statik yöntemin hem de "kapalı" Ürün sınıfının değişmesinin dezavantajlarına sahip olurdum . Heyetin sürtünme sorunundan yardım ettiğinden emin değilim. Bulucuyu değiştirmek için herhangi bir neden olsaydı, kesinlikle imzanın ve dolayısıyla Üründe tekrar değişikliğin ortaya çıkmasına neden olacaktır.
Wolfgang

OCP'nin anahtarı, kodunuzu değiştirmeden uygulamaları değiştirebilmenizdir. Uygulamada bu Liskov İkame ile yakından ilişkilidir. Bir uygulama diğerinin yerine geçmelidir. Hem IGetReviews.getReviews hem de getReviewsForProducts uygulayan farklı sınıflar olarak DatabaseReviews ve SoapReviews olabilir. Daha sonra sisteminiz uzantıya açıktır (incelemelerin alınma biçimini değiştirir) ve Değişiklik için kapalıdır (IGetReviews bağımlılıkları bozulmaz). Bağımlılık yönetimi derken bunu kastediyorum. Sizin durumunuzda, davranışını değiştirmek için kodunuzu değiştirmeniz gerekecektir.
pfries

Tanımladığınız Bağımlılık İnversiyon Prensibi (DIP) değil mi?
Wolfgang

Bunlar ilgili ilkelerdir. İkame noktanız DIP tarafından sağlanır.
pfries

1

Bunu diğer cevapların çoğundan biraz farklı bir şekilde ele alıyorum. Bence Productve Reviewtemelde veri aktarım nesneleri (DTOs). Kodumda DTOs / Entities davranışlarımdan kaçınmaya çalışıyorum. Modelimin mevcut durumunu saklamak için sadece güzel bir API.

OO ve SOLID hakkında konuştuğunuzda genellikle durumu (zorunlu olarak) temsil etmeyen (bunun yerine) sizin için soruları cevaplayan veya işinizin bir kısmını temsil edebileceğiniz bir tür hizmeti temsil eden bir "nesne" hakkında konuşuyorsunuz. . Örneğin:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

Sonra gerçek ProductRepositorybir döneceğini ExistingProductiçin GetProductByProductIdyöntemiyle vs.

Şimdi tek sorumluluk prensibini takip ediyorsunuz (miras kalan her ne olursa olsun IProductdevlete tutunuyor ve miras kalan her IProductRepositoryşey veri modelinize nasıl devam edeceğinizi ve rehidre edeceğinizi bilmekle sorumludur ).

Veritabanı şemanızı değiştirirseniz, DTO'larınızı vb. Değiştirmeden depo uygulamanızı değiştirebilirsiniz.

Kısacası, sanırım seçeneklerinizin hiçbirini seçmezdim. :)


0

Statik yöntemleri test etmek daha zor olabilir, ancak bu onları kullanamayacağınız anlamına gelmez - sadece onları test etmeniz gerekmeyecek şekilde ayarlamanız gerekir.

Hem product.GetReviews hem de Review.ForProduct gibi bir satır yöntem üretin

new ReviewService().GetReviews(productID);

ReviewService, daha karmaşık kodları içerir ve test edilebilirlik için tasarlanmış ancak doğrudan kullanıcıya açık olmayan bir arayüze sahiptir.

% 100 kapsama sahip olmanız gerekiyorsa, entegrasyon testlerinizin ürün / inceleme sınıfı yöntemlerini aramasını sağlayın.

Sınıf tasarımından ziyade genel API tasarımını düşünürseniz yardımcı olabilir - bu bağlamda mantıksal gruplama ile basit bir arayüz önemli olan tek şeydir - gerçek kod yapısı sadece geliştiriciler için önemlidir.

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.