title: 风格指南

风格指南

这是CatLib特有的代码风格指南,如果您在您的项目中使用CatLib,为了避免错误,降低沟通成本,小纠结和反模式,阅读本指南是一份不错的选择。

我们不能保证风格指南中的所有内容,对于所有工程和团队都是理想的,所以根据项目环境,周围技术环境,风格出现偏差是可行的。

我们应该尽可能的遵守本风格指南提出的建议。

根据周围技术堆栈对于命名规范相关我们建议您阅读微软提供的:框架设计指南

优先级定义

(A)必须

这些规范会帮你规避错误,您必须准守这些规范。这里可以存在例外,但是应该非常少见,只有您非常熟悉c#和周围技术栈且具备充足理由的情况下可以进行例外。

(B)强烈建议

这些规范能够在绝大多数项目中改善可读性和程序优雅度。即使你违反了,代码还是能照常运行,可以存在例外,但例外应该尽可能少且有合理的理由。

(C)建议

在这些规则里,我们提出一个默认的建议,如果理由充分,你可以随意在你的代码库中做出不同的选择。

(D)不建议

这些规则会导致代码变得难以维护甚至出现bug。这些规则列出了存在的潜在技术风险,并说明了它们什么时候不应该被使用。

(A)项目名始终作为命名空间的开头

命名空间的第一个片段为项目的名字。这样做可以避免不同的第三方服务提供者提供的类发生冲突。

错误的例子

  1. namespace FileSystem
  2. {
  3. public class FileSystem
  4. {
  5. }
  6. }

正确的例子

  1. namespace CatLib.FileSystem
  2. {
  3. public class FileSystem
  4. {
  5. }
  6. }

(A)服务的命名空间必须和服务的父级文件夹名一致

所有的服务(由多个类组成)命名空间必须和其父文件夹名字保持一致(对于根文件夹可以不包含在命名空间中,如:Providers),以避免通过目录检索时无法明确服务位置,同时可以避免现在以及未来的类名相冲突。

错误的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
  1. namespace CatLib.IO
  2. {
  3. public class AdapterLocal
  4. {
  5. }
  6. }
  • Providers/CatLib.FileSystem/OSS/AdapterAliyunOSS.cs
  1. namespace CatLib.IO.OSS
  2. {
  3. public class AdapterAliyunOSS
  4. {
  5. }
  6. }

正确的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
  1. namespace CatLib.FileSystem
  2. {
  3. public class AdapterLocal
  4. {
  5. }
  6. }
  • Providers/CatLib.FileSystem/OSS/AdapterAliyunOSS.cs
  1. namespace CatLib.FileSystem.OSS
  2. {
  3. public class AdapterAliyunOSS
  4. {
  5. }
  6. }

(A)文件名必须与类名一致

所有的类名必须和文件名一致,以避免通过目录来检索类时出现不一致的问题(这意味着我们不能将两个类写在同一个文件中,除非它是内部类)。

错误的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
  1. public class Local
  2. {
  3. }

正确的例子

  • Providers/CatLib.FileSystem/AdapterLocal.cs
  1. public class AdapterLocal
  2. {
  3. }

(A)内部类的访问级别不能高于protected

所有的内部类,不允许将访问级别设定为publicinternal,因为如果内部类可以被外部访问,将会出现不可预测的问题。

错误的例子

  1. public class FileSystem
  2. {
  3. public class Disk
  4. {
  5. }
  6. }

正确的例子

  1. public class FileSystem
  2. {
  3. private class Disk
  4. {
  5. }
  6. }

(A)对待编译器警告视同错误

所有的编译器警告都应该被处理,忽略编译器警告可能会导致一些隐藏的bug。

错误的例子

  1. int a = 0;
  2. if(a == null) // 引发一个编译器警告
  3. {
  4. }

正确的例子

  1. int a = 0;
  2. if(a == 0)
  3. {
  4. }

示例中会导致完全不同的两种结果。

(A)对外服务的接口放在API命名空间下

对外服务的接口放在API命名空间下,这样在IED using的时候可以避免错误的使用实现代码而发生耦合。

一般来说我们放置在:项目名.API.组件名的命名空间下

