Derslerimi çok ayrıntılı mı yapıyorum? Tek Sorumluluk İlkesi nasıl uygulanmalıdır?


9

Üç temel adım içeren bir sürü kod yazıyorum.

  1. Bir yerden veri alın.
  2. Bu verileri dönüştürün.
  3. Bu verileri bir yere koyun.

Tipik olarak, kendi tasarım modellerinden esinlenerek üç tip sınıf kullanıyorum.

  1. Fabrikalar - bazı kaynaklardan nesne oluşturmak için.
  2. Arabulucular - fabrikayı kullanmak, dönüşümü gerçekleştirmek, sonra komutanı kullanmak.
  3. Komutanlar - bu verileri başka bir yere koymak için.

Sınıflarım bana oldukça küçük olma eğilimindedir, genellikle tek bir (genel) yöntemdir, örneğin veri alma, veri dönüştürme, iş yapma, veri kaydetme. Bu, sınıfların çoğalmasına yol açar, ancak genel olarak iyi çalışır.

Mücadele ettiğim yerde teste geldiğimde, sonunda sıkı sıkıya bağlı testler yapacağım. Örneğin;

  • Fabrika - dosyaları diskten okur.
  • Komutan - dosyaları diske yazar.

Birini diğeri olmadan test edemiyorum. Ben de disk okuma / yazma yapmak için ek 'test' kodu yazabilirsiniz, ama sonra kendimi tekrar ediyorum.

.Net'e bakıldığında, File sınıfı farklı bir yaklaşım benimser, fabrikamın ve komutanımın sorumluluklarını birleştirir. Tek bir yerde Oluştur, Sil, Var ve Oku işlevlerine sahiptir.

.Net örneğini takip etmeli ve - özellikle dış kaynaklarla uğraşırken - derslerimi birleştirmeli miyim? Kod hala eşleşti, ancak daha kasıtlı - testlerden ziyade orijinal uygulamada gerçekleşir.

Tek Sorumluluk İlkesini biraz aşırı derecede uyguladığım sorunum burada mı? Okuma ve yazmadan sorumlu ayrı sınıflarım var. Sistem diski gibi belirli bir kaynakla ilgilenmekten sorumlu birleşik bir sınıfım olduğunda.



6
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.- "Sorumluluk" u "yapılacak şey" ile sınırlandırdığınızı unutmayın. Sorumluluk daha çok “endişe alanı” na benzer. File sınıfının sorumluluğu dosya işlemlerini gerçekleştirmektir.
Robert Harvey

1
Bana göre iyi durumdasın. Tek ihtiyacınız olan bir test aracısı (veya daha iyi isterseniz her dönüşüm türü için bir tane). Test aracısı, .net'in File sınıfını kullanarak doğruluğunu doğrulamak için dosyaları okuyabilir. Bunun bir SOLID perspektifinden hiçbir sorunu yoktur.
Martin Maat

1
@Robert Harvey tarafından belirtildiği gibi, SRP'nin asıl bir adı var çünkü gerçekten Sorumluluklarla ilgili değil. "Değişebilecek tek bir zor / zor endişe alanını kuşatmak ve soyutlamak" ile ilgilidir. Sanırım STDACMC çok uzundu. :-) Yani, üç parçaya bölünmenizin makul göründüğünü söyledi.
user949300

1
FileKütüphanenizdeki C # 'dan önemli bir nokta , Filesınıfın sadece bir cephe olabileceğini bildiğimiz için , tüm dosya işlemlerini tek bir yere - sınıfa yerleştirmek, ancak dahili olarak kendinize benzer bir okuma / yazma sınıfları kullanmak olabilir. aslında dosya işleme için daha karmaşık mantığı içerir. Böyle bir sınıf (the File) hala SRP'ye bağlı kalacaktır, çünkü dosya sistemiyle gerçekten çalışma süreci büyük olasılıkla birleştirici bir arayüzle başka bir katmanın arkasına çekilecektir. Öyle olduğunu söylememek, ama olabilir. :)
Andy

Yanıtlar:


5

Tek Sorumluluk prensibine uymak, sizi burada yönlendiren, ancak bulunduğunuz yerin farklı bir adı olabilir.

Komut Sorgusu Sorumluluk Ayrımı

