Bir sınıfın karmaşıklığını azaltmak


10

Bazı cevaplara baktım ve Google'da arama yaptım, ancak yararlı bir şey bulamadım (yani, garip yan etkileri olmaz).

Benim sorunum, soyut olarak, bir nesnem var ve üzerinde uzun bir işlem dizisi yapmam gerekiyor ; Bunu bir araba yapımı gibi bir çeşit montaj hattı olarak düşünüyorum.

Bu nesnelere Yöntem Nesneleri deneceğine inanıyorum .

Yani bu örnekte bir noktada, daha sonra installBackSeat, installFrontSeat, installWoodenInserts'i çalıştırmam gereken bir CarWithoutUpholstery'ye sahip olurdum (işlemler birbirine müdahale etmez ve hatta paralel olarak yapılabilir). Bu işlemler CarWithoutUpholstery.worker () tarafından gerçekleştirilir ve daha sonra belki cleanInsides (), verifyNoUpholsteryDefects () vb. Çalıştıracağım CarWithUpholstery olacak yeni bir nesne verir.

Tek bir fazdaki işlemler zaten bağımsızdır, yani, herhangi bir sırayla çalıştırılabilecek bir alt kümesiyle boğuşuyorum (ön ve arka koltuklar herhangi bir sırayla monte edilebilir).

Mantığım şu anda uygulamanın basitliği için Reflection kullanıyor.

Yani, bir kez CarWithoutUpholstery var, nesne kendini performSomething () denilen yöntemler için denetler. Bu noktada tüm bu yöntemleri yürütür:

myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...

hataları ve şeyleri kontrol ederken. İşlem sırası iken olduğu önemsiz, ben şimdiye kadar bazı sipariş sonuçta önemli olduğunu keşfetmek durumunda lexicographic düzeni atadınız. Bu , YAGNI ile çelişir , ancak çok az maliyetli - basit bir sıralama () - ve satırda yeniden adlandırma (veya test yapmak için başka bir yöntem, örneğin bir dizi yöntem) gibi büyük bir yöntem kaydedebilir.

Farklı bir örnek

Diyelim ki bir araba inşa etmek yerine birisi hakkında bir Gizli Polis raporu derlemeliyim ve bunu Evil Overlord'a sunmalıyım . Son hedefim bir ReadyReport olacak. İnşa etmek için temel bilgileri (ad, soyadı, eş ...) toplayarak başlıyorum. Bu benim A Aşamam. Bir eş olup olmamasına bağlı olarak, daha sonra B1 veya B2 aşamalarına ilerlemem ve bir veya iki kişi üzerinde cinsellik verileri toplamam gerekebilir. Bu gece hayatı, sokak kameraları, seks shop satış makbuzları ve ne kontrol farklı Evil Minions için birkaç farklı sorgu yapılır. Ve bu böyle devam eder.

Mağdurun ailesi yoksa, GetInformationAboutFamily aşamasına bile girmeyeceğim, ancak eğer öyleyse, önce babayı, anneyi veya kardeşleri (varsa) hedeflemem önemli değildir. Ancak bunu yapamam, bu nedenle daha önceki bir aşamaya ait bir FamilyStatusCheck gerçekleştirmedim.

Her şey harika çalışıyor ...

  • bazı ek işlemlere ihtiyacım olursa yalnızca özel bir yöntem eklemem gerekir,
  • eğer operasyon birkaç aşamada ortak ise bir üst sınıftan miras alabilirim,
  • operasyonlar basit ve bağımsızdır. Bir operasyondan şimdiye dek hiçbir değer (operasyonların başkalarının herhangi gereklidir yok , farklı bir fazda gerçekleştirilir)
  • çizginin altındaki nesneler , yaratıcıları nesneleri ilk etapta bu koşulları doğrulamamış olsalar bile var olamayacakları için birçok test yapmaları gerekmez . Yani, ekleri gösterge tablosuna yerleştirirken, gösterge tablosunu temizlerken ve gösterge tablosunu doğrularken, bir gösterge panelinin gerçekten orada olduğunu doğrulamam gerekmez .
  • kolay test yapılmasını sağlar. Kısmi bir nesneyi kolayca alay edebilir ve üzerinde herhangi bir yöntemi çalıştırabilirim ve tüm işlemler belirleyici kara kutulardır.

