Çok fazla soyutlama kodu genişletmeyi zorlaştırıyor


9

Ne kod tabanı (ya da en azından onunla ilgili) çok fazla soyutlama olduğunu hissediyorum sorunları ile karşı karşıya. Kod tabanındaki çoğu yöntemin kod tabanındaki en yüksek üst A'yı alması gerekir, ancak bu üst öğenin B alt öğesi, bu yöntemlerin bazılarının mantığını etkileyen yeni bir özniteliğe sahiptir. Sorun, girişlerin A olarak soyutlandığı ve A'nın elbette bu niteliğe sahip olmadığı için bu özniteliklerin bu yöntemlerde kontrol edilememesidir. B'yi farklı işlemek için yeni bir yöntem yapmaya çalışırsam, kod çoğaltma için çağrılır. Teknik liderimin önerisi, boole parametrelerini alan paylaşılan bir yöntem yapmaktır, ancak bununla ilgili sorun, bazı insanların bunu "gizli kontrol akışı" olarak görmesidir, burada paylaşılan yöntemin gelecekteki geliştiriciler tarafından görülmeyebilecek bir mantığı vardır. , ayrıca bu paylaşılan yöntem, daha küçük paylaşılan yöntemlere bölünmüş olsa bile, gelecekteki niteliklerin eklenmesi gerektiğinde aşırı karmaşık / kıvrımlı hale gelecektir. Bu aynı zamanda kuplajı arttırır, uyumu azaltır ve ekibimden birinin işaret ettiği tek sorumluluk ilkesini ihlal eder.

Esasen, bu kod tabanındaki soyutlamanın birçoğu kod çoğaltmasını azaltmaya yardımcı olur, ancak en yüksek soyutlamayı almak için yapıldığında genişletme / değiştirme yöntemlerini zorlaştırır. Böyle bir durumda ne yapmalıyım? Herkes neyi iyi gördükleri konusunda hemfikir olmasa da, sonuçta beni incitiyor.


10
"" sorunu "okuma yazma bilmeyen bir kod örneği eklemek durumu daha iyi anlamaya yardımcı olacaktır
Seabizkit

Bence burada iki tane SOLID ilkesi var. Tek Sorumluluk - Davranışı kontrol ettiği varsayılan bir işleve bir Boole iletirseniz, işlevin artık tek bir sorumluluğu olmayacaktır. Diğeri ise Liskov İkame ilkesidir. A sınıfını parametre olarak alan bir fonksiyon olduğunu düşünün. A yerine B sınıfından geçerseniz, bu işlevin işlevselliği bozulur mu?
bobek

A yönteminin oldukça uzun olduğunu ve birden fazla şey yaptığını sanıyorum. Durum böyle mi?
Rad80

Yanıtlar:


27

B'yi farklı işlemek için yeni bir yöntem yapmaya çalışırsam, kod çoğaltma için çağrılır.

Tüm kod çoğaltmaları eşit yaratılmaz.

Diyelim ki iki parametre alan ve bunları bir araya getiren bir yönteminiz var total(). Diyelim ki başka bir tane daha var add(). Uygulamaları tamamen aynı görünüyor. Tek bir yöntemle birleştirilmeli mi? HAYIR!!!

Sakın-tekrarlayın-Yourself veya KURU prensibi kodunu tekrar ibaret değildir. Bu, bir kararı, bir fikri yaymakla ilgilidir, böylece fikrinizi değiştirirseniz, bu fikri yaydığınız her yere yeniden yazmanız gerekir. Blegh. Bu korkunç. Yapma. Bunun yerine DRY'yi tek bir yerde karar vermenize yardımcı olması için kullanın .

KURU (Kendinizi Tekrarlama) İlkesi şunları belirtir:

Her bilgi parçasının bir sistem içinde tek, açık, yetkili bir temsili olmalıdır.

wiki.c2.com - Kendinizi Tekrar Etmeyin

Ancak DRY, başka bir yerin kopyası ve yapıştırması gibi görünen benzer bir uygulama arayan kod tarama alışkanlığına dönüştürülebilir. Bu DRY'nin beyin ölü formudur. Cehennem, bunu statik bir analiz aracıyla yapabilirsiniz. Bu yardımcı olmaz, çünkü kodu esnek tutacak olan DRY noktasını göz ardı eder .