错误的例子

  • Providers/CatLib.FileSystem/API/IFileSystem.cs
  1. namespace CatLib.FileSystem
  2. {
  3. public interface IFileSystem
  4. {
  5. }
  6. }

正确的例子

  • Providers/API/CatLib.FileSystem/IFileSystem.cs
  1. namespace CatLib.API.FileSystem
  2. {
  3. public interface IFileSystem
  4. {
  5. }
  6. }

(B)完整单词的类名

类名应该倾向于完整单词,而不是单词的缩写。

代码编辑器的自动补全功能已经让书写长类名的成本变得非常的低,而其带来的的明确性是非常宝贵的,我们不应该使用缩写来代替长类名。

错误的例子

  1. Providers/
  2. CatLib.FileSystem/
  3. ManagerFS.cs
  4. OptsFS.cs

正确的例子

  1. Providers/
  2. CatLib.FileSystem/
  3. ManagerFileSystem.cs
  4. OptionsFileSystem.cs

(B)紧密耦合的类名

如果一个类只在另外一个类的场景下有意义,这层关系应该体现在类名(包括文件名)上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。

错误的例子

  1. Providers/
  2. CatLib.FileSystem/
  3. Handler.cs
  4. DirectoryHandler.cs
  5. FileForHandler.cs

正确的例子

  1. Providers/
  2. CatLib.FileSystem/
  3. Handler.cs
  4. HandlerDirectory.cs
  5. HandlerFile.cs

(B)类名的单词顺序

类名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。

为什么不遵循自然语意

在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。

  • coffee with sugar

如果你愿意,你完全可以在类名里包含这些连接词,但是单词的顺序很重要。

在你的类名中,所谓的高级别一般和语境有关,比如对于一个登录界面来说它可能包含下面这些类:

错误的例子

  1. Providers/
  2. CatLib.LoginUI/
  3. AgreementCheckbox.cs
  4. LoginButton.cs
  5. PasswordInput.cs
  6. RegisterButton.cs
  7. TextInput.cs

我们可以发现我们很难发现那些类是针对输入这个功能的。现在我们根据单词顺序进行重命名:

正确的例子

  1. Providers/
  2. CatLib.LoginUI/
  3. ButtonLogin.cs
  4. ButtonRegister.cs
  5. CheckboxAgreement.cs
  6. InputText.cs
  7. InputPassword.cs

因为编辑器通常会按字母顺序组织文件,所以将高级别的单词排在前类之间的重要关系一目了然。

(B)多级目录与类命名

类名的单词顺序指出的问题可以通过多级目录进行解决,但是我们只推荐服务只有在非常多,且种类不一的情况下才这么做。因为在多级目录中查找要比在单目录中查找更加花费精力,并且对于命名空间的引用将会变得复杂。

如果使用了多级目录,那么也请务必保证类的完整命名,否则将会导致在代码中的理解成本增加。

错误的例子

  1. Providers/
  2. CatLib.LoginUI/
  3. Checkbox/
  4. Agreement.cs
  5. Button/
  6. Login.cs
  7. Register.cs
  8. Input/
  9. Password.cs
  10. Text.cs

正确的例子

  1. Providers/
  2. CatLib.LoginUI/
  3. Checkbox/
  4. CheckboxAgreement.cs
  5. Button/
  6. ButtonLogin.cs
  7. ButtonRegister.cs
  8. Input/
  9. InputPassword.cs
  10. InputText.cs

(B)函数名和类名大小写

在声明函数和类的时候,其命名始终使用CamelCase。

我们遵循了微软提供的框架设计指南来确保 API 的一致性和易用性。

错误的例子

  1. public class fileSystem
  2. {
  3. }

正确的例子

  1. public class FileSystem
  2. {
  3. }

(B)将接口绑定到服务,而不是为服务设定别名

我们强烈建议将接口绑定到服务,而不是为服务设定别名。如果以别名的形式设定,很多事件将会无法使用,如:Watch。

错误的例子

  1. App.Bind<FileSystem>().Alias<IFileSystem>();

错误点:直接使用了别名,而没有对实现进行绑定主要接口。

正确的例子

  1. App.Bind<IFileSystem>(()=> new FileSystem());
  1. App.Bind<IFileSystem, FileSystem>();
  1. App.Bind<IFileSystem, FileSystem>().Alias<IDisk>();