Bunu inceleyin ve bence bunu tanıdık bir kalıptan sonra bulacaksınız ve bunu ne kadar ileri götüreceğinizi merak etmede yalnız değilsiniz. Asit testi, bunu takip etmek size gerçek faydalar sağlıyorsa veya takip ettiğiniz kör bir mantra ise, düşünmenize gerek yoktur.

Test konusunda endişelerinizi ifade ettiniz. CQRS'yi takip etmek test edilebilir kod yazmayı engellemiyor sanmıyorum. Kodunuzu test edilemez hale getirecek şekilde CQRS'yi takip ediyor olabilirsiniz.

Kontrol akışını değiştirmeye gerek kalmadan kaynak kodu bağımlılıklarını tersine çevirmek için polimorfizmin nasıl kullanılacağını bilmek yardımcı olur. Beceri setinizin test yazma konusunda nerede olduğundan emin değilim.

Dikkatli olun, kütüphanelerde bulduğunuz alışkanlıkları takip etmek uygun değildir. Kütüphanelerin kendi ihtiyaçları vardır ve açıkçası eskidir. Yani en iyi örnek bile o zamanın en iyi örneğidir.

Bu, CQRS'yi takip etmeyen mükemmel geçerli örnekler olmadığı anlamına gelmez. Bunu takip etmek her zaman biraz acı olacaktır. Her zaman ödemeye değer biri değildir. Ama ihtiyacınız varsa onu kullandığınız için mutlu olacaksınız.

Kullanırsanız şu uyarı kelimesine dikkat edin:

Özellikle CQRS, bir bütün olarak sistemde değil, sadece bir sistemin belirli bölümlerinde (DDD lingo'da bir BoundedContext) kullanılmalıdır. Bu düşünme biçiminde, her Sınırlı Bağlamın nasıl modellenmesi gerektiğine ilişkin kendi kararlarına ihtiyacı vardır.

Martin Flowler: CQRS


İlginç daha önce CQRS görülmedi. Kod test edilebilir, bu daha iyi bir yol bulmaya çalışmakla ilgilidir. Yapabildiğim zaman alay ve bağımlılık enjeksiyonu kullanıyorum.
James Wood

Bu konuda ilk kez okuduğumda, uygulamam aracılığıyla benzer bir şey belirledim: esnek aramaları, filtrelenebilir / sıralanabilir, (Java / JPA) bir baş ağrısıdır ve temel bir arama motoru oluşturmadıkça bir ton kazan plakası koduna yol açarsınız. Bu şeyleri sizin için halledeceğim (rsql-jpa kullanıyorum). Aynı modele sahip olmama rağmen (her ikisi için de aynı JPA Varlıkları diyelim), aramalar özel bir genel hizmette çıkarılır ve model katmanı artık bunu işlemek zorunda değildir.
Walfrat

3

Kodun Tek Sorumluluk İlkesine uygun olup olmadığını belirlemek için daha geniş bir perspektife ihtiyacınız vardır. Sadece kodun kendisi analiz edilerek cevaplanamaz, gelecekte hangi güçlerin veya aktörlerin gereksinimlerin değişmesine neden olabileceğini düşünmelisiniz.

Diyelim ki uygulama verilerini bir XML dosyasında saklıyorsunuz. Hangi faktörler okuma veya yazma ile ilgili kodu değiştirmenize neden olabilir? Bazı olasılıklar:

  • Uygulamaya yeni özellikler eklendiğinde uygulama veri modeli değişebilir.
  • Modele yeni tür veriler - örn. Görüntüler - eklenebilir
  • Depolama biçimi, uygulama mantığından bağımsız olarak değiştirilebilir: Birlikte çalışabilirlik veya performans endişeleri nedeniyle XML'den JSON'a veya ikili biçime söyleyin.

Tüm bu durumlarda, hem okuma hem de yazma mantığını değiştirmeniz gerekecektir . Başka bir deyişle, bunlar ayrı sorumluluklar değildir .

Ancak farklı bir senaryo düşünelim: Uygulamanız bir veri işleme hattının parçasıdır. Ayrı bir sistem tarafından oluşturulan bazı CSV dosyalarını okur, bazı analiz ve işlemleri gerçekleştirir ve daha sonra üçüncü bir sistem tarafından işlenecek farklı bir dosya çıkarır. Bu durumda okuma ve yazma bağımsız sorumluluklardır ve ayrıştırılmalıdır.

Alt satır: Genel olarak, okuma ve yazma dosyalarının ayrı sorumluluklar olup olmadığını söyleyemezsiniz, bu uygulamadaki rollere bağlıdır. Ancak test konusundaki ipucunuza dayanarak, sizin durumunuzda tek bir sorumluluk olduğunu tahmin ediyorum.


2

Genel olarak doğru fikre sahipsiniz.

Bir yerden veri alın. Bu verileri dönüştürün. Bu verileri bir yere koyun.

Üç sorumluluğunuz var gibi görünüyor. IMO "Arabulucu" çok şey yapıyor olabilir. Sanırım üç sorumluluğunuzu modelleyerek başlamalısınız:

interface Reader[T] {
    def read(): T
}

interface Transformer[T, U] {
    def transform(t: T): U
}

interface Writer[T] {
    def write(t: T): void
}

Daha sonra bir program şu şekilde ifade edilebilir:

def program[T, U](reader: Reader[T], 
                  transformer: Transformer[T, U], 
                  writer: Writer[U]): void =
    writer.write(transformer.transform(reader.read()))

Bu sınıfların çoğalmasına yol açar

Bunun bir sorun olduğunu düşünmüyorum. IMO birçok küçük uyumlu, test edilebilir sınıf, büyük, daha az uyumlu sınıflardan daha iyidir.

Mücadele ettiğim yerde teste geldiğimde, sonunda sıkı sıkıya bağlı testler yapacağım. Birini diğeri olmadan test edemiyorum.

Her parça bağımsız olarak test edilebilir olmalıdır. Yukarıda modellenmiş olarak, bir dosyaya okuma / yazma işlemini şu şekilde temsil edebilirsiniz:

class FileReader(fileName: String) implements Reader[String] {
    override read(): String = // read file into string
}

class FileWriter(fileName: String) implements Writer[String] {
    override write(str: String) = // write str to file
}

Bu sınıfları dosya sisteminde okuduklarını ve yazdıklarını doğrulamak için sınama için tümleştirme sınamaları yazabilirsiniz. Mantığın geri kalanı dönüşüm olarak yazılabilir. Örneğin, dosyalar JSON biçimindeyse, Strings'yi dönüştürebilirsiniz .

class JsonParser implements Transformer[String, Json] {
    override transform(str: String): Json = // parse as json
}

Ardından uygun nesnelere dönüştürebilirsiniz:

class FooParser implements Transformer[Json, Foo] {
    override transform(json: Json): Foo = // ...
}

Bunların her biri bağımsız olarak test edilebilir. Sen birim test da yapabilirsiniz programalay yukarıda reader, transformerve writer.


Şu an bulunduğum yer burası. Her işlevi ayrı ayrı test edebilirim, ancak bunları test ederek birleşirler. Örneğin, FileWriter'ın test edilmesi için, başka bir şeyin yazılanları okuması gerekir, bariz çözüm FileReader'ı kullanmaktır. Fwiw, arabulucu genellikle iş mantığı uygulamak gibi bir şey yapar veya belki de temel uygulama Ana işlevi tarafından temsil edilir.
James Wood

1
@JamesWood entegrasyon testlerinde genellikle durum böyle. Sen yok olması ancak testinde çift sınıfları. Kullanmak FileWriteryerine doğrudan dosya sisteminden okuyarak test edebilirsiniz FileReader. Hedeflerinizin sınanması gerçekten size kalmış. Kullanırsanız FileReader, testlerden biri ya FileReaderda bozuksa FileWriterkırılır - bu hata ayıklamak daha uzun sürebilir.
Samuel

Ayrıca bkz. Stackoverflow.com/questions/1087351/… testlerinizin daha güzel olmasına yardımcı olabilir
Samuel

Şu anda olduğum yer neredeyse bu - bu% 100 doğru değil. Arabulucu desenini kullandığınızı söylediniz. Bunun burada yararlı olmadığını düşünüyorum; bu desen birbiriyle çok karışık bir akışta etkileşime giren çok sayıda farklı nesneniz olduğunda kullanılır; tüm ilişkileri kolaylaştırmak ve bunları tek bir yerde uygulamak için bir arabulucu koyarsınız. Bu sizin durumunuz gibi görünmüyor; çok iyi tanımlanmış küçük birimleriniz var. Ayrıca, @Samuel'in yukarıdaki yorumu gibi, bir üniteyi test etmeli ve diğer üniteleri çağırmadan önerilerinizi yapmalısınız
Emerson Cardoso

@EmersonCardoso; Sorumdaki senaryoyu biraz basitleştirdim. Bazı aracılarım oldukça basitken, diğerleri daha karmaşıktır ve genellikle birden fazla fabrika / komutan kullanır. Tek bir senaryonun detaylarından kaçınmaya çalışıyorum, daha çok çoklu senaryolara uygulanabilen daha üst seviye tasarım mimarisiyle ilgileniyorum.
James Wood

2

Sonunda sıkı sıkıya bağlı testler yapacağım. Örneğin;

  • Fabrika - dosyaları diskten okur.
  • Komutan - dosyaları diske yazar.

Yani buradaki odak , onları birleştiren şey . İkisi arasında bir nesne Filemi geçiriyorsunuz (örneğin ?) O zaman birbirleriyle değil, eşleştirildikleri Dosyadır.

Söylediklerinizden derslerinizi ayırdınız. Tuzak, onları birlikte test ediyor olmanızdır çünkü daha kolay veya 'mantıklı' .

CommanderDiskten gelmek için neden girdiye ihtiyacınız var ? Tek umursadığı şey belirli bir giriş kullanarak yazmaktır, daha sonra testte bulunanı kullanarak dosyayı doğru yazdığını doğrulayabilirsiniz .

Test ettiğiniz asıl kısım Factory'bu dosyayı doğru bir şekilde okuyacak ve doğru olanı çıkaracak mı?' Bu yüzden testte okumadan önce dosyayı alay et .

Alternatif olarak, Fabrika ve Komutanın birlikte birleştiğinde çalıştığını test etmek iyidir - Entegrasyon Testine oldukça mutlu bir şekilde uyum sağlar. Buradaki soru daha çok Üniteyi ayrı ayrı test edip edemeyeceğinize bağlıdır.


Bu özel örnekte, onları birbirine bağlayan şey kaynaktır - örneğin sistem diski. Aksi takdirde iki sınıf arasında etkileşim yoktur.
James Wood

1

Bir yerden veri alın. Bu verileri dönüştürün. Bu verileri bir yere koyun.

David Parnas'ın 1972'de hakkında yazdığı tipik bir prosedür yaklaşımıdır . İşlerin nasıl gittiğine konsantre olursunuz. Probleminizin somut çözümünü her zaman yanlış olan daha üst düzey bir desen olarak kabul edersiniz.

Eğer nesne yönelimli yaklaşım takip ederse, senin o konsantre olur etki . Bütün bunlar ne hakkında? Sisteminizin birincil sorumlulukları nelerdir? Alan adı uzmanlarınızın dilinde sunulan temel kavramlar nelerdir? Bu nedenle, alan adınızı anlayın, ayrıştırın, üst düzey sorumluluk alanlarını modülleriniz gibi ele alın , isimler olarak temsil edilen alt düzey kavramlara nesneleriniz gibi davranın. İşte son sorulara verdiğim bir örnek , çok alakalı.

Ve tutarlılık ile ilgili belirgin bir sorun var, siz kendinizden bahsettiniz. Bazı değişiklikler bir giriş mantığı ve üzerinde testler yazıyorsanız, hiçbir şekilde işlevselliğinizin çalıştığını kanıtlamaz, çünkü bu verileri bir sonraki katmana aktarmayı unutabilirsiniz. Bakın, bu katmanlar kendinden bağlanmıştır. Ve yapay bir ayırma işleri daha da kötüleştirir. Kendimi biliyorum: Tamamen bu tarzda yazılmış, omuzlarımın arkasında 100 adam yılı olan 7 yıllık bir proje. Mümkünse ondan kaç.

Ve tüm SRP olayında. Her şey sorun alanınıza, yani alan adınıza uygulanan uyum ile ilgilidir . SRP'nin arkasındaki temel prensip budur. Bu, nesnelerin akıllı olmasına ve sorumluluklarını kendileri için uygulamalarına neden olur. Kimse onları kontrol etmiyor, kimse onlara veri sağlamıyor. Veri ve davranışı birleştirerek yalnızca ikincisini ortaya çıkarırlar. Böylece nesneleriniz hem ham veri doğrulamasını, veri dönüşümünü (yani davranış) hem de sürekliliği birleştirir. Aşağıdaki gibi görünebilir:

class FinanceTransaction
{
    private $id;
    private $storage;

    public function __construct(UUID $id, DataStorage $storage)
    {
        $this->id = $id;
        $this->storage = $storage;
    }

    public function perform(
        Order $order,
        Customer $customer,
        Merchant $merchant
    )
    {
        if ($order->isExpired()) {
            throw new Exception('Order expired');
        }

        if ($customer->canNotPurchase($order)) {
            throw new Exception('It is not legal to purchase this kind of stuff by this customer');
        }

        $this->storage->save($this->id, $order, $customer, $merchant);
    }
}

(new FinanceTransaction())
    ->perform(
        new Order(
            new Product(
                $_POST['product_id']
            ),
            new Card(
                new CardNumber(
                    $_POST['card_number'],
                    $_POST['cvv'],
                    $_POST['expires_at']
                )
            )
        ),
        new Customer(
            new Name(
                $_POST['customer_name']
            ),
            new Age(
                $_POST['age']
            )
        ),
        new Merchant(
            new MerchantId($_POST['merchant_id'])
        )
    )
;

Sonuç olarak, bazı işlevleri temsil eden birkaç uyumlu sınıf vardır. Doğrulamanın tipik olarak değer nesnelerine gittiğini unutmayın - en azından DDD yaklaşımında.


1

Mücadele ettiğim yerde teste geldiğimde, sonunda sıkı sıkıya bağlı testler yapacağım. Örneğin;

  • Fabrika - dosyaları diskten okur.
  • Komutan - dosyaları diske yazar.

Dosya sistemi ile çalışırken sızdıran soyutlamalara dikkat edin - Çok sık ihmal edildiğini gördüm ve tarif ettiğiniz belirtiler var.

Sınıf bu dosyalardan gelen / bu dosyalara giren veriler üzerinde çalışıyorsa, dosya sistemi uygulama ayrıntısı (G / Ç) haline gelir ve dosyadan ayrılması gerekir. Bu sınıflar (fabrika / komutan / arabulucu) sağlanan tek veri depolamak / okumak değilse, dosya sisteminin farkında olmamalıdır. Dosya sistemi ile ilgilenen sınıflar, yollar (yapıcıdan geçirilebilir) gibi bağlama özgü parametreleri kapsamalıdır, böylece arayüz doğasını açığa çıkarmamıştır (arayüz adındaki "Dosya" kelimesi çoğu zaman bir kokudur).


"Bu sınıflar (fabrika / komutan / aracı), tek görevleri sağlanan verileri saklamak / okumak değilse, dosya sisteminin farkında olmamalıdır." Bu özel örnekte, tek yaptıkları bu.
James Wood

0

Bana göre bu doğru yolda ilerlemeye başlamışsınız ama yeterince ilerlemediniz. Bence işlevselliği bir şeyi yapan ve iyi yapan farklı sınıflara ayırmak doğrudur.

Bunu bir adım daha ileri götürmek için Factory, Mediator ve Commander sınıflarınız için arayüzler oluşturmalısınız. Daha sonra, diğerlerinin somut uygulamaları için birim testlerinizi yazarken bu sınıfların alay edilmiş sürümlerini kullanabilirsiniz. Alaylarla, yöntemlerin doğru sırada ve doğru parametrelerle çağrıldığını ve test edilen kodun farklı dönüş değerleriyle düzgün davrandığını doğrulayabilirsiniz.

Ayrıca, verilerin okunmasını / yazılmasını da soyutlayabilirsiniz. Şimdi bir dosya sistemine gideceksiniz, ancak gelecekte bir veritabanına veya hatta bir sokete gitmek isteyebilirsiniz. Verilerin kaynağı / hedefi değişirse arabulucu sınıfınızın değişmesi gerekmez.


1
YAGNI düşünmeniz gereken bir şey.
whatsisname
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.