...fakat...

Tüm modülün zorunlu karmaşıklık indeksini ("N özel yöntemden daha az") aşmasına neden olan yöntem nesnelerimden birine son bir işlem eklediğimde sorun ortaya çıktı .

Meseleyi daha önce yukarıda ele aldım ve bu durumda, özel yöntemlerin zenginliğinin bir felaketin göstergesi olmadığını önerdim. Karmaşıklığı olduğunu orada, ancak operasyon çünkü orada olduğunu karmaşık ve aslında o kadar da karmaşık değil - bu sadece uzun .

Kötü Overlord örneğini kullanarak, benim sorunum Evil Overlord (aka O Reddedilmeyecek O da ) tüm diyet bilgi istedi , Diyet Kölelerinin bana restoranlar, küçük mutfak, sokak satıcıları, lisanssız sokak satıcıları, sera sorgulamak gerektiğini söylüyor sahipleri vb. ve Evil (sub) Overlord - tanıdık O Olarak Reddedilmeyecek diye bilinen - GetDietaryInformation aşamasında çok fazla sorgu gerçekleştirdiğimden şikayet ediyor.

Not : Birkaç açıdan bakıldığında bunun hiç bir sorun olmadığını biliyorum (olası performans sorunlarını göz ardı etmek vb.). Tüm bunlar, belirli bir metriğin mutsuz olduğu ve bunun için bir gerekçe olduğu.

Ne yapabileceğimi düşünüyorum

İlki dışında, tüm bu seçenekler yapılabilir ve bence savunulabilir.

  • Sinsi olabileceğimi ve yöntemlerimin yarısını beyan edebileceğimi doğruladım protected. Ama test prosedüründeki bir zayıflığı sömürecektim ve yakalandığında kendimi haklı çıkarmanın dışında, bunu sevmiyorum. Ayrıca, bir durma aralığı ölçüsüdür. Gerekli işlem sayısı iki katına çıkarsa ne olur? Olası değil, ama sonra ne olacak?
  • Bu aşamayı keyfi olarak AnnealedObjectAlpha, AnnealedObjectBravo ve AnnealedObjectCharlie'ye bölebilirim ve her aşamada gerçekleştirilen işlemlerin üçte birini alabilirim. Bunun aslında bir testi geçmek dışında hiçbir faydası olmadan karmaşıklık (N-1 sınıf daha) eklediğine inanıyorum . Tabii ki bir CarWithFrontSeatsInstalled ve bir CarWithAllSeatsInstalled mantıksal olarak birbirini takip eden aşamalar olabilir. Bir Bravo yönteminin daha sonra Alpha tarafından istenmesi riski küçüktür ve iyi oynarsam daha da küçüktür. Ama hala.
  • Uzaktan benzer şekilde farklı işlemleri tek bir işlemde toplayabilirim. performAllSeatsInstallation(). Bu sadece bir durma aralığı ölçüsüdür ve tek işlemin karmaşıklığını artırır. A ve B işlemlerini farklı bir sırayla yapmam gerekiyorsa ve bunları E = (A + C) ve F (B + D) içine paketlediysem, E ve F'yi ayırmam ve kodu karıştırmam gerekecek .
  • Bir dizi lambda fonksiyonunu kullanabilir ve kontrolü tamamen kaldırırım, ancak bu hantal buluyorum. Ancak bu şimdiye kadarki en iyi alternatif. Yansımasından kurtulacaktı. Sahip olduğum iki sorun, muhtemelen sadece hipotetik değil, tüm yöntem nesnelerini yeniden yazmamın istenmesi CarWithEngineInstalledve bu çok iyi bir iş güvenliği olsa da, gerçekten bu kadar cazip değil; ve kod kapsamı denetleyicisinin lambdas'la ilgili sorunları var (çözülebilir ancak yine de ).