如果存在多个接口需要指向一个服务,请使用别名功能。

(B)服务内的命名规范一致

对于一个服务中的命名规范强烈建议一致,例如:要么变量是_开头要么始终是m_开头(或者其他统一的规范,如:无标示符开头)。而不能交叉混用。

交叉混用会导致团队一致性下降,从而提高理解成本。单个组件一般由2-3人协同开发。我们强烈建议以服务为最小单元统一代码命名规范。

错误的例子

  • Providers/CatLib.FileSystem/FileSystem.cs
  1. public class FileSystem
  2. {
  3. private string defaultDiskName;
  4. private IDictionary<string, IDisk> m_disks;
  5. }
  • Providers/CatLib.FileSystem/Disk.cs
  1. public class Disk
  2. {
  3. private string _diskName;
  4. }

正确的例子

  • Providers/CatLib.FileSystem/FileSystem.cs
  1. public class FileSystem
  2. {
  3. private string defaultDiskName;
  4. private IDictionary<string, IDisk> disks;
  5. }
  • Providers/CatLib.FileSystem/Disk.cs
  1. public class Disk
  2. {
  3. private string diskName;
  4. }

(C)使用上下文关系来处理不同实例相同接口的服务

我们建议使用上下文关系来处理不同实例相同接口的服务,而不是使用在具体实现构造函数中通过获取指定服务。如果在实际的实现中获取服务实例,将会和框架耦合,并产生公共耦合。

而将这些关系定义在服务提供者中将会避免这些问题。

错误的例子

  1. public class GameVideo : IGameVideo
  2. {
  3. public GameVideo(IFileSystem fileSystem)
  4. {
  5. var disk = fileSystem.Get("oss");
  6. }
  7. }

正确的例子

  1. public class GameVideo : IGameVideo
  2. {
  3. public GameVideo(IDisk disk)
  4. {
  5. }
  6. }
  1. App.Singleton<IGameVideo, GameVideo>()
  2. .Needs<IDisk>()
  3. .Given(()=> App.Make<IFileSystem>().Get("oss"));

(C)总是在服务提供者中来注册服务

我们建议服务在服务提供者中进行绑定,而不是在Register以外的其他地方。

错误的例子

  • Register之外的其他地方
  1. protected override void OnStartCompleted()
  2. {
  3. App.Singleton<IFileSystem, FileSystem>();
  4. }

正确的例子

  1. public class ProviderFileSystem : ServiceProvider
  2. {
  3. public override void Register()
  4. {
  5. App.Singleton<IFileSystem, FileSystem>();
  6. }
  7. }

(C)字符串常量的值,可以映射到实际类型

我们建议字符串常量,可以映射到实际有效的类型或者该常量本身。这样在未来需要通过常量值来分析时可以快速定位具体的类。

错误的例子

  1. public class ApplicationEvents
  2. {
  3. public const string Bootstrapping = "bootstrapping";
  4. }

正确的例子

  1. public class ApplicationEvents
  2. {
  3. public const string Bootstrapping = "ApplicationEvents.Bootstrapping";
  4. }

(C)一个服务只提供一个服务提供者

一个服务只提供一个服务提供者。如果一个服务提供了多个服务提供者将会导致沟通成本的上升,使用者无法了解不同服务提供者之间的区别或不知道该如何使用。

我们建议使用变量控制的方式来处理这个问题。

错误的例子

  • Providers/CatLib.FileSystem/ProviderFileSystemClean.cs
  1. public class ProviderFileSystemClean : ServiceProvider
  2. {
  3. public override void Register()
  4. {
  5. App.Singleton<IFileSystem, FileSystem>();
  6. }
  7. }
  • Providers/CatLib.FileSystem/ProviderFileSystem.cs
  1. public class ProviderFileSystem : ServiceProvider
  2. {
  3. public override void Register()
  4. {
  5. App.Singleton<IFileSystem, FileSystem>()
  6. .OnResolving((instance) =>
  7. {
  8. var fileSystem = (FileSystem)instance;
  9. fileSystem.Extend(()=> new Disk(new AdapterLocal()), "local");
  10. fileSystem.Extend(()=> new Disk(new AdapterHttp()), "http");
  11. });
  12. }
  13. }

