Kod bazı ortak kodlara nasıl refactor edilir?


16

Arka fon

Devam eden bir C # projesi üzerinde çalışıyorum. Ben bir C # programcı, öncelikle bir C ++ programcısı değilim. Bu yüzden temelde kolay ve yeniden düzenleyici görevler atandım.

Kod bir karmaşa. Bu büyük bir proje. Müşterimiz yeni özellikler ve hata düzeltmeleri ile sık sık yayınlanmasını istediğinden, diğer tüm geliştiriciler kodlama sırasında kaba kuvvet yaklaşımı almak zorunda kaldılar. Kod son derece sürdürülemez ve diğer tüm geliştiriciler buna katılıyor.

Doğru yapıp yapmadıklarını tartışmak için burada değilim. Yeniden düzenleme yaptığım için, yeniden düzenlenmiş kodum karmaşık göründüğü için doğru şekilde yapıyorum mu merak ediyorum! İşte basit örnek olarak görevim.

Sorun

Altı sınıfları vardır: A, B, C, D, Eve F. Tüm sınıfların bir işlevi vardır ExecJob(). Altı uygulamanın hepsi çok benzer. Temel olarak, ilk başta A::ExecJob()yazıldı. Ardından B::ExecJob(), kopyala-yapıştır modifikasyonu ile uygulanan biraz farklı bir sürüm gerekli oldu A::ExecJob(). Başka bir biraz farklı versiyon gerektiğinde, C::ExecJob()yazıldı vb. Altı uygulamanın hepsinde bazı ortak kodlar, sonra bazı farklı kod satırları, sonra tekrar bazı ortak kodlar vb. Vardır. Uygulamalara basit bir örnek:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

SNTam olarak aynı ifadelerden oluşan bir grup nerede .

Onları ortak yapmak için başka bir sınıf oluşturdum ve ortak kodu bir işlevde taşıdım. Hangi ifade grubunun yürütülmesi gerektiğini denetlemek için parametre kullanma:

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

Bu örnekte yalnızca üç sınıf ve basitleştirilmiş ifadeler kullanıldığını unutmayın. Pratikte, CommonTask()fonksiyon tüm bu parametre kontrolleri ile çok karmaşık görünüyor ve daha birçok ifade var. Ayrıca, gerçek kodda, birkaç CommonTask()görünümlü işlevler vardır.

Tüm uygulamalar ortak kod paylaşıyor ve ExecJob()işlevler şirin görünüyor, ancak beni rahatsız eden iki sorun var:

  • Herhangi bir değişiklik için CommonTask()altı özelliğin de (ve gelecekte daha fazla olabilir) test edilmesi gerekir.
  • CommonTask()zaten karmaşık. Zamanla daha karmaşık hale gelecektir.

Doğru şekilde mi yapıyorum?


Martin Fowler'in Yeniden Düzenleme kitabında, kodu yeniden düzenlemek için yararlı bulabileceğiniz birçok özel teknik bulunmaktadır.
Allan

Yanıtlar:


14

Evet, kesinlikle doğru yoldasın!

Deneyimlerime göre, işler karmaşık olduğunda, değişikliklerin küçük adımlarla gerçekleştiğini fark ettim. Yaptığınız şey , evrim sürecindeki (veya yeniden düzenleme sürecindeki) 1. adımdır . İşte adım 2 ve adım 3:

Adım 2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

Bu 'Şablon Tasarım Deseni' ve yeniden düzenleme sürecinde bir adım öndedir. Temel sınıf değişirse, alt sınıfların (A, B, C) etkilenmesi gerekmez. Yeni alt sınıfları nispeten kolayca ekleyebilirsiniz. Ancak, yukarıdaki resimden hemen soyutlamanın bozulduğunu görebilirsiniz. 'Boş uygulama' ihtiyacı iyi bir göstergedir; soyutlamanızla ilgili bir sorun olduğunu gösterir. Kısa vadede kabul edilebilir bir çözüm olabilir, ancak daha iyi bir çözüm var gibi görünüyor.

Aşama 3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

Bu 'Strateji Tasarım Deseni' ve davanıza uygun görünüyor. İşi yürütmek için farklı stratejiler vardır ve her sınıf (A, B, C) bunu farklı şekilde uygular.

Bu süreçte bir adım 4 veya adım 5 veya çok daha iyi bir yeniden düzenleme yaklaşımının olduğundan eminim. Ancak, bu, yinelenen kodu ortadan kaldırmanıza ve değişikliklerin yerelleştirildiğinden emin olmanıza izin verecektir.


"Adım 2" de ana hatlarıyla verilen çözümde gördüğüm en büyük sorun, S5'in somut uygulamasının iki kez var olmasıdır.
user281377

1
Evet, kod çoğaltma ortadan kaldırılmaz! Ve bu soyutlamanın çalışmadığının bir başka göstergesi. Sadece süreç hakkında nasıl düşündüğümü göstermek için 2. adımı ortaya koymak istedim; daha iyi bir şey bulmak için adım adım bir yaklaşım.
Güven

1
1 Çok iyi strateji (ve bahsetmiyorum desen )!
Jordão

7

Aslında doğru olanı yapıyorsunuz. Bunu söylüyorum çünkü:

  1. Ortak bir görev işlevselliği için kodu değiştirmeniz gerekirse, ortak bir sınıfta yazmazsanız kodu içeren 6 sınıfın hepsinde de değiştirmeniz gerekmez.
  2. Kod satırı sayısı azalacaktır.