Yani...

  • Hangisi benim en iyi seçeneğim?
  • Düşünmediğim daha iyi bir yol var mı? ( belki de temiz gelip doğrudan bunun ne olduğunu sorsam iyi olur. )
  • Bu tasarım umutsuzca kusurlu mu ve yenilgi ve hendek - bu mimari tamamen mi kabul etmeliyim? Kariyerim için iyi değil, ama kötü tasarlanmış kod yazmak uzun vadede daha iyi olur mu?
  • Mevcut seçimim aslında Tek Doğru Yol mu ve daha kaliteli metrikleri (ve / veya enstrümantasyonu) kurmak için savaşmam gerekiyor mu? Bu son seçenek için referanslara ihtiyacım var ... Mırıldanırken sadece @PHB'ye el sallayamıyorum Bunlar aradığınız metrikler değil . Ne kadar kazanmak istersem de

3
Veya "maksimum karmaşıklık aşıldı" uyarısını yok sayabilirsiniz.
Robert Harvey

Yapabilseydim, yapardım. Her nasılsa bu metrikler kendi başına kutsal bir nitelik kazanmıştır. PTB'yi yararlı bir araç olarak kabul etmeye ikna etmeye devam ediyorum, ama sadece bir araç. Başarılı olabilirim ...
LSerni

İşlemler gerçekten bir nesne mi oluşturuyor, yoksa bahsettiğiniz belirli özelliği paylaştığı için (montaj işlemleri kısmi bağımlılık düzenine sahip birçok işlem) sadece montaj hattı metaforunu mu kullanıyorsunuz? Önemi, birincisi oluşturucu desenini önerirken, ikincisi daha fazla ayrıntı içeren başka bir model önerebilir.
outis

2
Metriklerin zulmü. Metrikler yararlı göstergelerdir, ancak metriğin neden kullanıldığını göz ardı ederken bunları uygulamak yararlı değildir.
Jaydee

2
Üst düzey insanlara temel karmaşıklık ve kazara karmaşıklık arasındaki farkı açıklayın. en.wikipedia.org/wiki/No_Silver_Bullet Kuralı ihlal eden karmaşıklığın gerekli olduğundan eminseniz, programcılar.stackexchange.com/a/ 297414/51948 uyarınca refactor yapıyorsanız, muhtemelen kuralın etrafında kayıyor ve yayıyorsunuzdur. karmaşıklığı Bir şeyleri aşamalara kapsüllemek keyfi değilse (aşamalar sorunla ilgilidir) ve yeniden tasarım, kodu koruyan geliştiriciler için bilişsel yükü azaltırsa, bunu yapmak mantıklıdır.
Fuhrmanator

Yanıtlar:


15

Uzun operasyon dizisi, kendi sınıfında olması gerektiği gibi görünüyor, bu da görevlerini yerine getirmek için ek nesnelere ihtiyaç duyuyor. Bu çözüme yakın olduğunuz anlaşılıyor. Bu uzun prosedür birden fazla aşamaya veya aşamaya ayrılabilir. Her adım veya aşama, genel bir yöntemi ortaya koyan kendi sınıfı olabilir. Her aşamanın aynı arayüzü kullanmasını istersiniz. Daha sonra montaj hattınız bir faz listesini takip eder.

Araç montaj örneğiniz üzerine inşa etmek için Carsınıfı hayal edin :

public class Car
{
    public IChassis Chassis { get; private set; }
    public Dashboard { get; private set; }
    public IList<Seat> Seats { get; private set; }

    public Car()
    {
        Seats = new List<Seat>();
    }

    public void AddChassis(IChassis chassis)
    {
        Chassis = chassis;
    }

    public void AddDashboard(Dashboard dashboard)
    {
        Dashboard = dashboard;
    }
}

Ben ve kısaca uğruna IChassis, bazı bileşenlerin uygulamalarını dışarıda bırakacağım .SeatDashboard

CarKendisini bina sorumlu tutulamaz. Montaj hattı, bu yüzden bunu bir sınıfta kapsülleyelim:

public class CarAssembler
{
    protected List<IAssemblyPhase> Phases { get; private set; }

    public CarAssembler()
    {
        Phases = new List<IAssemblyPhase>()
        {
            new ChassisAssemblyPhase(),
            new DashboardAssemblyPhase(),
            new SeatAssemblyPhase()
        };
    }

