วิธีการใช้หลักการ DRY เมื่อใช้คำหลัก 'ใช้'?


23

พิจารณาวิธีการเหล่านี้:

public List<Employee> GetAllEmployees()
{
    using (Entities entities = new Entities())
    {
        return entities.Employees.ToList();
    }
}

public List<Job> GetAllJobs()
{
    using (Entities entities = new Entities())
    {
        return entities.Jobs.ToList();
    }
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    using (Entities entities = new Entities())
    {
        return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

การใช้บล็อกเหมือนกันและทำซ้ำ 3 ครั้งที่นี่ (แน่นอนมากกว่า 100 ครั้งในแอปพลิเคชันจริง) เป็นไปได้อย่างไรที่จะนำหลักการ DRY (อย่าซ้ำตัวเอง) ไปใช้กับusingบล็อก มันถือเป็นการฝ่าฝืนหลักการของ DRY หรือไม่?

อัปเดต: ฉันไม่ได้พูดถึงสิ่งที่นำไปใช้ในusingบล็อก using (Entities entities = new Entities())สิ่งที่จริงผมหมายถึงที่นี่เป็น บรรทัดนี้ทำซ้ำ 100 ครั้งหรือมากกว่า


2
นี่คือ C # หรือไม่ คำตอบสำหรับคำถามของคุณอาจขึ้นอยู่กับภาษา
เดวิด

ใช่ @David ขอโทษที่ฉันไม่ได้พูดภาษาของฉัน มันสามารถส่งผลกระทบต่อคำตอบได้อย่างไร
Saeed Neamati

บางภาษาสามารถมีไวยากรณ์เฉพาะที่สามารถช่วยให้คุณแยกรหัสของคุณได้ ฉันไม่รู้ C # แต่ใน Ruby ฉันคิดว่าคุณสามารถใช้บล็อคเพื่อแยกส่วนการใช้
เดวิด

ใช้คำสั่งจริงให้สนับสนุน C # ภาษาที่จะใช้หลักการแห้งเพื่อช่วยหลีกเลี่ยงการเข้ารหัสซ้ำขณะที่การจัดการทรัพยากรการกำจัดกับรูปแบบการออกแบบทิ้ง นั่นไม่ได้หมายความว่าเราไม่สามารถหาวิธีสร้างสิ่งที่แห้งได้! โดยส่วนตัวแล้วฉันคิดว่า DRY เป็นกระบวนการแบบเรียกซ้ำ
John Tobler

คำตอบ:


24

Funcหนึ่งความคิดที่จะห่อด้วยฟังก์ชั่นที่ใช้เป็น

บางสิ่งเช่นนี้

public K UsingT<T,K>(Func<T,K> f) where T:IDisposable,new()
{
    using (T t = new T())
    {
        return f(t);
    }
}

จากนั้นโค้ดด้านบนของคุณจะกลายเป็น

public List<Employee> GetAllEmployees()
{
    return UsingT<Entities,List<Employee>>(e=>e.Employees.ToList());
}

public List<Job> GetAllJobs()
{
    return UsingT<Entities,List<Job>>(e=>e.Jobs.ToList());
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return UsingT<Entities,List<Task>>(e=>e.Tasks.Where(t => t.JobId == job.Id).ToList());
}

ฉันทำEntitiesแบบพารามิเตอร์เช่นกันเพราะฉันคิดว่าคุณมีมากกว่าหนึ่งประเภทที่คุณทำเช่นนี้ หากคุณไม่ได้คุณสามารถลบออกและเพียงแค่ใช้ประเภทพารามิเตอร์สำหรับประเภทกลับ

จะซื่อสัตย์แม้ว่ารหัสประเภทนี้ไม่ได้ช่วยให้อ่านง่ายเลย จากประสบการณ์ของฉันเพื่อนร่วมงานจูเนียร์มากขึ้นก็มีช่วงเวลาที่ยากลำบากเช่นกัน

อัปเดต รูปแบบเพิ่มเติมบางอย่างเกี่ยวกับผู้ช่วยเหลือที่คุณอาจพิจารณา

//forget the Entities type param
public T UsingEntities<T>(Func<Entities,T> f)
{
    using (Entities e = new Entities())
    {
        return f(e);
    }
}
//forget the Entities type param, and return an IList
public IList<T> ListFromEntities<T>(Func<Entities,IEnumerable<T>> f)
{
    using (Entities e = new Entities())
    {
        return f(e).ToList();
    }
}
//doing the .ToList() forces the results to enumerate before `e` gets disposed.

1
1 การแก้ปัญหาที่ดีแม้ว่ามันจะไม่ได้อยู่ที่ปัญหาที่เกิดขึ้นจริง (ไม่รวมอยู่ในคำถามเดิม) Entitiesเช่นที่ปรากฏหลายแห่ง
back2dos

@Doc Brown: ฉันคิดว่าฉันชนะคุณด้วยชื่อฟังก์ชั่น ฉันIEnumerableออกจากฟังก์ชันในกรณีที่ไม่มีIEnumerableคุณสมบัติใด ๆของ T ที่ผู้โทรต้องการส่งคืน แต่คุณพูดถูกมันจะล้างมันเล็กน้อย บางทีการมีผู้ช่วยสำหรับทั้งซิงเกิลและIEnumerableผลลัพธ์ก็น่าจะดี ที่กล่าวว่าฉันยังคิดว่ามันช้าลงการรับรู้สิ่งที่รหัสทำโดยเฉพาะอย่างยิ่งสำหรับคนที่ไม่คุ้นเคยกับการใช้ generics และ lambdas จำนวนมาก (เช่นผู้ร่วมงานของคุณที่ไม่ได้อยู่ที่ SO :))
Brook

+1 ฉันคิดว่าวิธีนี้ใช้ได้ และสามารถอ่านได้สามารถปรับปรุงได้ ตัวอย่างเช่นใส่ "ToList" ลงในWithEntitiesใช้Func<T,IEnumerable<K>>แทนFunc<T,K>และให้ "WithEntities" ชื่อที่ดีขึ้น (เช่น SelectEntities) และฉันไม่คิดว่า "เอนทิตี" ต้องเป็นพารามิเตอร์ทั่วไปที่นี่
Doc Brown

1
ในการทำให้งานนี้ต้องมีข้อ จำกัดwhere T : IDisposable, new()ตามที่usingต้องการIDisposableเพื่อทำงาน
Anthony Pegram

1
@ Anthony Pegram: คงที่ขอบคุณ! (นั่นคือสิ่งที่ฉันได้รับสำหรับการเข้ารหัส C # ในหน้าต่างเบราว์เซอร์)
Brook

23

สำหรับฉันนี่จะเป็นเรื่องที่กังวลเกี่ยวกับการเก็บสะสมเดียวกันหลาย ๆ ครั้ง: มันเป็นสิ่งที่คุณต้องทำ ความพยายามใด ๆ ที่จะทำให้เป็นนามธรรมเพิ่มเติมจะทำให้โค้ดอ่านน้อยลงมาก


การเปรียบเทียบที่ยอดเยี่ยม @Ben +1
Saeed Neamati

3
ฉันไม่เห็นด้วยขอโทษ อ่านความคิดเห็นของ OP เกี่ยวกับขอบเขตการทำธุรกรรมและคิดเกี่ยวกับสิ่งที่คุณต้องทำเมื่อคุณเขียนโค้ดประเภทนั้น 500 ครั้งแล้วสังเกตว่าคุณจะต้องเปลี่ยนสิ่งเดียวกัน 500 ครั้ง การทำซ้ำรหัสชนิดนี้อาจใช้ได้เมื่อคุณมี <10 ของฟังก์ชันเหล่านั้นเท่านั้น
Doc Brown

โอ้และอย่าลืมถ้าคุณสะสมคอลเล็คชั่นชุดเดียวกันมากกว่า 10 ครั้งในลักษณะที่คล้ายกันมากโดยใช้โค้ดที่มีลักษณะคล้ายกันในแต่ละชุดคุณควรคิดเกี่ยวกับการวางหลักเกณฑ์ทั่วไป
Doc Brown

1
สำหรับฉันมันดูเหมือนว่าคุณกำลังทำซับในสามเป็นออนไลน์เนอร์ ... คุณยังคงทำซ้ำตัวเองอยู่
Jim Wolff

มันขึ้นอยู่กับบริบทถ้าforeachเป็นมากกว่าคอลเลกชันที่มีขนาดใหญ่มากหรือตรรกะภายในforeachวงจะใช้เวลานานเช่น คำขวัญที่ฉันได้นำมาใช้: อย่ามัว แต่หมกมุ่นอยู่กับวิธีการของคุณเสมอ
Coops

9

ดูเหมือนว่าคุณกำลังสับสนกับหลักการ "กาลครั้งเดียวเท่านั้น" กับหลักการอบแห้ง หลักการของ DRY:

ความรู้ทุกชิ้นจะต้องมีการเป็นตัวแทนเพียงหนึ่งเดียวไม่คลุมเครือและเชื่อถือได้ภายในระบบ

อย่างไรก็ตามหลักการในกาลครั้งเดียวมีความแตกต่างกันเล็กน้อย

[DRY] หลักการคล้ายกับ OnceAndOnlyOnce แต่มีวัตถุประสงค์แตกต่างกัน ด้วย OnceAndOnlyOnce คุณจะได้รับการสนับสนุนให้ปรับโครงสร้างเพื่อกำจัดรหัสและฟังก์ชันการทำงานที่ซ้ำซ้อน ด้วย DRY คุณจะพยายามระบุแหล่งที่มาที่ชัดเจนของความรู้ทุกชิ้นที่ใช้ในระบบของคุณจากนั้นใช้แหล่งข้อมูลนั้นเพื่อสร้างอินสแตนซ์ของความรู้นั้น (รหัสเอกสารการทดสอบและอื่น ๆ )

หลักการ DRY มักจะใช้ในบริบทของตรรกะที่แท้จริงไม่มากเกินไปโดยใช้คำสั่ง:

การรักษาโครงสร้างของโปรแกรม DRY นั้นยากกว่าและมีค่าต่ำกว่า มันเป็นกฎเกณฑ์ทางธุรกิจข้อความ if if สูตรคณิตศาสตร์และข้อมูลเมตาที่ควรปรากฏในที่เดียว สิ่งของเปียก - หน้า HTML, ข้อมูลการทดสอบซ้ำ, จุลภาคและตัวคั่น {} - ทั้งหมดง่ายต่อการเพิกเฉยดังนั้นการอบแห้งมีความสำคัญน้อยกว่า

แหล่ง


7

ฉันไม่เห็นการใช้usingที่นี่:

เกี่ยวกับ:

public List<Employee> GetAllEmployees() {
    return (new Entities()).Employees.ToList();
}
public List<Job> GetAllJobs() {
    return (new Entities()).Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return (new Entities()).Tasks.Where(t => t.JobId == job.Id).ToList();
}

หรือดีกว่านี้เพราะฉันไม่คิดว่าคุณต้องสร้างวัตถุใหม่ทุกครั้ง

private Entities entities = new Entities();//not sure C# allows for that kind of initialization, but you can do it in the constructor if needed

public List<Employee> GetAllEmployees() {
    return entities.Employees.ToList();
}
public List<Job> GetAllJobs() {
    return entities.Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

สำหรับการละเมิด DRY: DRY ใช้ไม่ได้กับระดับนี้ ในความเป็นจริงไม่มีหลักการใดที่ทำได้ยกเว้นว่าอ่านได้ การพยายามใช้ DRY ในระดับนั้นจริงๆแล้วเป็นเพียงการเพิ่มประสิทธิภาพทางสถาปัตยกรรมขนาดเล็กซึ่งการเพิ่มประสิทธิภาพแบบ micro ทั้งหมดนั้นเป็นเพียงการปลดจักรยานและไม่ได้รับการแก้ไขปัญหาใด ๆ แต่ยังมีความเสี่ยงในการแนะนำใหม่
จากประสบการณ์ของฉันเองฉันรู้ว่าถ้าคุณพยายามลดความซ้ำซ้อนของรหัสในระดับนั้นคุณจะสร้างผลกระทบด้านลบต่อคุณภาพของรหัสโดยทำให้สับสนกับสิ่งที่ชัดเจนและเรียบง่าย

แก้ไข:
ตกลง ดังนั้นปัญหาจึงไม่ใช่คำสั่งที่ใช้จริงปัญหาคือการพึ่งพาวัตถุที่คุณสร้างขึ้นทุกครั้ง ฉันขอแนะนำให้ฉีดนวกรรมิก:

private delegate Entities query();//this should be injected from the outside (upon construction for example)
public List<Employee> GetAllEmployees() {
    using (var entities = query()) {//AFAIK C# can infer the type here
        return entities.Employees.ToList();
    }
}
//... and so on

2
แต่ @ back2dos มีหลายที่ที่เราใช้using (CustomTransaction transaction = new CustomTransaction())บล็อคโค้ดในรหัสของเราเพื่อกำหนดขอบเขตของธุรกรรม AA ที่ไม่สามารถรวมเป็นวัตถุเดียวและในทุกที่ที่คุณต้องการใช้ธุรกรรมคุณควรเขียนบล็อก ทีนี้ถ้าคุณต้องการเปลี่ยนประเภทของธุรกรรมนั้นCustomTransactionเป็นBuiltInTransactionมากกว่า 500 วิธี? นี่ดูเหมือนว่าฉันจะเป็นงานที่ทำซ้ำและเป็นตัวอย่างของการละเมิดหลักของ DRY
Saeed Neamati

3
การมี "การใช้" ที่นี่จะเป็นการปิดบริบทของข้อมูลเพื่อให้การโหลดแบบขี้เกียจไม่สามารถทำได้นอกวิธีการเหล่านี้
Steven Striga

@Seed: นั่นคือเมื่อคุณดูการฉีดพึ่งพา แต่ดูเหมือนว่าจะค่อนข้างแตกต่างจากกรณีที่ระบุไว้ในคำถาม
CVn

@Saeed: โพสต์อัปเดตแล้ว
back2dos

@WeekendWarrior using(ในบริบทนี้) ยังเป็น "ไวยากรณ์ที่สะดวก" ไม่เป็นที่รู้จักมากขึ้นหรือไม่ ทำไมมันจึงเป็นเรื่องดีที่จะใช้ =)
Coops

4

ไม่เพียง แต่ใช้เป็นรหัสที่ซ้ำกัน (โดยวิธีการเป็นรหัสที่ซ้ำกันและเปรียบเทียบจริง ๆ กับคำสั่ง try..catch..finally) แต่ toList ยัง ฉันจะ refactor รหัสของคุณเช่นนี้:

 public List<T> GetAll(Func<Entities, IEnumerable<T>> getter) {
    using (Entities entities = new Entities())
    {
        return getter().ToList();
    }
 }

public List<Employee> GetAllEmployees()
{
    return GetAll(e => e.Employees);
}

public List<Job> GetAllJobs()
{
    return GetAll(e => e.Jobs);
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return GetAll(e => e.Tasks.Where(t => t.JobId == job.Id));
}

3

เนื่องจากไม่มีตรรกะทางธุรกิจของการจัดเรียงใด ๆ ที่นี่ยกเว้นสำหรับการล่าสุด มันไม่แห้งจริงๆในมุมมองของฉัน

อันสุดท้ายไม่มี DRY ใด ๆ ในบล็อกการใช้งาน แต่ฉันเดาว่าประโยคใดควรเปลี่ยนตำแหน่งที่เคยใช้

นี่เป็นงานทั่วไปสำหรับตัวสร้างรหัส เขียนและครอบคลุมตัวสร้างโค้ดและปล่อยให้มันสร้างสำหรับแต่ละประเภท


ไม่มี @ arunmur มีความเข้าใจผิดที่นี่ โดย DRY ฉันหมายถึงusing (Entities entities = new Entities())บล็อก ฉันหมายความว่ารหัสบรรทัดนี้ซ้ำ 100 ครั้งและได้รับการทำซ้ำมากขึ้นเรื่อย ๆ
Saeed Neamati

ส่วนใหญ่มาจากความจริงที่ว่าคุณต้องเขียนกรณีทดสอบหลายครั้งและจุดบกพร่องในที่เดียวหมายความว่าคุณต้องแก้ไขที่ 100 แห่ง การใช้ (เอนทิตี้ ... ) นั้นง่ายเกินไปสำหรับการถอดรหัส ไม่จำเป็นต้องแยกหรือใส่ในคลาสอื่น หากคุณยังยืนยันในการทำให้มันง่ายขึ้น ฉันอยากจะแนะนำฟังก์ชั่นโทรกลับ Yeild จากทับทิม
arunmur

1
@arnumur - การใช้ไม่ง่ายเกินไปที่จะทำลาย ฉันมักจะมีตรรกะที่ดีที่กำหนดตัวเลือกที่จะใช้ในบริบทข้อมูล เป็นไปได้อย่างมากว่าสตริงการเชื่อมต่อที่ไม่ถูกต้องอาจถูกส่งเข้ามาได้
Morgan Herlocker

1
@ironcode - ในสถานการณ์เหล่านั้นมันสมเหตุสมผลแล้วที่จะผลักมันออกไปที่ฟังก์ชั่น แต่ในตัวอย่างเหล่านี้มันค่อนข้างยากที่จะทำลาย แม้ว่ามันจะทำลายการแก้ไขที่มักจะอยู่ในคำนิยามของหน่วยงานระดับเองซึ่งควรจะครอบคลุมกับกรณีทดสอบแยกต่างหาก
arunmur

+1 @arunmur - ฉันเห็นด้วย ฉันมักจะทำเอง ฉันเขียนการทดสอบสำหรับฟังก์ชั่นนั้น แต่ดูเหมือนว่าจะอยู่ด้านบนเล็กน้อยในการเขียนการทดสอบสำหรับการใช้คำสั่งของคุณ
Morgan Herlocker

2

เนื่องจากคุณกำลังสร้างและทำลายวัตถุที่ใช้แล้วทิ้งที่เหมือนกันซ้ำแล้วซ้ำอีกคลาสของคุณจึงเป็นตัวเลือกที่ดีสำหรับการนำรูปแบบ IDisposable ไปใช้

class ThisClass : IDisposable
{
    protected virtual Entities Context { get; set; }

    protected virtual void Dispose( bool disposing )
    {
        if ( disposing && Context != null )
            Context.Dispose();
    }

    public void Dispose()
    {
        Dispose( true );
    }

    public ThisClass()
    {
        Context = new Entities();
    }

    public List<Employee> GetAllEmployees()
    {
        return Context.Employees.ToList();
    }

    public List<Job> GetAllJobs()
    {
        return Context.Jobs.ToList();
    }

    public List<Task> GetAllTasksOfTheJob(Job job)
    {
        return Context.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

สิ่งนี้ทำให้คุณต้องการเพียง "การใช้" เมื่อสร้างอินสแตนซ์ของคลาสของคุณ หากคุณไม่ต้องการให้ชั้นเรียนมีความรับผิดชอบในการกำจัดวัตถุคุณสามารถทำให้วิธีการยอมรับการพึ่งพาเป็นอาร์กิวเมนต์:

public static List<Employee> GetAllEmployees( Entities entities )
{
    return entities.Employees.ToList();
}

public static List<Job> GetAllJobs( Entities entities )
{
    return entities.Jobs.ToList();
}

public static List<Task> GetAllTasksOfTheJob( Entities entities, Job job )
{
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

1

บิตที่ฉันโปรดปรานในเวทมนตร์ที่ไม่อาจคาดเดาได้!

public class Blah
{
  IEnumerable<T> Wrap(Func<Entities, IEnumerable<T>> act)
  {
    using(var entities = new Entities()) { return act(entities); }
  }

  public List<Employee> GetAllEmployees()
  {
    return Wrap(e => e.Employees.ToList());
  }

  public List<Job> GetAllJobs()
  {
    return Wrap(e => e.Jobs.ToList());
  }

  public List<Task> GetAllTasksOfTheJob(Job job)
  {
    return Wrap(e => e.Tasks.Where(x ....).ToList());
  }
}

Wrapมีอยู่เพื่อนามธรรมที่ออกหรือสิ่งที่คุณต้องการเวทมนตร์ ฉันไม่แน่ใจว่าฉันจะแนะนำสิ่งนี้ตลอดเวลา แต่เป็นไปได้ที่จะใช้ แนวคิด "ที่ดีกว่า" คือการใช้คอนเทนเนอร์ DI เช่น StructureMap และเพียงแค่กำหนดขอบเขตคลาสเอนทิตีกับบริบทคำร้องขอฉีดเข้าไปในตัวควบคุมแล้วปล่อยให้ดูแลวงจรชีวิตโดยไม่ต้องใช้ตัวควบคุมของคุณ


พารามิเตอร์ประเภทของคุณสำหรับ Func <IEnumerable <T>, Entities> อยู่ในลำดับที่ไม่ถูกต้อง (ดูคำตอบของฉันซึ่งมีเหมือนกันโดยทั่วไป)
Brook

ดีง่ายต่อการแก้ไข ฉันไม่เคยจำสิ่งที่ถูกต้องคือ ฉันใช้Funcพอฉันควร
เทรวิส
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.