Toplam gereksinimlerim değişirse, totaluygulamamı değiştirmeniz gerekebilir . Bu benim adduygulamamı değiştirmem gerektiği anlamına gelmiyor . Eğer bir goober onları bir yöntemle birlikte içerse, şimdi biraz gereksiz acı içindeyim.

Ne kadar acı? Şüphesiz sadece kodu kopyalayabilir ve ihtiyacım olduğunda yeni bir yöntem yapabilirim. Yani önemli değil mi? Malarky! Başka bir şey yoksa bana iyi bir isim mal ettiniz! İyi isimler gelmek zordur ve anlamlarıyla uğraşırken iyi cevap vermezler. Niyeti açıkça ortaya koyan iyi isimler, yöntemin doğru isme sahip olduğu zaman düzeltilmesi daha kolay olan bir hatayı kopyalama riskinden daha önemlidir.

Bu yüzden tavsiyem, benzer koda diz sarsıntısı tepkilerinin kod tabanınızı düğümlere bağlamasına izin vermemektir. Yöntemlerin var olduğu gerçeğini görmezden gelmenin ve bunun yerine nilly kopyalayıp yapıştırmanın serbest olduğunu söylemiyorum. Hayır, her yöntemin, ilgili olduğu fikrini destekleyen çok iyi bir adı olmalıdır. Uygulanması başka bir iyi fikrin uygulanmasıyla eşleşirse, şu anda, bugün kimin umrunda?

Öte yandan sum(), aynı veya hatta farklı bir uygulamaya sahip bir yönteminiz varsa total(), ancak toplam gereksinimleriniz değiştiğinde her zaman değiştirmek zorundasınız, sum()o zaman bunların iki farklı isim altında aynı fikir olma ihtimali vardır. Kod birleştirildiklerinde yalnızca daha esnek olmakla kalmaz, aynı zamanda kullanımı daha az kafa karıştırıcı olur.

Boolean parametrelerine gelince, evet bu kötü bir kod kokusu. Kontrol akışının sadece bir sorun olması değil, kötü bir noktada bir soyutlamayı kestiğini göstermesi daha da kötüsüdür. Soyutlamaların işleri daha basit, daha karmaşık değil hale getirmesi gerekiyor. Davranışlarını kontrol etmek için bir yönteme bools geçirmek, hangi yöntemi aradığınızı belirleyen gizli bir dil oluşturmak gibidir. Ow! Bunu bana yapma. Gosh polimorfizmine karşı dürüst olmadıkça, her yönteme kendi adını verin .

Şimdi, soyutlamada yanmış gibisin. Bu çok kötü çünkü soyutlama iyi yapıldığında harika bir şey. Bunu düşünmeden çok kullanıyorsunuz. Raf ve pinyon sistemini anlamak zorunda kalmadan her araba sürdüğünüzde, OS kesintilerini düşünmeden bir yazdırma komutu her kullandığınızda ve her bir kıl hakkında düşünmeden dişlerinizi her fırçaladığınızda.

Hayır, karşılaştığınız sorun kötü soyutlamadır. Soyutlama, ihtiyaçlarınıza göre farklı bir amaca hizmet etmek için yaratılmıştır. Karmaşık nesnelere basit arayüzlere ihtiyacınız vardır; bu, ihtiyaçlarınızı hiçbir zaman anlamak zorunda kalmadan gereksinimlerinizin karşılanmasını talep etmenizi sağlar.

Başka bir nesne kullanan istemci kodu yazdığınızda, ihtiyaçlarınızı ve o nesneden neye ihtiyacınız olduğunu bilirsiniz. Öyle değil. Bu nedenle istemci kodu arayüzün sahibidir. Müşteri olduğunuzda, sizden başka ihtiyaçlarınızın ne olduğunu size söyleyemez. İhtiyaçlarınızın ne olduğunu gösteren bir arayüz oluşturuyorsunuz ve size ne olursa olsun bu ihtiyaçları karşılamayı talep ediyorsunuz.