    public void Assemble(Car car)
    {
        foreach (IAssemblyPhase phase in Phases)
        {
            phase.Assemble(car);
        }
    }
}

Buradaki önemli ayrım, CarAssembler'ın uygulayan montaj aşaması nesnelerinin bir listesine sahip olmasıdır IAssemblyPhase. Artık genel yöntemlerle açıkça ilgileniyorsunuz, yani her aşama kendi başına test edilebilir ve kod kalitesi aracınız daha mutludur, çünkü tek bir sınıfa çok fazla doldurmuyorsunuz. Montaj süreci de son derece basit. Sadece fazlar üzerinde döngü Assembleve arabada çağrı çağırmak . Bitti. IAssemblyPhaseArayüz ayrıca bir araba nesnesi alır sadece tek kamu yöntemle ölü basittir:

public interface IAssemblyPhase
{
    void Assemble(Car car);
}

Şimdi her bir aşama için bu arayüzü uygulayan somut sınıflarımıza ihtiyacımız var:

public class ChassisAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.AddChassis(new UnibodyChassis());
    }
}

public class DashboardAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        Dashboard dashboard = new Dashboard();

        dashboard.AddComponent(new Speedometer());
        dashboard.AddComponent(new FuelGuage());
        dashboard.Trim = DashboardTrimType.Aluminum;

        car.AddDashboard(dashboard);
    }
}

public class SeatAssemblyPhase : IAssemblyPhase
{
    public void Assemble(Car car)
    {
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
        car.Seats.Add(new Seat());
    }
}

Her aşama ayrı ayrı oldukça basit olabilir. Bazıları sadece bir astar. Bazıları sadece dört koltuk ekleyerek kör edici olabilir ve bir başkası arabaya eklemeden önce kontrol panelini yapılandırmalıdır. Bu uzun süren sürecin karmaşıklığı, kolayca test edilebilen çoklu, yeniden kullanılabilir sınıflara ayrılmıştır.

Siparişin şu anda önemli olmadığını söylemiş olsanız da, gelecekte olabilir.

Bunu bitirmek için, bir araba montaj sürecine bakalım:

Car car = new Car();
CarAssembler assemblyLine = new CarAssembler();

assemblyLine.Assemble(car);

Belirli bir araba veya kamyon türünü monte etmek için CarAssembler sınıfını alt gruplara ayırabilirsiniz. Her faz, kodun daha kolay kullanılmasını sağlayan daha geniş bir bütün içinde bir birimdir.


Bu tasarım umutsuzca kusurlu mu ve yenilgi ve hendek - bu mimari tamamen mi kabul etmeliyim? Kariyerim için iyi değil, ama kötü tasarlanmış kod yazmak uzun vadede daha iyi olur mu?

Yazdığım şeyin yeniden yazılması gerektiğine karar vermek kariyerimde bir kara leke olsaydı, şu anda bir hendek kazıcı olarak çalışıyordum ve tavsiyemi almamalıydın. :)

Yazılım mühendisliği, öğrenme, uygulama ve daha sonra yeniden öğrenme için sonsuz bir süreçtir. Kodunuzu yeniden yazmaya karar vermeniz, işten kovulacağınız anlamına gelmez. Eğer böyle olsaydı, yazılım mühendisleri olmazdı.

Mevcut seçimim aslında Tek Doğru Yol mu ve daha kaliteli metrikleri (ve / veya enstrümantasyonu) kurmak için savaşmam gerekiyor mu?

Ana hatlarıyla belirttiğiniz şey Tek Doğru Yol değildir, ancak kod metriklerinin de Tek Doğru Yol olmadığını unutmayın . Kod metrikleri uyarı olarak görülmelidir. Gelecekte bakım sorunlarına işaret edebilirler. Bunlar kılavuz değil, yasalar. Kod metrikleri aracınız bir şeyi işaret ettiğinde, önce kodu SOLID ilkelerini doğru bir şekilde uygulayıp uygulamadığını görmek için araştırırım . Çoğu zaman kodu daha fazla SOLID yapmak için yeniden düzenleme, kod metrik araçlarını tatmin edecektir. Bazen olmaz. Bu metrikleri duruma göre almanız gerekir.


