การใช้ struct เพื่อบังคับใช้การตรวจสอบความถูกต้องของชนิดในตัว


9

วัตถุโดเมนทั่วไปมีคุณสมบัติที่สามารถแสดงโดยชนิดในตัว แต่มีค่าที่ถูกต้องเป็นส่วนย่อยของค่าที่อาจแสดงโดยประเภทนั้น

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

วิธีหนึ่งในการแก้ไขปัญหานี้คือการเก็บค่าเป็นแบบกำหนดเองstructซึ่งมีprivate readonlyเขตข้อมูลสำรองเดียวของชนิดในตัวและตัวสร้างที่ตรวจสอบความถูกต้องของค่าที่ให้มา จากนั้นเราสามารถมั่นใจได้ว่าใช้ค่าที่ตรวจสอบได้โดยใช้structประเภทนี้เสมอ

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

ยกตัวอย่างสถานการณ์ที่เราต้องการแสดงชื่อของวัตถุโดเมนและค่าที่ถูกต้องคือสตริงใด ๆ ที่มีความยาวระหว่าง 1 ถึง 255 อักขระ เราสามารถนำเสนอสิ่งนี้โดยใช้โครงสร้างต่อไปนี้:

public struct ValidatedName : IEquatable<ValidatedName>
{
    private readonly string _value;

    private ValidatedName(string name)
    {
        _value = name;
    }

    public static bool IsValid(string name)
    {
        return !String.IsNullOrEmpty(name) && name.Length <= 255;
    }

    public bool Equals(ValidatedName other)
    {
        return _value == other._value;
    }

    public override bool Equals(object obj)
    {
        if (obj is ValidatedName)
        {
            return Equals((ValidatedName)obj);
        }
        return false;
    }

    public static implicit operator string(ValidatedName x)
    {
        return x.ToString();
    }

    public static explicit operator ValidatedName(string x)
    {
        if (IsValid(x))
        {
            return new ValidatedName(x);
        }
        throw new InvalidCastException();
    }

    public static bool operator ==(ValidatedName x, ValidatedName y)
    {
        return x.Equals(y);
    }

    public static bool operator !=(ValidatedName x, ValidatedName y)
    {
        return !x.Equals(y);
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value;
    }
}

ตัวอย่างที่แสดงให้เห็นประstringโยนเป็นimplicitเช่นนี้ไม่สามารถล้มเหลว แต่ from- stringหล่อเป็นexplicitเช่นนี้จะโยนสำหรับค่าที่ไม่ถูกต้อง แต่แน่นอนทั้งสองอาจจะเป็นอย่างใดอย่างหนึ่งหรือimplicitexplicit

โปรดทราบด้วยว่าเราสามารถเริ่มต้นโครงสร้างนี้โดยใช้วิธีการร่ายstringเท่านั้น แต่สามารถทดสอบได้ว่าการร่ายนั้นจะล้มเหลวล่วงหน้าโดยใช้IsValid staticวิธีการนี้หรือไม่

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

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

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

NBตอนแรกฉันถามคำถามนี้เกี่ยวกับ Stack Overflowแต่มันถูกพักไว้เป็นหลักโดยอิงตามความคิดเห็น (ส่วนตัวที่เป็นตัวของตัวเอง) - หวังว่ามันจะสนุกกับความสำเร็จที่นี่

ด้านบนเป็นข้อความต้นฉบับด้านล่างคิดเพิ่มเติมอีกสองสามส่วนในการตอบสนองต่อคำตอบที่ได้รับก่อนที่จะถูกระงับ:

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

คุณสามารถสร้างเทมเพลตที่ใช้แลมบ์ดา (Bool) (T)
วงล้อประหลาด

คำตอบ:


4