Bu soyutlama. Müşteri olarak ne konuştuğumu bile bilmiyorum . Sadece ondan neye ihtiyacım olduğunu biliyorum. Bu, bana bir şey vermeden önce arayüzünü değiştirmek için bir şey sarmanız gerektiği anlamına gelir. Umurumda değil. Sadece ihtiyacım olanı yap. Karmaşık hale getirmeyi bırak.

Nasıl kullanılacağını anlamak için bir soyutlamanın içine bakmam gerekirse, soyutlama başarısız oldu. Nasıl çalıştığını bilmeme gerek yok. Sadece işe yarıyor. Buna iyi bir isim verin ve eğer içeriye bakarsam bulduğum şey beni şaşırtmamalı. Nasıl kullanacağımı hatırlamak için içeri bakmaya devam etmeyin.

Soyutlamanın bu şekilde çalıştığında ısrar ettiğinizde, arkasındaki seviye sayısı önemli değildir. Soyutlamanın arkasına bakmadığınız sürece. Soyutlamanın, ona uyum sağlamayan ihtiyaçlarınıza uygun olduğunu ısrar ediyorsunuz. Bunun çalışması için kullanımı kolay, iyi bir isme sahip olmalı ve sızıntı yapmamalıdır .

Bu, Bağımlılık Enjeksiyonunu ortaya çıkaran tavırdır (ya da benim gibi eski bir okulsanız geçen referansı). Miras yerine kompozisyon ve delegasyon tercihiyle iyi çalışır . Tutum birçok isimle geçer. En sevdiğim söyle, sorma .

Bütün gün seni ilkelere boğabilirim. Ve iş arkadaşlarınızın zaten olduğu gibi geliyor. Ama işte şu: diğer mühendislik alanlarının aksine, bu yazılım şey 100 yıldan daha eski. Hepimiz hala çözüyoruz. Bu yüzden çok korkutucu sondaj kitabına sahip birinin zor okunan bir kod yazmasına zorbalık yapmasına izin vermeyin. Onları dinleyin ama mantıklı olmalarında ısrar edin. İman üzerine hiçbir şey alma. Sadece bu şekilde söylendiğinden bir şekilde kodlayan insanlar, neden en büyük karışıklığı yaptığını bilmeden.


Ben yürekten katılıyorum. KURU sırayla 14 sayfa, makale Do not tekrar edin üç kelimelik-catchphrase için üç harfli-kısaltmadır wiki . Tek yapmanız körlemesine okuma ve 14 sayfa yazı anlamadan bu üç harf mırıldanarak ise, olacak derde. Aynı zamanda Bir Kez Ve Sadece Bir Kez (OAOO) ile yakından ilişkilidir ve Tek Hakikat Noktası (SPOT) / Tek Hakikat Kaynağı (SSOT) ile daha gevşek ilişkilidir .
Jörg W Mittag

"Uygulamaları tamamen aynı görünüyor. Tek bir yöntemle birleştirilmeli mi? HAYIR !!!" - Bunun tersi de doğrudur: iki kod parçasının farklı olması kopya olmadıkları anlamına gelmez. Ron Jeffries'in OAOO wiki sayfasında büyük bir alıntı var : "Bir keresinde Beck'in neredeyse tamamen farklı koddan iki yamayı" çoğaltma "olarak ilan ettiğini gördüm. açıkça daha iyi bir şeyle. "
Jörg W Mittag

@ JörgW Tabii etiket. Önemli olan fikir. Fikri farklı görünümlü kodlarla çoğaltıyorsanız, hala kuru ihlal ediyorsunuz.
candied_orange

Kendinizi tekrar etmemekle ilgili 14 sayfalık bir makalenin kendini tekrar etme eğiliminde olacağını hayal etmeliyim.
Chuck Adams

7

Hepimizin burada ve orada okuduğumuz olağan derdi:

Tüm sorunlar, başka bir soyutlama katmanı eklenerek çözülebilir.

Bu doğru değil! Örneğiniz bunu gösteriyor. Bu nedenle biraz değiştirilmiş ifadeyi öneririm (yeniden kullanmaktan çekinmeyin ;-)):

