Tek Sorumluluk İlkesinin Uygulanabilirliği


40

Geçenlerde görünüşte önemsiz bir mimari sorunla karşılaştım. Kodumda şöyle denilen basit bir havuz vardı (kod C # 'dadır):

var user = /* create user somehow */;
_userRepository.Add(user);
/* do some other stuff*/
_userRepository.SaveChanges();

SaveChanges Veritabanında değişiklik yapan basit bir paketleyiciydi:

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
}

Daha sonra, bir süre sonra, bir sistemde her kullanıcı oluşturulduğunda e-posta bildirimleri gönderecek yeni bir mantık uygulamam gerekiyordu. Sistemde _userRepository.Add()ve SaveChangesçevresinde pek çok çağrı olduğundan , SaveChangesbu şekilde güncelleme yapmaya karar verdim :

void SaveChanges()
{
    _dataContext.SaveChanges();
    _logger.Log("User DB updated: " + someImportantInfo);
    foreach (var newUser in dataContext.GetAddedUsers())
    {
       _eventService.RaiseEvent(new UserCreatedEvent(newUser ))
    }
}

Bu şekilde, harici kod UserCreatedEvent'e abone olabilir ve bildirimleri gönderecek gerekli iş mantığını ele alabilir.

Ancak bana yapılan değişikliklerin SaveChangesTek Sorumluluk ilkesini ihlal ettiği ve bunun SaveChangesyalnızca herhangi bir olayı kurtarması ve ateşlememesi gerektiği belirtildi.

Bu geçerli bir nokta mı? Bana öyle geliyor ki burada bir olay yaratmanın esas olarak log kaydı yapmakla aynı şey olduğu görülüyor: fonksiyona bazı yan işlevler eklemek. Ve SRP, işlevlerinizde günlüğe kaydetme veya ateşleme olaylarını kullanmanızı yasaklamıyor, yalnızca bu tür bir mantığın diğer sınıflarda kapsüllenmesi gerektiğini ve bir havuzun bu diğer sınıfları çağırmasının uygun olmadığını söylüyor.


22
Sizin retortiniz: "Tamam, peki nasıl yazarsın ki, SRP'yi ihlal etmeyecek, ancak yine de tek bir değişiklik noktasına izin verecek mi ?"
Robert Harvey,

43
Benim gözlemim, bir etkinlik yaratmanın ek bir sorumluluk getirmediğidir. Aslında, tam tersi: sorumluluğu başka bir yere devrediyor.
Robert Harvey,

Sanırım iş arkadaşınız haklı, ancak sorunuz geçerli ve kullanışlı, bu nedenle çok oy verildi!
Andres F.

16
Tek Sorumluluğun kesin bir tanımı gibi bir şey yoktur. SRP'yi ihlal ettiğini belirten kişi, kişisel tanımını kullanarak doğru, tanımınızı kullanarak da haklısınız. Bence tasarımınız, bu olayın bir kereye mahsus olmadığı ve diğer benzer işlevselliklerin farklı şekillerde yapıldığı uyarısı ile tamamen iyi durumda. Tutarlılık, uç noktalara taşınan SRP gibi belirsiz bir kılavuza dikkat etmek için çok, çok daha önemlidir, kimsenin bir sistemde nasıl iş yapılacağını bilmeyen sınıfları anlamak çok kolaylaşır.
Dunk

Yanıtlar:


14

Evet, Addveya gibi belirli eylemlerde belirli olayları tetikleyen bir havuza sahip olmak için geçerli bir gereklilik olabilir SaveChanges- ve sadece bu soruları sormayacağım (diğer cevaplar gibi) çünkü sadece kullanıcı ekleme ve e-posta gönderme örneğinize bakabilirsiniz. biraz daraldı. Aşağıda, bu gereksinimin sisteminiz bağlamında kusursuz bir şekilde doğrulandığını varsayalım.

Bu yüzden evet , olay mekaniğinin yanı sıra günlüğe kaydetmenin yanı sıra bir yönteme kaydetme de SRP'yi ihlal ediyor . Birçok durumda, muhtemelen hiç kimse "değişiklikleri kaydet" ve "etkinliği yükselt" bakım sorumluluklarını farklı ekiplere / bakıcılara dağıtmak istemediğinde muhtemelen kabul edilebilir bir ihlaldir. Fakat bir gün birinin tam olarak bunu yapmak istediğini varsayalım, belki de bu kaygıların kodunu farklı sınıf kütüphanelerine koyarak basit bir şekilde çözülebilir mi?

Bunun çözümü, asıl havuzunuzun veritabanında değişiklik yapmaktan başka bir şey yapmadan sorumlu kalmasını sağlamak ve tamamen aynı ortak arayüze sahip, asıl depoyu yeniden kullanan ve yöntemlere ek olay mekaniği ekleyen bir proxy deposu yapmaktır .

// In EventFiringUserRepo:
public void SaveChanges()
{
  _basicRepo.SaveChanges();
   FireEventsForNewlyAddedUsers();
}

