首页 > 其他分享 >第4章 类型设计准则

第4章 类型设计准则

时间:2025-01-09 22:46:48浏览次数:1  
标签:DO ... 准则 接口 枚举 类型 设计 public

第4章 类型设计准则

  • DO​:类应该由一组定义明确、相互关联的成员组成。

    一个类,如果能用一句话描述清楚它的用途,那么它的设计是优秀的。

1 类型(class、struct)和 namespace

  • DO​:namespace 用于组织类,通过 namespace 将相关功能按层次铺开,但不要有过深的层次、过多的数量。

    要做到让开发人员很容易地浏览框架并找到想要的 API,过深的层次、过多的数量都不便于开发者浏览。

    namespace 的主要目的不是解决同名类型的名字冲突,而是为了把类型组织成一个有条理的、易于浏览、易于理解的层次结构。
    如果单个框架中出现类型名冲突,意味着框架设计得很烂。名字相同的类型要么应该合并起来,要么重命名以改进代码的可读性和可搜索性。

  • AVOID​:高级方案的 API 应和常见方案的 API 分隔开,放在不同的 namespace 中。

    用户能更容易理解框架基本概念,也更容易在常见场景中使用框架。

    高级的类应该放在简单类所在 namespace 的子 namespace 中,如:System.Mail​ 存放着简单类,System.Mail.Advanced​ 存放着高级类。

  • DON'T​:定义类型前要先指定类型的 namespace。

    不要每个类都使用默认的命名空间,这有助于可能存在的类型名冲突。不过,这不意味着可以引入这一的冲突,具体原因第 3 章已经讲过。

标准子 namespace 的命名

很少使用的类型应放在子 namespace 中,以下是我们过去的命名经验,可以参考

.Design 子命名空间

  • DO​:为基本类型提供设计功能的类型应放在. Design 后缀的 namespace 中。

    System.Windows.Forms.Design
    System.Messageing.Design
    System.Diagnostics.Design
    

.Permissions 子名字空间

  • DO​:权限类型应放在. Permissions 后缀的 namespace 中。

.Interop 子名字空间

  • DO​:为基本 namespace 提供互操作功能的类型应放在 . Interop 后缀的 namespace 中。

  • DO​:所有位于主互操作程序集中的代码应放在 . Interop 后缀的 namespace 中。

2 class 和 struct 之间的选择

  • CONSIDER​:一个类型,如果它实例较 且生命周期较 ,或经常被 内嵌 在其他对象中(尤其是 数组 ),可以使用结构体。

  • AVOID​:尽可能少使用结构体,使用结构体应遵循如下原则:

    • 它在逻辑上表示单个值,类似于基础类型(int、double 等)。
    • 它的实例大小小于 24 byte。
    • 它是不可变的。
    • 它不会被频繁装箱。

    除此以外,所有类型应该被定义为类。

3 class 和 interface 之间的选择

  • DO​:优先使用 ,而非 接口

    基于类的 API 更容易被改进,它可以在不破坏已有代码的情况下向类中添加成员。

  • DO​:要使用 抽象类 来分离协议与实现,而非 接口

    抽象类经过正确的设计,同样能够解除契约与实现之间的耦合,与接口达到的效果不相上下。

    当然,这不是说接口一无是处。如果契约不会随着时间改变,接口就非常合适;如果为一个族类型定义公共基类,抽象类更好。

  • DO​:如果结构体(struct)需要实现多态,应使用 接口

    值类型不能继承,但可以实现接口。如 IComparable​​、IFormattable​​、IConvertible​​:

    public struct Int32 : IComparable, IFormattable, IConvertible { ... }
    public struct Int64 : IComparable, IFormattable, IConvertible { ... }
    

定义良好的接口的标志:

  1. 它不会限定于类型,更像一个类型的“ 属性 ”;
  2. 每个接口只做一件事。

  • CONSIDER​:可以通过接口实现多重继承的效果。

    System.Drawing.Image ​实现了 System.IDisposable ​和 System.ICloneable ​接口,因此它是可以处置的(disposable)、可复制的(clonable),还继承了 MarshalByRefObject​:

    public class Image : MarshalByRefObject, IDisposable, ICloneable { ... }
    