正确的例子

  • Providers/CatLib.FileSystem/ProviderFileSystem.cs
  1. public class ProviderFileSystem : ServiceProvider
  2. {
  3. public bool ExtendDefaultAdapter { get; set; } = false;
  4. public override void Register()
  5. {
  6. var binder = App.Singleton<IFileSystem, FileSystem>();
  7. if(!ExtendDefaultAdapter)
  8. {
  9. return;
  10. }
  11. binder.OnResolving((instance) =>
  12. {
  13. var fileSystem = (FileSystem)instance;
  14. fileSystem.Extend(()=> new Disk(new AdapterLocal()), "local");
  15. fileSystem.Extend(()=> new Disk(new AdapterHttp()), "http");
  16. });
  17. }
  18. }

(C)可拆分单词的命名

有些时候我们可能纠结于可拆分单词如何进行命名,如:username可以被拆分为username。虽然这些单词可以被拆分但是我们需要注意username为英文中的一个整体单词。所以我们应该视作为一个单词。

错误的例子

  1. public class LoginUI
  2. {
  3. private string userName; // UserName, _userName 都是错误的例子
  4. }

正确的例子

  1. public class LoginUI
  2. {
  3. private string username; // Username, _username 都是正确的例子
  4. }

(C)门面应该放置在Facades命名空间下

属于门面的代码应该被放置在项目名.Facades的命名空间下。

错误的例子

  • Providers/CatLib.FileSystem/Facades/FileSystem.cs
  1. namespace CatLib.FileSystem
  2. {
  3. public class FileSystem : Facade<IFileSystem>
  4. {
  5. }
  6. }

正确的例子

  • Providers/CatLib.FileSystem/Facades/FileSystem.cs
  1. namespace CatLib.Facades
  2. {
  3. public class FileSystem : Facade<IFileSystem>
  4. {
  5. }
  6. }

门面作为一个特殊存在,所以我们允许其命名空间例外于其他规范。

(C)对外提供的接口总是放在API文件夹下

我们建议对外提供服务的接口放在Providers/API/组件名文件夹下,这样可以达成接口即文档的意义。

内部使用的接口可以直接放在组件实现的文件夹中,而无需放在API文件夹下。

错误的例子

  1. - Providers/
  2. - - FileSystem/
  3. - - - API/
  4. - - - - IFileSystem.cs
  5. - - - - IDisk.cs
  6. - - - FileSystem.cs
  7. - - - ProviderFileSystem.cs
  8. - - Debugger/
  9. - - - API/
  10. - - - - IDebugger.cs
  11. - - - Debugger.cs
  12. - - - ProviderDebugger.cs

正确的例子

  1. - Providers/
  2. - - API/
  3. - - - FileSystem/
  4. - - - - IFileSystem.cs
  5. - - - - IDisk.cs
  6. - - - Debugger/
  7. - - - - IDebugger.cs
  8. - - FileSystem/
  9. - - - FileSystem.cs
  10. - - - ProviderFileSystem.cs
  11. - - Debugger/
  12. - - - Debugger.cs
  13. - - - ProviderDebugger.cs

(D)在循环中生成Lambda表达式,并尝试访问迭代器变量。

在循环中生成Lambda表达式,并尝试访问迭代器变量时,会导致迭代器变量不是预期值的问题。

错误的例子

  1. foreach (var index in new int[1, 2, 3, 4, 5])
  2. {
  3. closure(()=> index); // index 为预期外的值
  4. }

正确的例子

  1. foreach (var index in new int[1, 2, 3, 4, 5])
  2. {
  3. var localIndex = index;
  4. closure(()=> localIndex);
  5. }

迭代器会导致index发生变化,从而使lambda表达式不能返回正确的index值。

(D)不要让泛型方法支持虚函数重载

在一些平台下(如:unity3d)使用的静态编译(AOT)技术会导致泛型方法虚函数调用非常危险,会导致下面AOT裁剪异常:

Attempting to call method 'xxxxxxxxxxxx' for which no ahead of time (AOT) code was generated.

错误的例子

  1. public virtual void GenericMethod<T1,T2>(T1 data) // 一旦虚函数被覆盖(override)并调用会引发AOT异常
  2. {
  3. }

正确的例子

  1. public void GenericMethod<T1,T2>(T1 data)
  2. {
  3. }