นี่เป็นเรื่องธรรมดาในภาษาสไตล์ ML เช่น Standard ML / OCaml / F # / Haskell ที่สร้างประเภท wrapper ได้ง่ายกว่ามาก มันให้ประโยชน์สองประการแก่คุณ:

  • จะช่วยให้ชิ้นส่วนของรหัสในการบังคับใช้ว่าสตริงได้ผ่านการตรวจสอบโดยไม่ต้องดูแลการตรวจสอบที่ตัวเอง
  • จะช่วยให้คุณสามารถแปลรหัสตรวจสอบในที่เดียว หากValidatedNameเคยมีค่าที่ไม่ถูกต้องคุณรู้ว่าข้อผิดพลาดอยู่ในIsValidวิธีการ

หากคุณได้รับIsValidวิธีที่ถูกต้องคุณจะรับประกันได้ว่าฟังก์ชั่นใด ๆ ที่ได้รับValidatedNameนั้นแท้จริงแล้วได้รับชื่อที่ผ่านการตรวจสอบแล้ว

หากคุณต้องการทำการจัดการสตริงคุณสามารถเพิ่มวิธีการสาธารณะที่ยอมรับฟังก์ชั่นที่รับสตริง (ค่าของValidatedName) และส่งกลับสตริง (ค่าใหม่) และตรวจสอบผลลัพธ์ของการใช้ฟังก์ชัน ซึ่งจะช่วยกำจัดต้นแบบการรับค่าสตริงพื้นฐานและห่อใหม่

การใช้งานที่เกี่ยวข้องกับการห่อค่าคือการติดตามที่มาของพวกเขา เช่นบางครั้ง OS API ที่ใช้ C จะให้การจัดการทรัพยากรเป็นจำนวนเต็ม คุณสามารถตัด OS APIs เพื่อใช้Handleโครงสร้างแทนและให้การเข้าถึงตัวสร้างไปยังส่วนของรหัสเท่านั้น หากรหัสที่สร้างHandles นั้นถูกต้องจะใช้เฉพาะการจัดการที่ถูกต้องเท่านั้น


1

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

ดี :

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

ไม่ดี :

  • กลอุบายการหล่อถูกซ่อนอยู่ มันไม่ใช่สำนวน C # ดังนั้นอาจทำให้เกิดความสับสนเมื่ออ่านรหัส
  • มันพ่น การมีสตริงที่ไม่ตรงกับการตรวจสอบไม่ได้เป็นสถานการณ์ที่พิเศษ การทำIsValidก่อนที่นักแสดงจะไม่ได้ประโยชน์
  • ไม่สามารถบอกคุณได้ว่าทำไมบางอย่างไม่ถูกต้อง
  • ค่าเริ่มต้นValidatedStringไม่ถูกต้อง / ตรวจสอบความถูกต้อง

ฉันเห็นสิ่งนี้บ่อยขึ้นUserและAuthenticatedUserเรียงลำดับมากขึ้นเมื่อวัตถุเปลี่ยนไป มันอาจเป็นวิธีที่ดีแม้ว่ามันจะดูไม่เหมาะสมใน C #


1
ขอขอบคุณฉันคิดว่า "ข้อผิดพลาด" ข้อที่สี่ของคุณเป็นข้อโต้แย้งที่น่าสนใจที่สุดโดยการใช้ค่าเริ่มต้นหรืออาร์เรย์ประเภทที่ให้ค่าที่ไม่ถูกต้อง (ขึ้นอยู่กับว่าสตริง zero / null เป็นค่าที่ถูกต้อง) นี่คือ (ฉันคิดว่า) สองวิธีเท่านั้นที่จะจบลงด้วยค่าที่ไม่ถูกต้อง แต่ถ้าเราไม่ใช้รูปแบบนี้สองสิ่งนี้จะยังคงให้ค่าที่ไม่ถูกต้อง แต่ฉันคิดว่าอย่างน้อยเราก็จะรู้ว่าพวกเขาจำเป็นต้องได้รับการตรวจสอบ ดังนั้นสิ่งนี้อาจทำให้วิธีการที่ค่าเริ่มต้นของประเภทต้นแบบไม่ถูกต้องสำหรับประเภทของเรา
gmoody1979

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