4 abstract class 的设计

  • DON'T​​:不要在抽象类中定义访问类型为 public ​、 protected-internal ​的构造函数。

    抽象类禁止进行实例化,因此不应该为抽象类定义访问类型为 public、protected-internal 的构造函数。这么做不光是错误的,还会误导用户。

    // 坏设计
    public abstract class Claim {
        public Claim() { ... }
    }
    
    // 好设计
    public abstract class Claim {
        protected Claim() { ... }
    }
    

  • DO​​:抽象类应定义访问类型为 protectedinternal 的构造函数。

    protected 构造函数更常见,可以做到仅允许子类调用它。

    public abstract class Claim {
        protected Claim() { ... }
    }
    

    internal 构造函数,可以做到仅程序集内的子类调用它,我们可以在该构造函数中反向操作子类,以实现一些特殊的设计模式,构造函数又不会因其他程序集实现该抽象类而遭到滥用。

    public abstract class Claim {
        internal Claim() { ... }
    }
    

    Tips

    即使我们未定义构造函数,CLR 也会为我们隐式创建一个 protected 无参构造函数。

  • DO​:public​/protected ​抽象类应至少有一个实现。

    这有助于验证抽象类的设计是否正确。如:System.IO.FileStream ​是 System.IO.Stream ​抽象类的一个实现。

5 static class 的设计

  • DO​:少用静态类。

    静态类应作为辅助类,用于辅助框架的面向对象的核心。

  • DON'T​:不要把静态类当作杂物桶。

    每个静态类都应该有其明确的角色划分。如果你对类的描述包含了“和”这样的连词,或包含一个全新的句子,说明你需要另外一个类。

6 interface 的设计

  • DO​​:如果值类型需要支持一些公共 API,要通过 接口 实现。

    int ​实现了 IComparable ​接口。

  • CONSIDER​​:需要多重继承时,可以通过 接口 实现。

  • AVOID==: 避免==使用记号接口(没有成员的接口)。

    最好使用自定义 Attribute​​而非记号接口:

    // 避免
    public interface IImmutable {} // 记号接口
    
    public class Key : IImmutable { ... }
    
    // 考虑
    [IMutable]
    public class Key { ... }
    

    使用 Attribute 也有缺点:

    1. 开销更大;
    2. 编译时无法发现 class 是否标注了 Attribute。

    如果我们需要编译时检查,则可以使用记号接口:

    public interface ITestSerializable {} // 记号接口
    public void Serialize (ITextSerializable item){
        // 通过反射序列号公有属性
    }
    

  • DO​:接口应至少有一个实现,有一个调用(一个以该接口为参数的方法,或一个该类型的属性)

    这有助于验证接口的设计和实现。

  • DON'T ​​: 禁止 给已发行的接口再添加成员。

    这样会破坏接口的实现。为避免版本问题,应该创建一个新的接口。