private void FireEventsForNewlyAddedUsers()
{
  foreach (var newUser in _basicRepo.DataContext.GetAddedUsers())
  {
     _eventService.RaiseEvent(new UserCreatedEvent(newUser))
  }
}

Proxy sınıfına a diyebilir NotifyingRepositoryveya ObservableRepositoryisterseniz @ Peter'in yüksek oylu cevabı (gerçekten de SRP ihlalinin nasıl çözüleceğini söylemez, sadece ihlalin iyi olduğunu söyler).

Yeni ve eski depo sınıfı , klasik Proxy modeli açıklamasında gösterildiği gibi ortak bir arayüzden türetilmelidir .

Sonra, orijinal kodunuzda, _userRepositoryyeni EventFiringUserReposınıfın bir nesnesiyle ilklendirin . Bu şekilde, orijinal depoyu etkinlik mekaniğinden ayrı tutarsınız. Gerekirse, olayı kovan olayı ve orijinal depoyu yan yana getirebilir ve arayanların eskisini mi yoksa ikincisini mi kullanacaklarına karar vermelerini sağlayabilirsiniz.

Yorumlarda belirtilen bir endişeye değinmek: bu vekillerin üstünde vekillerin üstünde vekillere yol açmaz mı? Aslında, olay mekaniğinin eklenmesi, yalnızca olaylara abone olarak "e-posta gönder" türünün ek gereksinimlerini eklemek için bir temel oluşturur, bu nedenle SRP'ye bu gereksinimlere ek herhangi bir ek proxy olmadan da bağlı kalın. Fakat buraya bir kez eklenmesi gereken tek şey olay mekaniğinin kendisidir.

Bu tür bir ayrılığa gerçekten değiyorsanız, sisteminiz bağlamında buna değiyorsanız, siz ve hakeminiz karar vermeniz gereken bir şeydir. Muhtemelen günlük kaydını orijinal koddan ayırmam, ne de mümkün olsa da, dinleyiciye bir logger ekleyerek değil, başka bir proxy kullanarak.


3
Bu cevaba ek olarak. AOP gibi vekillere alternatifler var .
Laiv,

1
Bence bu noktayı özlüyorsunuz, bunun bir etkinliği arttırmanın SRP'yi kırmaması değil, yalnızca "Yeni" kullanıcılar için bir etkinlik yaratmasının, reponun "Yeni Bana Eklenenler" yerine "Yeni" bir kullanıcı oluşturduğunu bilmekle sorumlu olmasını gerektirmesidir. "kullanıcı
Ewan

@Ewan: Lütfen soruyu tekrar okuyun. Kodun, o nesnenin sorumluluğu dışındaki diğer eylemlerle birleştirilmesi gereken belirli eylemleri yapan bir yerle ilgiliydi. Eylem ve olayı tek bir yere yerleştirmek, bir hakem değerlendiricisinin SRP'yi kırdığı şeklinde sorgulandı. "Yeni kullanıcıları kaydetme" örneği yalnızca tanıtım amaçlıdır, isterseniz örnek olanı arayın, ancak bu IMHO değil.
Doktor Brown

2
Bu en iyi / doğru cevap IMO. Sadece SRP'yi sağlamakla kalmaz aynı zamanda Açık / Kapalı prensibini de korur. Ve sınıf içindeki değişikliklerin bozabileceği tüm otomatik testleri düşünün. Yeni işlevler eklendiğinde mevcut testleri değiştirmek büyük bir koku.
Keith Payne

1
Devam eden bir işlem varsa bu çözüm nasıl çalışır? Bu devam ederken, SaveChanges()aslında veritabanı kaydını oluşturmaz ve geri alınmasına neden olabilir. AcceptAllChangesTransactionCompleted etkinliğine geçersiz kılmak veya abone olmanız gerekecek gibi görünüyor .
John Wu

29

Kalıcı veri deposunun değiştiğine dair bir bildirim gönderme, kaydederken yapılacak mantıklı bir şey gibi görünüyor.

Elbette, Add'i özel bir durum olarak ele almamalısınız - Değiştirme ve Silme için de etkinlik başlatmalısınız. Koku veren, okuyucuyu neden koktuğunu açıklamaya zorlayan ve sonuçta kodun bazı okuyucularına SRP'yi ihlal etmesi gerektiğine karar veren özel muamele budur.

Değişikliklerle ilgili sorgulanabilen, değiştirilebilen ve olayları tetikleyebilen "bildiren" bir havuz tamamen normal bir nesnedir. Hemen hemen herhangi bir terbiyeli ölçekli projede birden fazla varyasyon bulmayı bekleyebilirsiniz.


Ama aslında "bildiren" bir havuz mu ihtiyacınız var? C # 'dan bahsettiniz: Birçok kişi, ihtiyaç duyduğunuz tek şey System.Collections.ObjectModel.ObservableCollection<>yerine System.Collections.Generic.List<>, her türlü kötü ve yanlış kullanım yerine, çok azının derhal SRP'yi göstereceğini kabul edecekti.