Her problem, DOĞRU soyutlama seviyesi kullanılarak çözülebilir.

Davanızda iki farklı sorun var:

  • aşırı genelleme soyut bir düzeyde her yöntemi ekleyerek neden;
  • parçalanma büyük resmi almak ve kayıp hissi değil izlenime yol beton davranışları. Windows olay döngüsünde olduğu gibi.

Her ikisi de çekirdeklidir:

  • her uzmanlığın farklı yaptığı bir yöntemi soyutlarsanız, her şey yolunda demektir. Kimse a'nın bunu özel bir Shapeşekilde hesaplayabileceğini kavramakta sorun yaşamaz surface().
  • Ortak bir genel davranış modelinin olduğu bir operasyonu soyutlarsanız, iki seçeneğiniz vardır:

    • ya da her uzmanlıkta ortak davranışı tekrar edersiniz: bu çok gereksizdir; ve özellikle ortak kısmın uzmanlıklara paralel kalmasını sağlamak için bakımı zor:
    • şablon yöntemi deseninin bir çeşit varyantını kullanırsınız : bu, kolayca özelleştirilebilen ek soyut yöntemler kullanarak ortak davranışı hesaba katmanızı sağlar. Daha az gereksizdir, ancak ek davranışlar aşırı derecede bölünme eğilimindedir. Çok fazla şey, belki de çok soyut olduğu anlamına gelir.

Ek olarak, bu yaklaşım tasarım düzeyinde soyut bir birleştirme etkisi ile sonuçlanabilir. Her tür yeni özel davranış eklemek istediğinizde, bunu soyutlamanız, soyut üst öğeyi değiştirmeniz ve diğer tüm sınıfları güncellemeniz gerekir. Bu , kişinin isteyebileceği türden bir değişim değildir . Ve bu, uzmanlığa (en azından tasarımda) bağlı olmayan soyutlamaların ruhunda değil.

Tasarımını bilmiyorum ve daha fazla yardım edemem. Belki de gerçekten çok karmaşık ve soyut bir sorundur ve daha iyi bir yolu yoktur. Ama ihtimaller neler? Aşırı genleşme belirtileri burada. Tekrar bakmanın ve kompozisyonu genelleme üzerine düşünmenin zamanı geldi mi?


5

Davranışın parametresinin türünü açtığı bir yöntem gördüğümde, bu yöntem aslında method parametresine aitse hemen ilk önce düşünürüm. Örneğin, aşağıdaki gibi bir yöntem yerine:

public void sort(List values) {
    if (values instanceof LinkedList) {
        // do efficient linked list sort
    } else { // ArrayList
        // do efficient array list sort
    }
}

Bunu yapardım:

values.sort();

// ...

class ArrayList {
    public void sort() {
        // do efficient array list sort
    }
}

class LinkedList {
    public void sort() {
        // do efficient linked list sort
    }
}

Davranışı, ne zaman kullanılacağını bilen yere taşıyoruz. Uygulamanın türlerini veya ayrıntılarını bilmenize gerek olmayan gerçek bir soyutlama yaratırız . Durumunuz için, bu yöntemi yazıp geçersiz kılmak için orijinal sınıftan (arayacağım O) taşımak daha mantıklı olabilir . Yöntem bazı nesnelerde çağrılırsa , içindeki farklı davranışa geçin ve geçersiz kılın . Başlangıçta denilen yerden veri bitleri varsa veya yöntem yeterli yerlerde kullanılıyorsa, orijinal yöntemi bırakıp temsilci atayabilirsiniz:ABdoItdoItABdoIt

class O {
    int x;
    int y;

    public void doIt(A a) {
        a.doIt(this.x, this.y);
    }
}

Yine de biraz daha derine dalabiliriz. Bunun yerine bir boole parametresi kullanma önerisine bakalım ve iş arkadaşınızın nasıl düşündüğü hakkında neler öğrenebileceğimizi görelim. Onun önerisi:

public void doIt(A a, boolean isTypeB) {
    if (isTypeB) {
        // do B stuff
    } else { 
        // do A stuff
    }
}