7 struct 的设计

  • DON'T ​​: 不要 为结构体提供默认构造函数。

    否则会造成 default(SomeStruct)​ 和 new SomeStruct()​ 行为不同。

    附注:.NET Framework 中 C# 禁止自定义默认构造函数

  • DO​​:要定义不可变的值类型,并使用 readonly 修饰符声明。

    可变值类型在传值时会 传递副本(副本隐式创建) ,使用者可能意识不到他们修改的是副本,而非源值。

    readonly 修饰符在某些操作上能够避免 防御性拷贝

    // 坏设计
    public struct ZipCode {
        public int FiveDigitCode { get; set; }    // get、set都有
        public int PludsFourExtension { get; set; }
    }
    
    // 好设计
    public readonly struct ZipCode {
        public ZipCode(int fiveDigitCode, int plusFourExtension) { ... }
        public ZipCode(int fiveDigitCode) : this(fiveDigitCode, 0) { }
    
        public int FiveDigitCode { get; }    // 只有get
        public int PludsFourExtension { get; }
    
        public override string ToString() {
            ...
        }
    }
    
    public partial class Other{
        private readonly ZipCode _zipCode;
        ...
        private void Work(){
            // 因ZipCode是readonly struct,调用ToString()不会带来防御性拷贝
            string zip = _zipCode.ToString();
        }
    }
    

  • DO​​:对于可变值类型,声明不可变方法。

    调用值类型的方法时,可以避免 防御性拷贝

    // 可变值类型
    public struct ZipCode {
        private int _plusFour;
        public int FiveDigitCode { get; set; }
        // 显式只读,调用get时得到的不再是拷贝
        public int PludsFourExtension {
            readonly get => _plusFour;
            set { ... }
        }
        // 不可变方法,调用它时不再进行拷贝
        public override readonly string ToString() { ... }
    }
    

    使用 readonly​ 修饰 struct​ 时,C# 编译器会自动将 readonly​ 应用到每个方法和属性的 get​ 方法。

  • DO​​​:当结构实例为 默认 值(如 0、false、null)时,结构仍处于有效状态

    // 坏设计
    public struct PositiveInteger {
        int value;
    
        public PositiveInteger(int value) {
            if (value <= 0) throw new ArgumentException(...);
            this.value = value;
        }
    
        public override string ToString() {
            return value.ToString();
        }
    }
    
    // 好设计
    public struct PositiveInteger {
        int value;    // 逻辑值是 value+1
    
        public PositiveInteger(int value) {
            if (value <= 0) throw new ArgumentException(...);
            this.value = value - 1;
        }
    
        public override string ToString() {
            return (value+1).ToString();
        }
    }
    

  • DON'T​​​:不要定义类似于 ref struct​ 类型的值类型,除非是在性能至关重要的特定底层使用场景中。

    ref struct​ 只允许存在于栈中,不能被装箱至堆中。因此,它不能被作为其他类型中字段的类型使用(除非该类型也是 ref struct​),也不能用于 async 声明的异步方法中

  • DO​​:值类型需要实现 IEquatable<T> ​​。

    IEquatable<T>​ 可以避免以下问题:

    1. 值类型的 Object.Equals ​方法会导致装箱,
    2. Object.Equals ​使用了反射,它的默认实现效率不高。

8 枚举的设计

  • DO​​​:要使用 枚举 来加强那些表示值的集合的参数、属性以及返回值的类型。

  • DO​​:要使用枚举代替静态常量。

    // 避免
    public static class Color {
        public static const int Red      = 0;
        public static const int Green    = 1;
        public static const int Blue     = 2;
        ...
    }
    
    // 推荐
    public enum Color {
        Red,
        Green,
        Blue,
        ...
    }
    

  • DON'T​​:不要使用枚举来定义开放集合。

    比如操作系统的版本、朋友的名字等。

  • DON'T​​:不要设保留值。

    保留值只会污染实际值的集合,还会误导用户。

    public enum DeskType{
        Circular,
        Oblong,
        Rectangular,
    
        // 以下保留值不应该存在
        ReservedForFutureUse1,
        ReservedForFutureUse2,
    }
    

  • AVOID​​:避免创建只有一个值的枚举。

    我们可以用方法重载在日后添加参数,而非用单值枚举做占位.

    // 坏设计
    public enum SomeOption {
        DefaultOption
        // 我们日后将添加其他值
    }
    ...
    
    // option不是必须的。未来任何时候都可以通过重载实现该方法
    public void SomeMethod(SomeOption option) {
        ...
    }
    

  • DON'T​:禁止在枚举中包含 sentinel(哨兵)值

    sentinel 值用来跟踪枚举的状态,却不属于枚举所表示值的集合。

    // 坏做法
    public enum DeskType{
        Circular     = 1,
        Oblong       = 2,
        Rectangular  = 3,
    
        LastValue    = 3,  // 不应包含sentinel值
    }
    
    public void OrderDesk(DeskType desk){
        if (desk > DeskType.LastValue) 
            throw new ArgumentOutOfRangeException(...);
        ...
    }
    
    // 好做法
    public void OrderDesk(DeskType desk){
        if (desk > DeskType.Rectangular || desk < DeskType.Circular) 
            throw new ArgumentOutOfRangeException(...);
        ...
    }
    

  • DO​:要为简单枚举提供一个零值。

    应该把枚举中 最常用的默认值 赋值为零。

    public enum Compression {
        None = 0,
        GZip,
        Deflate,
    }
    public enum EventType {
        Error = 0,
        Warning,
        Information,
        ...
    }
    

  • CONSIDER​​:以 Int32​ 为载体实现枚举。

    例外:

    • 该枚举是标记枚举,且超过 32 个标记,或预计今后会有更多的标记。

    • 需要与非托管代码进行交互,且非托管代码使用非 Int32 的枚举(即非 4 字节)。

    • 为了节省内存:

      • 枚举用作 struct 或 class 的字段,且会被频繁实例化;
      • 枚举用于创建大型数组或集合;
      • 枚举的大量实例用于序列化。

  • DO​:标记枚举用复数名词命名;简单枚举用单数名词命名。