1
Evet, bir tanrı sınıfına sahip olmaktan çok daha iyi.
BЈовић

Bu yaklaşım işe yarar, ancak çoğunlukla parametresiz araç elemanlarını alabileceğiniz için. Ya onları geçmen gerekiyorsa? Sonra tüm bunları pencereden dışarı atabilir ve prosedürü tamamen yeniden düşünebilirsiniz, çünkü gerçekten 150 parametreli bir araba fabrikasına sahip değilsiniz, bu fındık.
Andy

@DavidPacker: Bir sürü şeyi parametreleştirmeniz gerekse bile, bunu "assembly" nesnenizin yapıcısına ilettiğiniz bir çeşit Config sınıfında da kapsülleyebilirsiniz. Bu araba örneğinde, boya rengi, iç renk ve malzemeler, trim paketi, motor ve çok daha fazlası özelleştirilebilir, ancak mutlaka 150 özelleştirmeniz olmayacaktır. Muhtemelen bir düzine nesneye sahip olacaksınız, bu da bir seçenekler nesnesine borç veriyor.
Greg Burghardt

Ancak sadece yapılandırmayı eklemek, muazzam bir utanç olan güzel for döngüsü amacına meydan okuyacaktır, çünkü yapılandırmayı ayrıştırmanız ve karşılığında size uygun nesneleri verecek uygun yöntemlere atamanız gerekir. Böylece muhtemelen orijinal tasarımınkine oldukça benzer bir şey elde edersiniz.
Andy

Anlıyorum. Bir sınıf ve elli yöntem yerine, aslında elli montajcı yalın sınıf ve bir orkestrasyon sınıfım olurdu. Sonunda sonuç aynı, aynı zamanda performans açısından da akıllıca görünüyor, ancak kod düzeni daha temiz. Bunu sevdim. Operasyonları eklemek biraz daha garip olacak (onları sınıflarında ayarlamam, artı orkestrasyon sınıfını bilgilendirmem; bu zorunluluktan kolay bir yol göremiyorum), ama yine de çok beğendim. Bu yaklaşımı keşfetmem için bana birkaç gün izin ver.
LSerni

1

Bunun sizin için mümkün olup olmadığını bilmiyorum, ama daha veri odaklı bir yaklaşım kullanmayı düşünürdüm. Fikir, kuralların ve kısıtlamaların, işlemlerin, bağımlılıkların, durumların, vb. kodu daha doğrudan kullanmak yerine kuralların ve durum değişikliklerinin bildirici yakalanmasını kullanarak durumu değiştirin. Programlama dilinde veri bildirimleri veya bazı DSL takımları veya bir kural veya mantık motoru kullanılabilir.


Bazı işlemler aslında bu şekilde uygulanır (kurallar listesi olarak). Araba örneğine devam etmek için, bir kutu sigorta, ışık ve fiş takarken aslında üç farklı yöntem kullanmıyorum, ancak iki pinli elektriksel şey yerine genel olarak uyan ve üç Sigorta Listesi'ni alan sadece bir tane kullanıyorum. , ışıklar ve fişler. Diğer yöntemlerin büyük çoğunluğu maalesef bu yaklaşıma kolayca uygun değildir.
LSerni

1

Sorun bir şekilde bana bağımlılıkları hatırlatıyor. Belirli bir yapılandırmada bir araba istiyorsunuz. Greg Burghardt gibi, her adım / madde /… için nesneler / sınıflarla giderdim.

Her adım karmaya ne eklediğini (ne provides) (birden fazla şey olabilir), aynı zamanda ne gerektirdiğini de bildirir .

Ardından, gerekli son yapılandırmayı bir yerde tanımlar ve neye ihtiyacınız olduğunu inceleyen bir algoritmaya sahip olursunuz ve hangi adımların yürütülmesi gerektiğine / hangi parçaların eklenmesi gerektiğine / hangi belgelerin toplanması gerektiğine karar verirsiniz.