Bu instanceof, ilk örneğimde kullandığım gibi korkunç bir şey gibi görünüyor , ancak bu kontrolü dışsallaştırıyoruz. Bu, onu iki yoldan biriyle çağırmamız gerektiği anlamına gelir:

o.doIt(a, a instanceof B);

veya:

o.doIt(a, true); //or false

İlk olarak, çağrı noktasının ne tür olduğuna dair hiçbir fikri yoktur A. Bu nedenle, booleanları tamamen aşağı mı geçirmeliyiz? Bu gerçekten kod tabanının her yerinde istediğimiz bir kalıp mı? Hesaplamamız gereken üçüncü bir tür varsa ne olur? Eğer yöntemin adı bu ise, onu türüne taşımalı ve sistemin polimorfik olarak bizim için uygulamayı seçmesine izin vermeliyiz.

İkinci şekilde, çağrı noktasındaki türünü zaten bilmeliyiza . Genellikle bu, oradaki örneği oluşturduğumuz veya bu türden bir örneği parametre olarak aldığımız anlamına gelir. Üzerinde bir yöntemi oluşturma Obir sürer Bçalışacak burada. Derleyici hangi yöntemi seçeceğini bilecektir. Bu gibi değişikliklerden geçerken , en azından gerçekten nereye gittiğimizi anlayana kadar , çoğaltma yanlış soyutlamayı yaratmaktan daha iyidir . Tabii ki, bu noktaya ne değiştirirsek değiştirelim, gerçekten yapılmadığını öneriyorum.

AVe arasındaki ilişkiye daha yakından bakmamız gerekiyor B. Genellikle, bize miras yerine kompozisyonu tercih etmemiz gerektiği söylenir . Bu, her durumda doğru değildir, ama biz kazmak kez vakaların şaşırtıcı sayıda doğrudur. BDevralır A, biz inanıyoruz anlamına Bbir olduğunu A. Btıpkı Abiraz farklı çalışması dışında kullanılmalıdır . Fakat bu farklar nelerdir? Farklılıklara daha somut bir isim verebilir miyiz? Değil Bbir A, ama gerçekten olabilir Abir Xvar A'ya da B'? Bunu yapsaydık kodumuz nasıl olurdu?

Yöntemi Adaha önce önerildiği şekilde üzerine taşırsak, Xiçine bir örnek Aekleyebilir ve bu yönteme şu şekilde temsilci atayabiliriz X:

class A {
    X x;
    A(X x) {
        this.x = x;
    }

    public void doIt(int x, int y) {
        x.doIt(x, y);
    }
}

Uygulayabiliriz A've B'kurtulabiliriz B. Daha örtük olabilecek bir kavram için bir ad vererek kodu geliştirdik ve derleme zamanı yerine çalışma zamanında bu davranışı ayarlamamıza izin verdik. Aaslında daha az soyutlaştı. Genişletilmiş miras ilişkisi yerine, temsilci bir nesne üzerinde yöntemler çağırıyor. Bu nesne soyuttur, ancak yalnızca uygulamadaki farklılıklara odaklanır.

Yine de bakılması gereken son bir şey var. İş arkadaşınızın teklifine geri dönelim. Tüm çağrı sitelerinde sahip olduğumuz türü açıkça biliyorsak, aşağıdaki Agibi çağrılar yapmalıyız:

B b = new B();
o.doIt(b, true);

Daha önce bu ya da Aolan bir kompozisyon oluştururken varsaymıştık . Ama belki bu varsayım bile doğru değildir. Bu tek yer arasındaki bu fark mı ve konular? Eğer öyleyse, belki biraz farklı bir yaklaşım alabiliriz. Hala ya da olan bir tane var , ama ait değil . Sadece umurunda, bu yüzden sadece şuraya geçelim :XA'B'ABXA'B'AO.doItO.doIt

class O {
    int x;
    int y;

    public void doIt(A a, X x) {
        x.doIt(a, x, y);
    }
}

Şimdi çağrı sitemiz şöyle görünüyor:

A a = new A();
o.doIt(a, new B'());

Bir kez daha Bkaybolur ve soyutlama daha odaklı hale gelir X. Ancak bu sefer Adaha az şey bilmek daha da kolay. Daha az soyut.

Kod tabanındaki yinelemeyi azaltmak önemlidir, ancak yinelemenin neden ilk etapta gerçekleştiğini düşünmeliyiz. Çoğaltma, dışarı çıkmaya çalışan daha derin soyutlamaların bir işareti olabilir.


1
Burada verdiğiniz örnek "kötü" kodun, OO olmayan bir dilde yapmaya meyilli olduğum şeye benzediğine dikkat çekiyor. Yanlış dersleri öğrendiklerini ve onları kodlama şekli olarak OO dünyasına getirip getirmediklerini merak ediyorum.
Baldrickk

1
@Baldrickk Her paradigma, benzersiz avantajları ve dezavantajları ile kendi düşünme yollarını getirir. Fonksiyonel Haskell'de, desen eşleştirme daha iyi bir yaklaşım olacaktır. Böyle bir dilde olsa da, orijinal sorunun bazı yönleri de mümkün olmazdı.
cbojar

1
Bu doğru cevap. Çalıştırdığı türe göre uygulamayı değiştiren bir yöntem, bu tür bir yöntem olmalıdır.
Roman Reiner

0

Kalıtım yoluyla soyutlama oldukça çirkinleşebilir. Tipik fabrikalara sahip paralel sınıf hiyerarşileri. Yeniden düzenleme baş ağrısı haline gelebilir. Ve daha sonra gelişim, bulunduğunuz yer.

Bir alternatif var: uzatma noktaları , katı soyutlamalar ve katmanlı özelleştirme. Belirli bir şehir için yapılan özelleştirmeye dayalı olarak, hükümet müşterilerinin bir özelleştirmesini söyleyin.

Uyarı: Maalesef bu, tüm (veya çoğu) sınıflar extendale yapıldığında en iyi sonucu verir. Senin için seçenek yok, belki küçük.

Bu genişletilebilirlik, genişletilebilir bir nesne tabanı sınıfının uzantıları saklamasıyla çalışır:

void f(CreditorBO creditor) {
    creditor.as(AllowedCreditorBO.class).ifPresent(allowedCreditor -> ...);
}

Dahili olarak, nesnenin uzantı sınıfına göre genişletilmiş nesnelere tembel bir eşleştirilmesi vardır.

GUI sınıfları ve bileşenleri için, kısmen kalıtımla aynı genişletilebilirlik. Düğmeler ve benzeri ekleme.

Sizin durumunuzda bir doğrulama, genişletilip genişletilmediğine bakmalı ve uzantılara karşı kendini doğrulamalıdır. Sadece bir vaka için uzatma noktalarının tanıtılması iyi değil anlaşılmaz bir kod ekler.

Yani mevcut bağlamda çalışmaya çalışmaktan başka bir çözüm yok.


0

'gizli akış kontrolü' kulağa çok elle geliyor.
Bağlam dışına çıkarılan herhangi bir yapı veya unsur bu özelliğe sahip olabilir.

Soyutlamalar iyidir. Onları iki kuralla öfkelendiriyorum:

  • Çok erken soyutlamamak daha iyi. Soyutlamadan önce daha fazla örnek örneği bekleyin. 'Daha' elbette öznel ve zor olan duruma özgüdür.

  • Sadece soyutlamanın iyi olması nedeniyle çok fazla soyutlama düzeyinden kaçının. Bir programcı, kod tabanını çekip 12 seviyeyi derinleştirdikçe yeni veya değiştirilmiş kod için bu seviyeleri başlarında tutmalıdır. İyi soyutlanmış bir kod arzusu, o kadar çok seviyeye yol açabilir ki, pek çok kişi takip etmek zordur. Bu aynı zamanda 'sadece ninja korunur' kod tabanlarına yol açar.

Her iki durumda da 'daha fazla ve' çok fazla 'sabit sayılar değildir. Değişir. Bunu zorlaştıran da bu.

Sandi Metz'den bu yazıyı da seviyorum

https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

çoğaltılması yanlış soyutlama çok daha ucuzdur
ve
yanlış soyutlama üzerine tekrarını tercih

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.