1 标记枚举的设计

  • DO​​:标记枚举应使用 System.FlagsAttribute ​​标记。

    [Flags]
    public enum AttributeTargets {
        ...
    }
    

  • DO​​:用 2 的幂次方 作为标记枚举的值。

    这样可以使用位操作符自由的组合。

    [Flags]
    public enum WatcherChangeTypes {
        None = 0,
        Created = 0x0002,
        Deleted = 0x0004,
        Changed = 0x0008,
        Renamed = 0x0010,
    }
    

    我们还可以这样做:

    [Flags]
    public enum WatcherChangeTypes {
        None = 0,
        Created = 1 << 1,
        Deleted = 1 << 2,
        Changed = 1 << 3,
        Renamed = 1 << 4,
    }
    

  • CONSIDER​​:为常用的组合标记提供特殊的枚举值。

    [Flags]
    public enum FileAccess {
        Read = 1,
        Write = 2,
        ReadWrite = Read | Write,
    }
    

  • AVOID​​:标记枚举不应包含某些无效组合。

    System.Reflection.BindingFlags​ 枚举就是这种错误设计的例子。该枚举试图表示许多不同的概念,如可见性、静态性、成员类型等:

    // 原设计
    [Flags]
    public enum BindingFlags {
        Default = 0,
    
        Instance = 0x4,
        Static = 0x8,
    
        Public = 0x10,
        NonPublic = 0x20,
        CreateInstance = 0x200,
        GetField = 0x400,
        SetField = 0x800,
        GetProperty = 0x1000,
        SetProperty = 0x2000,
        InvokeMethod = 0x100,
        ...
    }
    

    其中一些枚举值的组合是无效的,如 Type.GetMembers ​方法以该枚举为参数,但必须指定 BindingFlags.Instance ​或 BindingFlags.Static​。

    好的做法是将该枚举值分成两个或更多个枚举或其他类型:

    // 好设计
    [Flags]
    public enum Visibilities {
        None = 0,
        Public = 0x10,
        NonPublic = 0x20,
    }
    
    [Flags]
    public enum MemberScopes {
        None = 0,
        Instance = 0x4,
        Static = 0x8,
    }
    
    [Flags]
    public enum MemberKinds{
        None = 0,
        Constructor = 1 << 0,
        Field = 1 << 1,
        PropertyGetter = 1 << 2,
        PropertySetter = 1 << 3,
        Method = 1 << 4,
    }
    
    public class Type {
        public MemberInfo[] GetMembers(MemberKinds members, 
                                      Visibilities visibility, 
                                      MemberScopes scope);)
    }
    

  • AVOID​​: 0 不能作为标记枚举的值。(表示“清除标记”除外)

  • DO​​:标记枚举的 0 值应命名为 None ,该值必须始终表示“ 所有标记均被清除 ”.

    [Flags]
    public enum BorderStyle {
        Fixed3D        = 0x1,
        FixedSingle    = 0x2,
        None           = 0x0
    }
    

    0 值之所以特殊,是因为不进行赋值操作时,枚举成员默认为 0。因此在设计时要考虑到这一点。特别是,0 值应该是以下两者之一:

    • 常用的默认值
    • 表示错误的值,且 API 会检查这个错误值