Bağımlılık çözümleme algoritmaları o kadar da zor değildir. En basit durum, her adımın bir şey sağladığı ve hiçbir şeyin aynı şeyi sağlamadığı ve bağımlılıkların basit olduğu (bağımlılıklarda 'veya' hayır) olmasıdır. Daha karmaşık durumlar için: debian apt gibi bir araca bakın.

Son olarak, arabanızı inşa etmek olası adımların azalmasına neden olur ve CarBuilder'ın gerekli adımları bulmasına izin verir.

Ancak önemli bir şey, CarBuilder'a tüm adımlar hakkında bilgi vermenin bir yolunu bulmanız gerektiğidir. Bir yapılandırma dosyası kullanarak bunu yapmaya çalışın. Ek bir adım eklemek için yalnızca yapılandırma dosyasına bir ekleme yapılması gerekir. Mantığa ihtiyacınız varsa, yapılandırma dosyasına bir DSL eklemeyi deneyin. Her şey başarısız olursa, bunları kodda tanımlamak zorunda kalırsınız.


0

Bahsettiğiniz birkaç şey benim için 'kötü' olarak göze çarpıyor

"Mantığım şu anda uygulamanın basitliği için Reflection kullanıyor"

Gerçekten bunun için bir mazeret yok. neyin çalıştırılacağını belirlemek için yöntem adlarının dize karşılaştırmasını gerçekten kullanıyor musunuz ?! sırayı değiştirirseniz, yöntemin adını değiştirmeniz gerekir mi ?!

"Tek bir aşamadaki işlemler zaten bağımsız"

Eğer birbirlerinden gerçekten bağımsızlarsa, sınıfınızın birden fazla sorumluluk üstlendiğini gösterir. Bana göre, her iki örnek de kendilerini bir tür

MyObject.AddComponent(IComponent component) 

Her bir karşıtlık için mantığı kendi sınıfına veya sınıflarına bölmenize izin verecek bir yaklaşım.

Aslında onların gerçekten bağımsız olmadıklarını, her birinin nesnenin durumunu incelediğini ve değiştirdiğini veya en azından başlamadan önce bir tür doğrulama gerçekleştirdiğini hayal ediyorum.

Bu durumda aşağıdakilerden birini yapabilirsiniz:

  1. OOP'u pencereden dışarı atın ve sınıfınızdaki açıkta kalan veriler üzerinde çalışan hizmetlere sahip olun.

    Service.AddDoorToCar (Araba arabası)

  2. Önce gerekli verileri toplayıp tamamlanan nesneyi geri vererek nesneyi oluşturan bir oluşturucu sınıfına sahip ol.

    Araba = CarBuilder.WithDoor (). WithDoor (). WithWheels ();

(withX mantığı tekrar alt inşaatçılara bölünebilir)

  1. Fonksiyonel yaklaşımı benimseyin ve fonksiyonları / delgeleri bir değiştirme yöntemine geçirin

    Car.Modify ((c) => c.doors ++);


Ewan şöyle dedi: "OOP'u pencereden dışarı atın ve sınıfınızdaki açık veriler üzerinde çalışan hizmetlere sahip olun" --- Bu nasıl Nesne yönelimli değil?
Greg Burghardt

?? değil, onun OO olmayan bir seçenek
Ewan

Nesneleri kullanıyorsunuz, yani "nesne yönelimli". Kodunuz için bir programlama deseni tanımlayamamanız, nesnenin nesneye yönelik olmadığı anlamına gelmez. KATI prensipleri uygulamak iyi bir kuraldır. SOLID ilkelerinin bir kısmını veya tamamını uygularsanız, nesne yönelimlidir. Gerçekten, nesneleri kullanıyorsanız ve sadece farklı prosedürlere veya statik yöntemlere yapıları iletmekle kalmazsanız, nesne yönelimlidir. "Nesneye yönelik kod" ile "iyi kod" arasında bir fark vardır. Kötü nesne yönelimli kod yazabilirsiniz.
Greg Burghardt

onun 'OO değil' çünkü verileri bir yapıdaymış gibi açığa çıkarıyorsunuz ve işlemde olduğu gibi ayrı bir sınıfta çalıştırıyorsunuz
Ewan

tbh Sadece kaçınılmaz olan "OOP değil!" yorumlar
Ewan
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.