@Doval ฉันเห็นด้วยยกเว้นตามที่ระบุไว้ในความคิดเห็นอื่นของฉัน ประเด็นทั้งหมดของรูปแบบคือการรู้แน่ชัดว่าถ้าเรามี ValidatedName มันต้องถูกต้อง สิ่งนี้จะพังลงถ้าค่าเริ่มต้นของประเภทพื้นฐานนั้นไม่ได้เป็นค่าที่ถูกต้องของประเภทโดเมน นี่คือการขึ้นอยู่กับโดเมนแน่นอน แต่มีแนวโน้มที่จะเป็นกรณี (ฉันคิดว่า) สำหรับประเภทสตริงตามกว่าสำหรับประเภทตัวเลข รูปแบบทำงานได้ดีที่สุดเมื่อค่าเริ่มต้นของประเภทพื้นฐานเหมาะกับค่าเริ่มต้นของประเภทโดเมน
gmoody1979

@Doval - ฉันเห็นด้วยโดยทั่วไป แนวความคิดนั้นดี แต่พยายามอย่างมีประสิทธิภาพในการปรับแต่งประเภทรองเท้าให้เป็นภาษาที่ไม่รองรับ จะมีปัญหาการใช้งานอยู่เสมอ
Telastyn

ต้องบอกว่าฉันคิดว่าคุณสามารถตรวจสอบค่าเริ่มต้นในการโยน "ขาออก" และในสถานที่ที่จำเป็นอื่น ๆ ภายในวิธีการของ struct และโยนถ้าไม่ได้เริ่มต้น แต่ที่เริ่มยุ่ง
gmoody1979

0

วิธีการของคุณค่อนข้างหนักและเข้มข้น ฉันมักจะกำหนดเอนทิตีโดเมนเช่น:

public class Institution
{
    private Institution() { }

    public Institution(int organizationId, string name)
    {
        OrganizationId = organizationId;            
        Name = name;
        ReplicationKey = Guid.NewGuid();

        new InstitutionValidator().ValidateAndThrow(this);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }        
    public virtual ICollection<Department> Departments { get; private set; }

    ... other properties    

    public Department AddDepartment(string name)
    {
        var department = new Department(Id, name);
        if (Departments == null) Departments = new List<Department>();
        Departments.Add(department);            
        return department;
    }

    ... other domain operations
}

ในตัวสร้างของเอนทิตีการตรวจสอบจะถูกทริกเกอร์โดยใช้ FluentValidation.NET เพื่อให้แน่ใจว่าคุณไม่สามารถสร้างเอนทิตีที่มีสถานะไม่ถูกต้อง โปรดทราบว่าคุณสมบัติทั้งหมดเป็นแบบอ่านอย่างเดียว - คุณสามารถตั้งค่าได้ผ่านตัวสร้างหรือการดำเนินงานโดเมนเฉพาะ

การตรวจสอบความถูกต้องของเอนทิตีนี้เป็นคลาสแยกต่างหาก:

public class InstitutionValidator : AbstractValidator<Institution>
{
    public InstitutionValidator()
    {
        RuleFor(institution => institution.Name).NotNull().Length(1, 100).WithLocalizedName(() =>   Prim.Mgp.Infrastructure.Resources.GlobalResources.InstitutionName);       
        RuleFor(institution => institution.OrganizationId).GreaterThan(0);
        RuleFor(institution => institution.ReplicationKey).NotNull().NotEqual(Guid.Empty);
    }  
}

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


ผู้ลงคะแนนเสียงสนใจจะอธิบายหรือไม่ว่าทำไมคำตอบของฉันจึงถูกลดระดับลง
L-Four

คำถามเกี่ยวกับโครงสร้างเพื่อ จำกัด ประเภทของค่าและคุณเปลี่ยนเป็นคลาสโดยไม่ต้องอธิบายว่าทำไม (ไม่ใช่ผู้ลงคะแนนเพียงทำข้อเสนอแนะ)
DougM

ฉันอธิบายว่าทำไมฉันถึงพบทางเลือกที่ดีกว่านี้และนี่เป็นหนึ่งในคำถามของเขา ขอบคุณสำหรับการตอบกลับ.
L-Four