2 给枚举添加值

  • CONSIDER​:建议为枚举添加新值,尽管要冒一点兼容性风险。

    如果添加新值会导致应用程序不兼容,则可以考虑添加一个新的 API 来返回新老枚举值,同时要求用户停止使用老 API(仍返回老枚举值)。

9 嵌套类型

  • DO​:当嵌套类型与其外部类型之间的关系需要 成员可访问性语义 时,才使用嵌套类型。

  • DON'T​​:不要使用嵌套类型进行逻辑分组,应使用 namespace 分组。

  • AVOID​​:避免公开暴漏嵌套类型。

    唯一的例外是:在极少数情况下,比如在子类化或其他高级的自定义场景下,需要声明嵌套类型的变量。

  • DON'T​:如果一个类不仅外层类会用,其他类也会使用,则不应该定义为嵌套类型。

  • DON'T​​:不要将嵌套类型定义为 接口 的成员。

    许多语言不支持这种构造模式。

10 类型和程序集元数据

  • DO​:在包含公共类型的程序集中使用 CLSCompliant(true) ​ 特性。

    该特性用于声明该程序集中的类型是符合 CLS 规范的,可以为所有.NET 编程语言所使用。

    [assembly:ClSCompliant(true)]
    

  • DO​:在包含 公共 类型的程序集中使用 AssemblyVersionAttribute​ 特性。

    [assembly:AssemblyVersion(...)]
    

  • DO​​:要将下列信息特性应用到程序集中。

    Visual Studio 会识别这些信息,告知用户程序集内容:

    [assembly:AssemblyTitle("System.Core.dll")]
    [assembly:AssemblyCompany("Microsoft Corporation")]
    [assembly:AssemblyProduct("Microsoft .NET Framework")]
    [assembly:AssemblyDescription(...)]
    

  • CONSIDER​​:在程序集版号中使用 的格式。

    V:主版本号

    S:服务版本号

    B:构建号

    R:构建修订号

    [assembly:AssmeblyVersion("3.5.21022.8")]
    

    附注:现如今更常用的版本控制是语义化版本 2.0.0 | Semantic Versioning (semver.org)

  • CONSIDER​:使用 ComVisible(false) ​ 标注不允许 COM 调用。

    可供 COM 调用的 API 需要特别设计,不应该将.NET 程序集暴漏给 COM。如果 COM 确实需要调用该 API,可以在该 API 中或整个程序集中使用 ComVisible(true)​​。

  • CONSIDER​:在程序集中使用 AssemblyFileVersionAttribute​ 和 AssemblyCopyrightAttribute​。

11 强类型字符串

  • CONSIDER​​​:当基类所支持的一组固定输入参数(枚举),不能满足 派生 类需要的参数时,建议使用强类型字符串。

  • DO​​​:要将强类型字符串声明成带有 字符串 构造函数的不可变 类型( readonly struct )。

    强类型字符串要遵循 不可变值 类型的准则

  • DO:要允许 构造函数接收空白输入。

强类型字符串是 struct,而 struct 可以零初始化(即使用默认构造函数初始化)。构造函数接收空白输入应等效于零初始化。

  • DO​​​:对于已知选项,应通过 static readonly 属性 声明至该类型中。

    这样可以提供类似于枚举的 InteliSense 体验。

  • DO​​​:要覆写 ToString() 方法返回隐含的字符串值。

    便于调试。

  • CONSIDER​​​:建议通过一个 只读 属性来暴露强类型字符串所隐含的字符串值。

    因为 ToString()​ 是为调试服务的,如果开发者确实需要知道该值,使用属性获取更好。属性名没有惯例/准则,可以使用“ Value ”作为属性名。

    如果开发者很少或从来不用该属性,不定义属性会让这个类看起来 更像枚举

  • DO​​​:要覆写 相等 运算符。

    通过覆写 operator == ​​​,可以让强类型字符串看起来像 字符串枚举 。通常,强类型字符串应利用字符串相等性来比较相等,如果强调不区分大小写,则可以通过覆写 相等 运算实现。

  • AVOID​​​​:避免提供强类型字符串和 System.String ​的重载。

    这种重载虽然便于调用(省去 new 一个对象的功夫),但会让新手开发者困惑。