Şu an yaptığın şey seninle UserList _userRepositorybir yerini almak ObservableUserCollection _userRepository. Bu en iyi eylem şekli olup olmamasına göre uygulamaya bağlıdır. Ancak tartışmasız _userRepositoryolarak daha az hafif olmasına rağmen, alçakgönüllü görüşüme göre SRP'yi ihlal etmiyor.


Kullanarak sorun ObservableCollectionbu durum için değil çağrısına de eşdeğer olayı tetikler olduğunu SaveChanges, ancak yapılan çağrı ile Addörnek gösterilen olandan çok farklı bir davranışa yol açacak. Orijinal repoyu nasıl hafif tutabileceğime cevabımı görün ve semantikleri sağlam tutarak hala SRP'ye bağlı kal.
Doktor Brown

@DocBrown; bilinen sınıfları çağrılır ObservableCollection<>ve List<>karşılaştırma ve bağlam için. Asıl sınıfları hem iç uygulama hem de dış arabirim için kullanmanızı tavsiye etmek istememiştim.
Peter,

Tamam, ancak OP, "Değiştir" ve "Sil" e olayları eklese bile (OP'nin, soruyu kısa tutmak için, basitlik adına dışarıda bıraktığını düşünüyorum), bir yorumcunun kolayca sonuçlanabileceğini düşünüyorum. bir SRP ihlali. Muhtemelen kabul edilebilir bir şeydir, ancak gerektiğinde çözülemeyen hiçbiri yoktur.
Doktor Brown

16

Evet, tek sorumluluk ilkesinin ihlali ve geçerli bir nokta.

Daha iyi bir tasarım, depodan 'yeni kullanıcıları' ayrı bir sürecin alıp e-postaları göndermesi olacaktır. Hangi kullanıcıların bir e-posta gönderildiğini, başarısızlıkları, yeniden göndermeleri vb.

Bu şekilde hataları, çökmeleri ve benzerlerini halledebilir, ayrıca "olayların veritabanına işlendiğinde" olayların gerçekleştiği fikrine sahip depo gereksinimlerinizi önleyebilirsiniz.

Depo, eklediğiniz kullanıcının yeni bir kullanıcı olduğunu bilmiyor. Sorumluluğu sadece kullanıcıyı depolamaktır.

Muhtemelen aşağıdaki yorumlarda genişlemeye değer.

Depo, eklediğiniz kullanıcının yeni bir kullanıcı olduğunu bilmiyor - evet öyle, Ekle adında bir yöntemi var. Semantiği, eklenen tüm kullanıcıların yeni kullanıcılar olduğu anlamına gelir. Save'i çağırmadan önce Add'e iletilen tüm argümanları birleştirin - tüm yeni kullanıcıları elde edin

Yanlış. "Depoya Eklendi" ve "Yeni" öğelerini birleştiriyorsunuz.

"Depoya Eklendi" tam olarak ne demek istediğini gösterir. Kullanıcıları çeşitli depolara ekleyebilir ve kaldırabilir ve yeniden ekleyebilirim.

"Yeni", iş kuralları tarafından tanımlanan bir kullanıcının durumudur.

Şu anda iş kuralı "Yeni == depoya yeni eklenmiş" olabilir, ancak bu, bu kuralı bilmenin ve uygulamanın ayrı bir sorumluluk olmadığı anlamına gelmez.

Bu tür veritabanı merkezli düşünmekten kaçınmak için dikkatli olmalısınız. Yeni olmayan kullanıcıları havuza ekleyen son durum işlemlerine sahip olacaksınız ve onlara e-posta gönderdiğinizde tüm işletmeler "Tabii ki bunlar 'yeni' kullanıcılar değil! Asıl kural X'tir" diyecek

Bu cevap IMHO'nun noktasını çok eksik: repo, yeni kullanıcıların ne zaman eklendiğini bilen koddaki tek merkezi yer.

Yanlış. Yukarıdaki nedenlerden ötürü, aslında bir etkinlik düzenlemek yerine sınıfa e-posta gönderme kodunu eklemiyorsanız merkezi bir konum değildir.

Depo sınıfını kullanan, ancak e-postayı gönderme koduna sahip olmayan uygulamalarınız olacaktır. Kullanıcıları bu uygulamalara eklediğinizde, e-posta gönderilmez.


11
Depo, eklediğiniz bir kullanıcının yeni bir kullanıcı olduğunu bilmiyor - evet öyle, adı verilen bir yöntemi var Add. Semantiği, eklenen tüm kullanıcıların yeni kullanıcılar olduğu anlamına gelir. Aramadan Addönce iletilen tüm argümanları birleştirin Save- ve tüm yeni kullanıcıları elde edin.
Andre Borges

Bu öneriyi seviyorum. Ancak, pragmatizm saflık üzerinde hüküm sürmektedir. Koşullara bağlı olarak, mevcut bir uygulamaya tamamen yeni bir mimari katman eklemek, tek yapmanız gereken bir kullanıcı eklendiğinde yapmanız gereken tek bir e-posta göndermektir.
Alexander,

Ancak olay kullanıcı eklediğini söylemiyor. Kullanıcının yarattığı yazıyor. İşleri doğru şekilde adlandırmayı düşünürsek ve add ve create arasındaki anlamsal farklılıklara katılıyorsak, pasajdaki olay ya yanlış adlandırılmış ya da yanlış yerleştirilmiş. Gözden geçiren kişinin notyfing depolarına karşı bir şeyi olduğunu sanmıyorum. Muhtemelen olayın türü ve yan etkileri hakkında endişeliydi.
Laiv,

7
@Andre Repo için yeni, ancak iş anlamında mutlaka "yeni". ilk bakışta fazladan sorumluluk saklayan bu iki fikrin birleşmesidir. Ben yeni deposuna eski kullanıcıların bir ton ithal veya kaldırma ve "yeni kullanıcı" "dB eklendi" ötesinde ne olduğu hakkında iş kuralları olacaktır vb bir kullanıcı yeniden ekleyebiliriz
Ewan

1
Moderatör Notu: Cevabınız gazeteci röportajı değil. Düzenlemeleriniz varsa, tüm "son dakika haberi" etkisini oluşturmadan bunları doğal olarak cevabınıza dahil edin. Tartışma forumu değiliz.
Robert Harvey,

7

Bu geçerli bir nokta mı?

Evet, kodunuzun yapısına çok bağlı olmasına rağmen. Tam bağlamım yok bu yüzden genel olarak konuşmaya çalışacağım.

Bana öyle geliyor ki burada bir olay yaratmanın esas olarak log kaydı yapmakla aynı şey olduğu görülüyor: fonksiyona bazı yan işlevler eklemek.

Kesinlikle değil. Günlük kaydı, iş akışının bir parçası değildir, devre dışı bırakılabilir, (iş) yan etkilerine neden olmamalı ve herhangi bir nedenle giriş yapamamanız durumunda bile uygulamanızın durumunu ve yönünü etkilememelidir. artık bir şey yok. Şimdi bunu eklediğiniz mantıkla karşılaştırın.

Ve SRP, işlevlerinizde günlüğe kaydetme veya ateşleme olaylarını kullanmanızı yasaklamıyor, yalnızca bu tür bir mantığın diğer sınıflarda kapsüllenmesi gerektiğini ve bir havuzun bu diğer sınıfları çağırmasının uygun olmadığını söylüyor.

SRP, ISS (SOLID'de S ve I) ile paralel çalışır. Çok özel şeyler yapan ve başka hiçbir şey yapmayan birçok sınıf ve yöntemle karşılaşıyorsunuz. Çok odaklanmış, güncellenmesi veya değiştirilmesi çok kolay ve genel olarak testi kolay (er). Elbette pratikte, aynı zamanda orkestrasyonla ilgilenen birkaç büyük sınıfa da sahip olacaksınız: çok sayıda bağımlılığa sahip olacaklar ve atomize edilmiş eylemlere değil, birden fazla adım gerektiren iş eylemlerine odaklanacaklar. İş bağlamı açık olduğu sürece, bunlara tek sorumluluk olarak da adlandırılabilir, ancak doğru bir şekilde söylediğiniz gibi, kod büyüdükçe, bir kısmını yeni sınıflara / arayüzlere soyutlamak isteyebilirsiniz.

Şimdi özel örneğe dönelim. Ne zaman bir kullanıcı oluşturulmuşsa ve hatta daha fazla özel eylemler gerçekleştirdiğinde mutlaka bir bildirim yollamanız gerekiyorsa, o zaman her iki depolamayı da kapsayan (bir çağrıyı ele alan, UserCreationServicebir yöntemi açıklayan gibi) , bu gereksinimi içine alan ayrı bir hizmet oluşturabilirsiniz. Add(user)deponuza) ve tek bir ticari işlem olarak bildirim. Veya sonradan orijinal snippet'inizde yapın_userRepository.SaveChanges();


2
Günlük kaydı iş akışının bir parçası değildir - bunun SRP bağlamında nasıl bir önemi vardır? Etkinliğimin amacı Google Analytics’e yeni kullanıcı verileri göndermekse - o zaman devre dışı bırakmak, günlüğe kaydetmeyi devre dışı bırakmakla aynı ticari etkiye sahip olacaktır: kritik değil, ancak oldukça üzücü. Bir işleve yeni mantık eklemek / eklememek için baş parmağın kuralı nedir? “Devre dışı bırakmak önemli iş yan etkilerine neden olur mu?”
Andre Borges

2
If the purpose of my event would be to send new user data to Google Analytics - then disabling it would have the same business effect as disabling logging: not critical, but pretty upsetting . Ya sahte "haber" e neden olan erken olayları başlatıyorsanız. Analizler, DB işlemindeki hatalardan dolayı nihayet yaratılmayan "kullanıcıları" dikkate alıyorsa ne olur? Ya şirket, kesin olmayan verilere dayanarak sahte tesislerde kararlar alıyorsa? Konunun teknik yönüne çok odaklandın. "Bazen ağaçlar için ahşabı göremezsin" "
Laiv

@Laiv, geçerli bir noktaya değiniyorsunuz, ancak bu sorumun ya da bu cevabın noktası değil. Soru, bunun SRP bağlamında geçerli bir çözüm olup olmadığıdır, bu nedenle herhangi bir DB işlem hatası olmadığını varsayalım.
Andre Borges

Temelde benden duymak istediğini söylememi istiyorsun. Sana sadece kapsam veriyorum. SRP'nin önemli olup olmadığına karar vermeniz için daha geniş kapsam, SRP uygun bağlam olmadan işe yaramaz olduğundandır. IMO endişeye yaklaşma şekliniz yanlış çünkü sadece teknik çözüme odaklanıyorsunuz. Bütün içeriğe yeterince ilgi göstermelisin. Ve evet, DB başarısız olabilir. Bunun olması için bir şans var ve bunu ihmal etmemelisiniz, çünkü bildiğiniz gibi, şeyler olur ve bu şeyler SRP veya diğer iyi uygulamalarla ilgili şüpheler konusundaki fikrinizi değiştirebilir.
Laiv,

1
Bununla birlikte, prensiplerin taşla yazılmış kurallar olmadığını unutmayın. Geçirgendirler (uyarlanabilir). Gördüğünüz gibi, yorumlamaya açıklar. Gözden geçiren kişinin bir yorumu var ve bir tane daha var. Gördüğünüzü görmeye çalışın, şüpheleri ve endişeleri giderin ya da kendinizinkileri çözmesine izin verin. Burada "doğru" cevabı bulamazsınız. Doğru cevap, projenin gerekliliklerini (işlevsel ve işlevsel olmayan) ilk olarak soran, siz ve inceleyen kişidir.
Laiv,

4

SRP, teorik olarak, Bob Amca'nın Tek Sorumluluk İlkesi makalesinde açıkladığı gibi insanlar hakkındadır . Yorumunuzda sağladığınız için teşekkürler Robert Harvey.

Doğru soru şudur:

Hangi "paydaş" "e-posta gönder" gereksinimini ekledi?

Eğer bu paydaş aynı zamanda veri kalıcılığından da sorumlu ise (muhtemel fakat mümkün değil), bu SRP'yi ihlal etmez. Aksi takdirde yapar.


2
İlginç - SRP'nin bu yorumunu hiç duymadım. Bu yorum hakkında daha fazla bilgi / literatür hakkında herhangi bir işaret var mı?
Mart’ta sleske

2
@sleske: Bob Amcanın kendisinden : "Ve bu Tek Sorumluluk Prensibi'nin ilkesine yaklaştı . Bu ilke insanlarla ilgilidir. Bir yazılım modülü yazdığınızda, değişikliklerin talep edildiğinde, bu değişikliklerin yalnızca kaynaklanabileceğinden emin olmak istiyorsunuz. tek bir kişiden veya daha doğrusu, tek bir dar tanımlanmış iş işlevini temsil eden sıkı bir şekilde birleştirilmiş bir grup insandan. ”
Robert Harvey,

Sağol Robert. IMO, "Tek Sorumluluk Prensibi" adı basit gibi görünmekle birlikte korkunç, ancak çok az insan "sorumluluk" un amaçlanan anlamını izliyor. OOP'un orijinal kavramlarının çoğundan nasıl değiştiği gibi, ve şimdi de oldukça anlamsız bir terim.
user949300,

1
Evet. REST terimine bu oldu. Roy Fielding bile insanların yanlış kullandığını söylüyor.
Robert Harvey,

Alıntıyla ilgili olsa da, bu cevabın "e-posta gönder" şartının, SRP ihlali sorusunun doğrudan ilgili olduğu şartların hiçbiri olmadığını özlüyorum. Bununla birlikte, "Hangi" paydaş "" olayı yükselt "şartını" eklediğini söyleyerek , bu cevap asıl soruya daha fazla ilişkin olacaktır. Bunu daha net hale getirmek için kendi cevabımı biraz değiştirdim.
Doktor Brown

2

Teknik olarak, olayları bildiren depolarda yanlış olan bir şey olmamasına rağmen, rahatlığının bazı endişeleri ortaya çıkardığı işlevsel bir bakış açısıyla bakmanızı öneririm.

Bir kullanıcı oluşturmak, yeni bir kullanıcının ne olduğuna karar vermek ve kalıcılığı 3 farklı şeydir .

Benim maddi

Deponun ticari olayları bildirmek için uygun bir yer olup olmadığına karar vermeden önce önceki önceliği göz önünde bulundurun (SRP ne olursa olsun). İş olayı demiştim çünkü benim için veya 1'denUserCreated farklı bir çağrışım var . Ayrıca, bu olayların her birinin farklı izleyicilere yönelik olduğunu düşünüyorum.UserStoredUserAdded

Bir tarafta, kullanıcı oluşturmak, sürekliliği olan veya içermeyen işe özgü bir kuraldır. Daha fazla veritabanı / ağ işlemi içeren daha fazla işletme işlemi içerebilir. İşlemler kalıcılık katmanının farkında değil. Kalıcılık katmanı, kullanım durumunun başarıyla bitip bitmediğine karar vermek için yeterli içeriğe sahip değildir.

Kapak tarafında, _dataContext.SaveChanges();kullanıcıyı başarılı bir şekilde devam ettiren mutlaka doğru değildir . Veritabanının işlem süresine bağlı olacaktır. Örneğin, işlemlerin atomik olduğu MongoDB gibi veritabanları için doğru olabilir, ancak daha fazla işlemin yapılabileceği ve henüz gerçekleştirilemeyeceği ACID işlemlerini uygulayan geleneksel RDBMS için bu doğru olamazdı.

Bu geçerli bir nokta mı?

Olabilir. Ancak, bunun sadece bir SRP meselesi değil (teknik olarak konuşur), aynı zamanda bir kolaylık meselesi (işlevsel olarak konuşur) olduğunu söylemeye cüret ediyorum.

  • İşle ilgili olayları, devam etmekte olan ticari faaliyetlerden habersiz bileşenlerden başlatmak uygun mudur?
  • Doğru yeri, yapılacak en kısa sürede temsil ediyor mu?
  • Bu bileşenlerin iş mantığımı bu gibi bildirimlerle düzenlemesine izin vermeli miyim?
  • Erken olayların neden olduğu yan etkileri geçersiz kılabilir miyim? 2

Bana öyle geliyor ki, burada bir olay yaratmanın esas olarak log kaydıyla aynı şey olduğu görülüyor.

Kesinlikle hayır. Günlüğe kaydetmenin hiçbir yan etkisi olmamakla birlikte, etkinliğin UserCreatedbaşka iş operasyonlarının gerçekleşmesine neden olabileceğini öne sürdüğünüz gibi . Bildirimler gibi. 3

sadece bu tür bir mantığın diğer sınıflarda kapsüllenmesi gerektiğini ve bir havuzun bu diğer sınıfları çağırmasının uygun olmadığını söylüyor.

Mutlaka doğru değil. SRP sadece sınıfa özgü bir endişe değildir. Katmanlar, kütüphaneler ve sistemler gibi farklı soyutlama seviyelerinde çalışır! Aynı paydaşların eliyle aynı nedenlerle nelerin değiştiğini bir arada tutmak, uyum ile ilgilidir . Kullanıcı oluşturma ( kullanım senaryosu ) değişirse, an ve olayın gerçekleşmesinin sebepleri de büyük olasılıkla değişir.


1: İşleri yeterince isimlendirmek de önemlidir.

2: UserCreatedSonra gönderdiğimizi söyleyin _dataContext.SaveChanges();, ancak daha sonra bağlantı sorunları veya kısıtlamalar ihlalleri nedeniyle veritabanı işleminin tamamı başarısız oldu. Olayların erken yayınlanmasına dikkat edin, çünkü yan etkilerini geri almak zor olabilir (eğer mümkünse).

3: Uygun şekilde işlem görmeyen bildirim işlemleri, geri alınamayan / desteklenemeyen bildirimleri başlatmanıza neden olabilir>


1
+1 İşlem süresi hakkında çok iyi bir nokta. Kullanıcının oluşturulduğunu iddia etmek erken olabilir, çünkü geri dönüşler olabilir; ve bir günlükten farklı olarak, uygulamanın başka bir bölümünün olayla ilgili bir şeyler yapması olasıdır.
Andres F.

2
Kesinlikle. Olaylar kesinliği ifade eder. Bir şeyler oldu ama bitti.
Laiv

1
@Laiv: Yapmadıkları zamanlar hariç. Microsoft, önceden belirlenmiş Beforeveya Previewkesinliği garanti etmeyen her türlü olayı içerir .
Robert Harvey,

1
@ jpmc26: Bir alternatif olmadan öneriniz yardımcı değil.
Robert Harvey,

1
@ jpmc26: Yani cevabınız "tamamen farklı bir araç seti ve performans özellikleri olan tamamen farklı bir geliştirme ekosistemine geçmek." Bana aksini söyleyin, ancak bunun geliştirme çabalarının büyük çoğunluğu için mümkün olmadığını hayal ediyorum.
Robert Harvey,

1

Hayır, bu SRP'yi ihlal etmez.

Birçoğunun, Tek Sorumluluk İlkesi'nin, bir işlevin yalnızca "bir şey" yapması ve ardından "bir şeyi" neyin teşkil ettiği konusundaki tartışmalara kapılması gerektiği anlamına geldiğini düşündüğü görülmektedir.

Ancak bu prensibi kastetmiyor. İş düzeyinde endişeler hakkında. Bir sınıf, işletme düzeyinde bağımsız olarak değişebilecek birden fazla kaygı veya gerekliliği yerine getirmemelidir. Diyelim ki bir sınıf hem kullanıcıyı saklar hem de e-posta yoluyla kodlanmış bir karşılama mesajı gönderir. Birden fazla bağımsız kaygı, böyle bir sınıfın gereksinimlerinin değişmesine neden olabilir. Tasarımcı, postanın html / stil sayfasını değiştirmesini gerektirebilir. İletişim uzmanı, postanın ifadesinin değişmesini gerektirebilir. Ve UX uzmanı, postanın onboarding akışında farklı bir noktaya gönderilmesi gerektiğine karar verebilir. Bu yüzden sınıf, bağımsız kaynaklardan gelen birçok gereksinim değişikliğine tabidir. Bu, SRP'yi ihlal ediyor.

Ancak bir olayı başlatmak, SRP'yi ihlal etmez, çünkü etkinlik yalnızca kullanıcıyı kurtarmaya ve herhangi bir endişeye bağlı olmamasına bağlıdır. Olaylar aslında SRP'yi korumanın gerçekten güzel bir yoludur, çünkü depoyu postadan etkilenmeden - hatta e-postadan haberdar olmadan - kaydetme tarafından tetiklenen bir e-postaya sahip olabilirsiniz.


1

Tek sorumluluk ilkesi hakkında endişelenme. Burada iyi bir karar vermenize yardımcı olmayacaktır, çünkü sübjektif bir kavramı “sorumluluk” olarak seçebilirsiniz. Sınıfın sorumluluğunun veritabanında veri kalıcılığını yönetmek olduğunu söyleyebilir ya da sorumluluğunun bir kullanıcı oluşturmakla ilgili tüm işleri yapmak olduğunu söyleyebilirsin. Bunlar, uygulamanın davranışının sadece farklı seviyeleridir ve her ikisi de "tek bir sorumluluğun" geçerli kavramsal ifadeleridir. Yani bu ilke, probleminizi çözmede yararsızdır.

Bu durumda uygulanacak en faydalı ilke, en az sürpriz ilkesidir . Öyleyse soruyu soralım: Sürdürülen verinin birincil rolü olan bir veri tabanına sahip bir havuzun da e-posta göndermesi şaşırtıcı mıdır?

Evet, çok şaşırtıcı. Bunlar birbirinden tamamen ayrı iki harici sistemdir ve ad SaveChangesaynı zamanda bildirim gönderme anlamına da gelmez. Bunu bir olaya devretme gerçeği, davranışı daha da şaşırtıcı kılar, çünkü kodu okuyan bir kişi artık hangi ek davranışların çağrıldığını kolayca göremez. Dolaylı okunabilirliğe zarar verir. Bazen, faydalar okunabilirlik maliyetlerine değer olabilir, ancak otomatik olarak son kullanıcılar için gözlemlenebilir etkileri olan ek bir harici sistemi çalıştırdığınızda. (Etki, hata ayıklama amacıyla temelde kayıt tutma olduğundan, kayıt işlemi burada hariç tutulabilir. Son kullanıcılar günlüğü kullanmaz, bu nedenle her zaman günlüğe kaydetmenin zararı olmaz.) Daha da kötüsü, zamanlamadaki esnekliği azaltır e-postanın gönderilmesi, kaydetme ve bildirim arasındaki diğer işlemlerin birleştirilmesini olanaksız kılar.

Kodunuzun tipik olarak bir kullanıcı başarıyla oluşturulduğunda bir bildirim göndermesi gerekiyorsa, aşağıdakileri yapan bir yöntem oluşturabilirsiniz:

public void AddUserAndNotify(IUserRepository repo, IEmailNotification notifier, MyUser user)
{
    repo.Add(user);
    repo.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Ancak bunun değer katıp katmadığı, uygulamanızın özelliklerine bağlıdır.


Aslında SaveChangesyöntemin varlığından vazgeçirirdim . Bu yöntem muhtemelen bir veritabanı işlemi gerçekleştirecektir, ancak diğer havuzlar veritabanını aynı işlemde değiştirmiş olabilir . Hepsini işlediği gerçeği yine şaşırtıcıdır, çünkü SaveChangeskullanıcı havuzunun bu örneğine özel olarak bağlıdır.

Bir veritabanı işlemini yönetmek için en basit desen bir dış usingbloktur:

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    context.SaveChanges();
    notifier.SendUserCreatedNotification(user);
}

Bu, programcıya, tüm depolar için yapılan değişiklikler kaydedildiğinde açık bir kontrol sağlar , kodu bir taahhütten önce yapılması gereken olayların sırasını açıkça belgelendirmeye zorlar, bir geri dönüşün hatayla yapılmasını sağlar (bir geri dönüş yayınladığını varsayar DataContext.Dispose) ve gizlenmeyi önler durum bilgisi olan sınıflar arasındaki bağlantılar.

Ayrıca isteği doğrudan e-postayla göndermemeyi tercih ederim. Kuyruktaki bir bildirime duyulan ihtiyacı kaydetmek daha sağlam olurdu . Bu, daha iyi arıza yönetimi için izin verir. Özellikle, e-posta gönderilirken bir hata oluşursa, daha sonra kullanıcıyı kaydetmeyi kesmeden tekrar denenebilir ve kullanıcının yaratıldığı durumdan kaçınır ancak bir hata site tarafından döndürülür.

using (DataContext context = new DataContext())
{
    _userRepository.Add(context, user);
    _emailNotificationQueue.AddUserCreateNotification(user);
    _emailNotificationQueue.Commit();
    context.SaveChanges();
}

Önce bildirim kuyruğunu işlemek daha iyidir, çünkü kuyruğun tüketicisi, context.SaveChanges()çağrı başarısız olursa , kullanıcının e-postayı göndermeden önce var olduğunu doğrulayabilir . (Aksi takdirde, heisenbug'ları önlemek için tamamen gelişmiş iki aşamalı bir taahhüt stratejisine ihtiyacınız olacak.)


Alt satırda pratik olmaktır. Aslında kod yazmanın sonuçları (hem risk hem de fayda açısından) belirli bir şekilde düşünün. "Tek sorumluluk ilkesi" nin bunu yapmamda pek yardımcı olmadığını, "en az sürpriz ilkesi" de sık sık başka bir geliştiricinin kafasına girmeme (konuşmak için) ve ne olabileceğini düşünmeme yardımcı olduğunu biliyorum.


4
Verilerin birincil rolü olan bir veritabanının veri tabanına gönderilmesinin şaşırtıcı olması da e-postalar göndermesidir - sanırım sorumun noktasını kaçırdınız. Depom e-posta göndermiyor. Sadece bir etkinlik düzenler ve bu olayın nasıl işlendiği - dış kodun sorumluluğundadır.
Andre Borges

4
Temel olarak "olayları kullanma" argümanını yapıyorsunuz.
Robert Harvey,

3
[shrug] Olaylar çoğu kullanıcı arabirimi çerçevesinin merkezindedir. Olayları ortadan kaldırın ve bu çerçeveler hiç çalışmıyor.
Robert Harvey,

2
@ jpmc26: Buna ASP.NET Webforms adı verilir. Berbat.
Robert Harvey,

2
My repository is not sending emails. It just raises an eventsebep-sonuç. Havuz bildirim sürecini tetikliyor.
Laiv,

0

Şu anda iki şey SaveChangesyapar : değişiklikleri kaydeder ve yaptığı günlüğe kaydeder. Şimdi ona başka bir şey eklemek istiyorsun: e-posta bildirimleri gönder.

Buna bir etkinlik ekleme konusunda zekice bir fikriniz vardı, ancak bu, Tek Halli Sorumluluk İlkesini (SRP) ihlal ettiği için zaten ihlal edildiğini fark etmeden eleştirildi.

Saf bir SRP çözümü elde etmek için önce etkinliği tetikleyin, ardından şu anda üçü olan o olaya ilişkin tüm kancaları çağırın: kaydetme, günlüğe kaydetme ve son olarak e-posta gönderme.

Ya önce olayı tetiklersiniz ya da eklemeniz gerekir SaveChanges. Çözümünüz, ikisi arasında bir melez. Mevcut ihlali ele almazken, üç şeyin ötesine geçmesini önlemeyi teşvik eder. Mevcut kodu SRP'ye uygun şekilde yeniden düzenlemek, kesinlikle gerekenden daha fazla çalışma gerektirebilir. SRP'yi ne kadar uzağa götürmek istedikleri projenize kalmış.


-1

Kod zaten SRP'yi ihlal etti - aynı sınıf veri bağlamı ile iletişim kurmak ve kayıt olmaktan sorumluydu.

Siz sadece 3 sorumluluğu yerine getirin.

Olayları 1 sorumluluğa geri çekmenin bir yolu, soyutlamayı _userRepository; komut yayıncısı yap.

Bir takım komutlar ve bir dizi dinleyici var. Komut alır ve onları dinleyicilerine yayınlar. Muhtemelen bu dinleyicilere emir verilir ve belki de komutun başarısız olduğunu bile söyleyebilirler (ki bu daha önce bildirilmiş dinleyicilere yayınlanır).

Şimdi, komutların çoğunda yalnızca 1 dinleyici olabilir (veri içeriği). Değişikliklerinizden önce SaveChanges 2 içeriyor: veri bağlamı ve ardından kaydedici.

Değişikliğiniz daha sonra değişiklikleri kaydetmek için başka bir dinleyici ekler, bu da etkinlik hizmetinde yeni kullanıcı tarafından oluşturulan etkinlikleri arttırır.

Bunun bir kaç faydası var. Artık, kodunuzun geri kalan kısmını bakmadan günlük kodunu kaldırabilir, yükseltebilir veya çoğaltabilirsiniz. İhtiyaç duyduğunuz şeyler için kaydetme değişikliklerine daha fazla tetikleyici ekleyebilirsiniz.

Tüm bunlar, ne zaman _userRepositoryoluşturulduğunda ve kablolandığında karar verir (ya da belki bu ek özellikler anında eklenir / kaldırılır; uygulama çalışırken günlük kaydı ekleyebilir / geliştirebilir).

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.