3

Bu tür kodların olay odaklı tasarımda çok fazla paylaşımda olduğunu görüyorsunuz (özellikle .NET). En sürdürülebilir yol, paylaşılan davranışınızı olabildiğince küçük parçalar halinde tutmaktır.

Yüksek seviyeli kodun bir sürü küçük yöntemi yeniden kullanmasına izin verin, yüksek seviyeli kodu paylaşılan tabanın dışında bırakın.

Yaprak / beton uygulamalarınızda çok fazla kazan plakası olacaktır. Panik yapma, sorun değil. Tüm bu kod doğrudan, anlaşılması kolay. İşler bozulduğunda ara sıra yeniden düzenlemeniz gerekecek, ancak değiştirilmesi kolay olacak.

Yüksek seviye kodunda birçok desen göreceksiniz. Bazen gerçektirler, çoğu zaman değildirler. Yukarıdaki beş parametrenin "konfigürasyonları" benzer görünür , ancak değildir. Bunlar tamamen farklı üç stratejidir.

Ayrıca tüm bunları kompozisyonla yapabileceğinizi ve asla miras konusunda endişelenmeyeceğinizi belirtmek isterim. Daha az kuplajınız olacak.


3

Eğer ben olsaydım muhtemelen başlangıçta 1 adım daha ekleyeceğim: UML tabanlı bir çalışma.

Tüm ortak parçaları bir araya getiren kodun yeniden düzenlenmesi her zaman en iyi hareket değildir, kulağa iyi bir yaklaşımdan ziyade geçici bir çözüm gibi gelir.

Bir UML şeması çizer, işleri basit ama etkili tutar, projenizle ilgili "bu yazılımı ne yapması gerekir?" "Bu yazılım parçasını soyut, modüler, genişletilebilir, ... vs vs tutmanın en iyi yolu nedir?" "Enkapsülasyonu en iyi şekilde nasıl uygulayabilirim?"

Ben sadece şunu söylüyorum: şu anda kod umurumda değil, sadece mantığı önemsemek zorundasınız, aklınızda açık bir mantık olduğunda, her şey gerçekten kolay bir iş haline gelebilir, sonunda tüm bu tür Karşılaştığınız sorunların nedeni sadece kötü bir mantıktan kaynaklanmaktadır.


Herhangi bir yeniden düzenleme yapılmadan önce bu ilk adım olmalıdır. Kod eşlenecek kadar anlaşılıncaya kadar (uml veya vahşi doğaların başka bir haritası), karanlıkta yeniden düzenleme mimar olacak.
Kzqai

3

İlk adım, nereye giderse gitsin, görünüşte büyük olan yöntemi A::ExecJobdaha küçük parçalara ayırmak olmalıdır .

Bu nedenle, yerine

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

anladın

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

Buradan sonra, gidilebilecek birçok yol var. Benim almam: A'yı sınıf hiyerarşisi ve ExecJob'un temel sınıfı yapın ve çok fazla kopyala yapıştırmadan B, C, ... oluşturmak kolaylaşır - sadece ExecJob'u (şimdi beş astarlı) değiştirilmiş ile değiştirin sürümü.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

Ama neden bu kadar çok ders var? Belki hepsini, hangi eylemlerin gerekli olduğu söylenebilen bir yapıcıya sahip tek bir sınıfla değiştirebilirsiniz ExecJob.



1

Eğer sınıflar tarafından kullanılan fonksiyonlar için ortak bir yer gerekir çünkü sadece - Öncelikle, miras işi için burada doğru araç gerçekten olduğundan emin olmak gerekir Aiçin Fbazen ayrı yardımcı - ortak bir taban sınıfı burada doğru şey olduğu anlamına gelmez sınıf işi daha iyi yapar. Öyle olabilir, olmayabilir. Bu, A'dan F'ye ve ortak temel sınıfınız arasında yapay isimler AF'den söylenmesi imkansız bir "is-a" ilişkisine sahip olmasına bağlıdır. Burada bu konuyla ilgili bir blog yazısı bulacaksınız.

Diyelim ki, ortak taban sınıfının sizin durumunuz için doğru olan şey olduğuna karar verdiniz. Sonra yapacağını ikinci şey emin S5 için kod parçaları S1 her ayrı yöntemlerde uygulanan yapmaktır S1()için S5()sizin temel sınıf. Daha sonra "ExecJob" işlevleri şöyle görünmelidir:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Gördüğünüz gibi, S1'den S5'e sadece yöntem çağrıları olduğundan, artık kod bloğu yok, kod çoğaltma neredeyse tamamen kaldırıldı ve artık daha fazla kontrol etme için herhangi bir parametreye ihtiyaç duymuyorsunuz. aksi takdirde.

Son olarak, ancak yalnızca üçüncü bir adım (!) Olarak, tüm bu ExecJob yöntemlerini temel sınıfınızdan biriyle birleştirmeyi düşünebilirsiniz, bu parçaların yürütülmesi parametrelerle, sadece önerdiğiniz şekilde veya kullanılarak kontrol edilebilir. şablon yöntem kalıbı. Gerçek koda dayanarak, durumunuzdaki çabaya değip değmeyeceğine kendiniz karar vermelisiniz.

Ancak IMHO, büyük yöntemleri küçük yöntemlere ayırmak için temel teknik, kod çoğaltmasından kaçınmak için kalıp uygulamaktan çok, çok daha önemlidir.

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.