建议:如果在原有 API 上新定义了强类型字符串,用于辅助有效输入,可以考虑将原先基于 System.String​ 的重载方法标记为 [EditorBrowsable(EditorBrowsableState.Advanced)] ​、 [EditorBrowsable(EditorBrowsableState.Never)] ​或 [Obsolete]

标签:DO,...,准则,接口,枚举,类型,设计,public
From: https://www.cnblogs.com/hihaojie/p/18663015/chapter-4-type-design-guidelines-jyary

相关文章

  • 第5章 成员设计
    第5章成员设计1成员设计的通用规范1成员重载形参要求:​DO​:在较长的重载中,应使用描述性的参数名来说明较短的重载使用的默认值。以如下代码举例,ignoreCase暗示“短重载大小写敏感”,如果参数名改为caseSensitive,将不会隐含该含义。publicclassType{publicM......
  • 【开源】基于SpringBoot框架公司日常考勤系统(计算机毕业设计)+万字毕业论文 T134
    系统合集跳转源码获取链接一、系统环境运行环境:最好是javajdk1.8,我们在这个平台上运行的。其他版本理论上也可以。IDE环境:Eclipse,Myeclipse,IDEA或者SpringToolSuite都可以tomcat环境:Tomcat7.x,8.x,9.x版本均可操作系统环境:WindowsXP/7/8//8.1/10/11或者L......
  • JSP考试系统的设计与实现qih9c(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景与意义随着信息技术的飞速发展,教育领域正经历着深刻的变革。传统的考试方式已难以满足现代教育的需求,因此,设计并实现一套高效、智能的......
  • JSP考勤系统设计与实现4jhkn(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、项目背景与意义随着企业规模的扩大和管理的精细化,传统的考勤方式已难以满足现代企业的需求。因此,设计并实现一套高效、智能的考勤系统显得尤为......
  • 设计模式之简单工厂模式
    设计模式之简单工厂模式(SimpleFactoryPattern)模式定义简单工厂模式又叫静态工厂方法模式,属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。类图代码enumProd......
  • 设计模式之工厂方法模式
    设计模式之工厂方法模式(FactoryMethodPattern)模式定义工厂方法模式(FactoryMethodPattern)又称为工厂模式,也叫虚拟构造器(VirtualConstructor)模式或者多态工厂(PolymorphicFactory)模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工......
  • 设计模式之抽象工厂模式
    设计模式之抽象工厂模式(AbstractFactoryPattern)模式定义抽象工厂模式(AbstractFactoryPattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。类图代码//抽象产品类AclassAbstractProductA{pub......
  • 设计模式之观察者模式
    设计模式之观察者模式(ObserverPattern)模式定义观察者模式(ObserverPattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Sourc......
  • python 代码实现了一个高级的 Coppersmith 攻击,用于对特定类型的 RSA 加密系统进行攻
    importmathimportnumpyasnpfromtypingimportList,Tuple,Optionalimportloggingfrommathimportgcd#导入gcd函数try:fromsage.allimport(Matrix,QQ,PolynomialRing,vector,next_prime,randint,ZZ)exceptImport......
  • flask框架硕士研究生院校报考推荐系统的设计与实现毕设源码+论文
    校园二手货物交易平台m1a2o本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、选题背景关于硕士研究生院校报考推荐系统的研究,现有研究主要集中在研究生教育的宏观层面,如招生政策、教育质量评估......