SRP, belirsiz bir ifadeyle, bir sınıfın değişmek için sadece bir sebebi olması gerektiğini belirtir.
Sorudaki "rapor" sınıfının yeniden yapılandırılmasının üç yöntemi vardır:
printReport
getReportData
formatReport
Report
Her yöntemde kullanılan fazlalığı göz ardı ederek , bunun neden SRP'yi ihlal ettiğini görmek kolaydır:
"Yazdır" terimi bir tür kullanıcı arayüzü veya gerçek bir yazıcı anlamına gelir. Dolayısıyla bu sınıf bir miktar kullanıcı arayüzü veya sunum mantığı içerir. Kullanıcı arayüzü gereksinimlerindeki bir değişiklik, Report
sınıfta bir değişiklik yapılmasını gerektirecektir .
"Veri" terimi bir tür veri yapısını ifade eder, fakat gerçekten ne olduğunu (XML? JSON? CSV?) Belirtmez. Ne olursa olsun, raporun "içeriği" hiç değişmezse, bu yöntem de değişecektir. Bir veritabanına veya etki alanına bağlantı var.
formatReport
genel olarak bir yöntem için korkunç bir isim, ancak bir kez daha UI ile ilgili bir şey olduğunu ve muhtemelen UI'nin farklı bir yönüne sahip olduğunu düşünerek varsayıyorum printReport
. Yani, başka, ilgisiz bir değişiklik nedeni.
Yani bu bir sınıf muhtemelen bir veritabanı, bir ekran / yazıcı aygıtı ve günlükler veya dosya çıktısı ya da ne için dahili biçimlendirme mantığı ile birleştirilir. Bir sınıftaki her üç işlevin de bulunmasıyla, bağımlılık sayısını çoğaltırsınız ve herhangi bir bağımlılık veya gereksinim değişikliğinin bu sınıfı (veya buna bağlı başka bir şeyi) kırma olasılığını üç katına çıkarırsınız.
Buradaki sorunun bir kısmı, özellikle dikenli bir örnek seçmiş olmanızdır. Muhtemelen Report
sadece bir şey yapsa bile denilen bir sınıfınız olmamalı , çünkü ... hangi rapor? Tüm "raporlar", farklı verilere ve farklı gereksinimlere dayalı olarak tamamen farklı hayvanlar değil mi? Ve rapor , ekran veya yazdırma için zaten biçimlendirilmiş bir şey değil mi?
Ancak, geçmişe bakmak ve varsayımsal bir somut ad oluşturmak - buna IncomeStatement
(çok yaygın bir rapor) diyelim - uygun bir "SRPed" mimarisinin üç tipi olacaktır:
IncomeStatement
- biçimlendirilmiş raporlarda görünen bilgileri içeren ve / veya hesaplayan alan ve / veya model sınıfı .
IncomeStatementPrinter
gibi bir standart arayüz uygular IPrintable<T>
. Bir anahtar yönteme Print(IncomeStatement)
ve belki de baskıya özgü ayarları yapılandırmak için başka yöntemlere veya özelliklere sahiptir.
IncomeStatementRenderer
, ekran görüntülemeyi işler ve yazıcı sınıfına çok benzer.
Sonunda IncomeStatementExporter
/ gibi daha fazla özelliğe özgü sınıf da ekleyebilirsiniz IExportable<TReport, TFormat>
.
Bu, jenerikler ve IoC kaplarının tanıtımı ile modern dillerde önemli ölçüde kolaylaştırılmıştır. Uygulama kodunuzun çoğunun belirli IncomeStatementPrinter
sınıfa dayanması gerekmez IPrintable<T>
, herhangi bir yazdırılabilir rapor kullanabilir ve bu şekilde çalışabilir , bu da size Report
bir print
yöntemle bir temel sınıfın algılanan tüm avantajlarını verir ve her zamanki SRP ihlallerinden hiçbirini vermez . Gerçek uygulamanın, IoC konteynır kaydında yalnızca bir kez bildirilmesi gerekir.
Bazı insanlar, yukarıdaki tasarımla karşı karşıya kaldıklarında, şöyle bir şeyle yanıt verirler: "ancak bu prosedürel koda benziyor ve OOP'un tamamı, bizi veri ve davranışın ayrılmasından uzaklaştırmaktı!" Dediğim: yanlış .
IncomeStatement
Olduğu değil sadece "veri" şeklinde ve yine hata ki içine ilgisiz işlevselliği her türlü parazit başlamak sonradan böyle bir "saydam" sınıfı yaratarak bir şey yanlış yapıyorsun ve hissetmek cepten millet bir sürü sebep budur IncomeStatement
ki (iyi ve genel tembellik). Bu sınıf sadece veri olarak başlayabilir , ancak zamanla garantili olarak daha fazla model olarak ortaya çıkacaktır .
Örneğin, gerçek gelir tablosunda toplam gelirler , toplam giderler ve net gelir çizgileri bulunur. Düzgün tasarlanmış bir finansal sistem büyük olasılıkla bunları depolamayacaktır çünkü bunlar işlemsel veriler değildir - aslında, yeni işlem verilerinin eklenmesine bağlı olarak değişirler. Ancak, raporu yazdırıyor, oluşturuyor veya dışa aktarıyor olsanız da, bu satırların hesaplanması her zaman tam olarak aynı olacaktır. Senin Yani IncomeStatement
sınıf şeklinde kendisine davranışın adil bir miktarına sahip olacak getTotalRevenues()
, getTotalExpenses()
ve getNetIncome()
yöntemlerle ve muhtemelen birkaç diğerleri. Gerçekten fazla bir şey yapmıyor gibi görünse bile, kendi davranışı olan gerçek bir OOP tarzı nesnedir.
Ancak format
ve print
yöntemleri, bilginin kendisiyle hiçbir ilgisi yoktur. Aslında, bu yöntemlerle ilgili birkaç uygulama yapmak istemeniz pek olası değildir , örneğin yönetim için ayrıntılı bir açıklama ve hissedarlar için o kadar ayrıntılı olmayan bir açıklama. Bu bağımsız işlevleri farklı sınıflara ayırmak, tek bir boyuta uyan tüm print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
yöntemin yükü olmadan çalışma zamanında farklı uygulamaları seçebilmenizi sağlar . Yuck!
Umarım yukarıdaki, büyük ölçüde parametrelendirilmiş yöntemin nerede yanlış gittiğini ve ayrı uygulamaların nerede doğru gittiğini görebilirsiniz; tek nesne durumunda, yazdırma mantığına her yeni kırışıklık eklediğinizde, etki alanı modelinizi değiştirmeniz gerekir ( Finanstaki Tim sayfa numaralarını ister, ancak yalnızca dahili raporda, bunu ekleyebilir misiniz? ) bunun yerine bir veya iki uydu sınıfına bir yapılandırma özelliği eklemek yeterlidir.
SRP'yi doğru şekilde uygulamak bağımlılıkları yönetmekle ilgilidir . Bir sınıf zaten yararlı bir şeyler yapar ve (bir UI, yazıcı bir ağ, bir dosya, ne olursa olsun gibi) yeni bir bağımlılık getirecek başka bir yöntemi ekleyerek düşünüyorsanız Özetle, yok . Bunun yerine bu işlevselliği yeni bir sınıfa nasıl ekleyebileceğinizi ve bu yeni sınıfı genel mimarinize nasıl sığdırabileceğinizi düşünün (bağımlılık enjeksiyonunu tasarlarken oldukça kolaydır). Genel ilke / süreç budur.
Yan not: Robert gibi, SRP uyumlu bir sınıfın yalnızca bir veya iki durum değişkenine sahip olması gerektiği fikrini açıkça reddediyorum. Böyle ince bir ambalajın nadiren gerçekten yararlı bir şey yapması beklenebilir. Yani bununla aşırıya kaçmayın.