0

ฉันชอบวิธีการนี้กับประเภทของค่า แนวคิดนี้ยอดเยี่ยม แต่ฉันมีคำแนะนำ / บ่นเกี่ยวกับการใช้งาน

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

ToString : ฉันชอบใช้ToStringoverload เพียงเพื่อจุดประสงค์ในการดีบั๊ก และฉันไม่คิดว่าจะคืนค่าดิบสำหรับมันเป็นความคิดที่ดี นี่เป็นปัญหาเดียวกับการแปลงเป็นสตริงโดยนัย การรับค่าภายในควรเป็นการดำเนินการที่ชัดเจน ฉันเชื่อว่าคุณกำลังพยายามทำให้โครงสร้างทำงานเป็นสตริงปกติไปยังโค้ดภายนอก แต่ฉันคิดว่าการทำเช่นนั้นคุณกำลังสูญเสียคุณค่าบางส่วนที่คุณได้รับจากการนำประเภทนี้มาใช้

เท่ากับและGetHashCode : โครงสร้างกำลังใช้ความเท่าเทียมกันของโครงสร้างโดยค่าเริ่มต้น ดังนั้นEqualsและคุณGetHashCodeกำลังทำซ้ำพฤติกรรมเริ่มต้นนี้ คุณสามารถลบมันออกได้และมันก็จะเป็นสิ่งเดียวกัน


การส่งสัญญาณ: ความหมายนี้ให้ความรู้สึกกับฉันมากกว่าการแปลงสตริงเป็นชื่อ ValidatedName แทนที่จะสร้างชื่อ ValidatedName ใหม่: เรากำลังระบุสตริงที่มีอยู่ว่าเป็น ValidatedName ดังนั้นสำหรับฉันนักแสดงดูเหมือนถูกต้องมากขึ้นความหมาย เห็นด้วยมีความแตกต่างเล็กน้อยในการพิมพ์ (ของนิ้วบนแป้นพิมพ์หลากหลาย) ฉันไม่เห็นด้วยกับ to-string cast: ValidatedName เป็นส่วนย่อยของสตริงดังนั้นจะไม่มีการสูญเสียความแม่นยำ ...
gmoody1979

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

เท่ากับและ GetHashCode: ใช่โครงสร้างใช้ความเท่าเทียมกันของโครงสร้าง แต่ในอินสแตนซ์นี้ที่เปรียบเทียบการอ้างอิงสตริงไม่ใช่ค่าของสตริง ดังนั้นเราจึงจำเป็นต้องแทนที่ Equals ฉันยอมรับว่าสิ่งนี้จะไม่จำเป็นหากประเภทพื้นฐานเป็นประเภทค่า จากความเข้าใจของฉันเกี่ยวกับการใช้งาน GetHashCode ที่เป็นค่าเริ่มต้นสำหรับประเภทค่า (ซึ่งค่อนข้าง จำกัด ) สิ่งนี้จะให้ค่าเดียวกัน แต่จะมีประสิทธิภาพมากกว่า ฉันควรทดสอบว่าเป็นกรณีจริงหรือไม่ แต่เป็นประเด็นปัญหาเล็กน้อยไปยังประเด็นหลักของคำถาม ขอบคุณสำหรับคำตอบโดย :-)
gmoody1979

@ gmoody1979 มีการเปรียบเทียบโครงสร้างโดยใช้เท่ากับในทุกฟิลด์โดยค่าเริ่มต้น ไม่ควรมีปัญหากับสตริง เหมือนกับ GetHashCode สำหรับโครงสร้างที่เป็นส่วนย่อยของสตริง ฉันชอบคิดว่าเป็นตาข่ายนิรภัย ฉันไม่ต้องการทำงานกับ ValidatedName แล้วตั้งใจแอบใช้สตริง ฉันต้องการถ้าคอมไพเลอร์ทำให้ฉันระบุอย่างชัดเจนว่าตอนนี้ฉันต้องการทำงานกับข้อมูลที่ไม่ได้ตรวจสอบ
ร่าเริง

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