
今天的任务是一个非常标准的分层式架构面向接口对的代码。很绕很抽象但好处同样很多。在讲之前我自己先寻思着写了一份是意大利面条型的就分了三大类乱七八糟写到后面我自己都有点迷。今天一天的时间都在捋这个规范的优秀的牛b的企业级的可多人合作的可维护的后续可升级的....省略代码项目是经典的学生管理系统分男生女生可以增删改查这样。我自己写的就不提了 功能肯定是能实现的就是乱难以维护几天之后不看注释我自己都看不懂如果几天之后没有注释谁来了也看不懂爆论下面主要谈谈这个优秀案例这里主要讲的是代码的结构搭建具体实现部分就一笔带过了。以下内容仅为个人学习笔记如有疏漏欢迎评论区指正第一部分 基本类对象的定义第一步是定义公开的枚举通过枚举来设置性别 我感觉先写这个的原因是枚举是后续所有代码的基础从People的构造函数开始都要用到它。好处是男,女字符串变成了Gender.Male ||Female写错有爆红提示以及通过这个后续需要用到性别直接调用即可。因为这个是老师的炫技教学式的代码所以怎么漂亮怎么写我自己手搓大概率是不这样写的毕竟还处于初级水平。这也是区分一个好的健壮的代码和一个一般代码的区别了public enum Gender //枚举设置性别 { Male 1, Female 2 }第二步定义抽象 父类 Peopleusing System; namespace Plusa.Model { public abstract class People { public string Name { get; set; } public int Age { get; set; } public Gender Sex { get; protected set; } public double Score { get; set; } protected People(string name, int age, Gender sex, double score) { Name name; Age age; Sex sex; Score score; } public override string ToString() { return ${Name} {Age}岁 {(Sex Gender.Male ? 男 : 女)} {Score}; } } }为什么这里要用抽象我的理解是抽象类不能被实例化不能new 一个people 出来只能被子类继承。因为NameAgeSexScore是每一个学生都要有的默认逻辑并且不希望new people只希望有新的子类存在所以就用了抽象父类people。另外这里override重写了Tostring方法就不需要狼狈的写一长串输出格式了只需要打印的时候调用ToString就可以一次性输出格式化信息。关于为什么先写这个因为下面要创建的Boy类和Girl类都是People的子类所以要先有父类。另外一点为什么要用泛型来写因为这个项目是分男女的两者逻辑一样但是实体类不同泛型可以实现一套代码来同时处理男女两种类型甚至更多类型所以就用了泛型。缺点是更加抽象可读性肯定不如直接写但是可拓展性会更好。第三步定义具体的子类Boy和Girlnamespace Plusa.Model { public class Boy : People { public Boy(string name, int age, double score) : base(name, age, Gender.Male, score) { } } }这里只列出Boy类两者除了类名和性别别的一模一样。解释一下代码public class Boy ------ People 表示boy继承自people所以boy拥有people的四个属性。public Boy ------Boy的构造参数仅接受三个姓名年龄分数值不接受性别参数basename ........------这里是调用父类的构造函数直接传入四个值这里直接传参性别为Gender.Male写死了性别为男性因为是Boy类。另外这里还有封装的体现外部调用的时候根本不需要关心性别因为调用Boy肯定是男性写的时候只需要new Boy(张三,18,76.6)就可以自动就设定为男生。 就是子类把父类需要的但是外部不需要知道的细节给包起来了。为什么先写这个Boy和Girl呢因为后面的DalT需要用到这些来作为泛型的参数所以先把所有的需要用到的全部搭建好。到这里第一部分数据模型整个项目的地基搭建完毕抽象父类people定义所拥有的属性boy / girl 继承people并固定性别通过base传参直接赋值且固定性别无需重复定义这里枚举部分是可以选写的没有也可以实现有的话会让代码更加优秀但是抽象父类和继承子类是非常合适的可选结构建议直接参考。另外如果后续项目升级需要加入父母老师等更多种类的对象直接在这部分新建teacher等新的类就可以实现。这就是泛型的魅力了第二部分 接口和方法第四步定义接口using Plusa.Model; using System.Collections.Generic; namespace Plusa.Dal { public interface IDalT where T : People { void Add(T item); bool Remove(string name); void Update(string name, T newValue); T FindOne(string name); IEnumerableT GetAll(); } }这里就是整个面向接口的灵魂了接口文件用户层的所有操作都是通过接口来调用后面写的Del部分无法越过这些直接接触底层数据where T People是泛型约束表明T必须是People或子类BoyGirl这样接口内部才可以安全的使用people的属性和方法void Add 声明了一个 add方法接受一个T类型的参数item无返回值这里是接口仅仅声明了这个方法但是没有方法体所有的方法体都在DalT里面.后面T会变成Girl或者Boy也就是外界直接传过来一个Boy或者Girl的对象对象里面有这个对象的数值Nameage等然后直接把这个对象丢到List数组里面之类的实现了非常完美的封装。下面四个方法也一样仅功能不同就不再赘述为什么要先写接口再写后面的方法体接口就相当于合同的大纲先拟定好大纲都有什么项目再去DalT去具体执行这样的话首先一个好处就是这些功能是你一开始就拟定好的接口写好了后面的方法体就必须要有这些防止遗漏另外数据封装program和后面要写的stManager只能和接口对接,外界想要修改数据只能通过Add等方法来操作不能随随便便便就list[0]null这样的改变数据。增强了安全性。另外如果有更换数据库的需求上层代码也无需更改仅更改接口下面的底层数据库就可以。第五步具体方法体的实现DalTusing Plusa.Model; using System; using System.Collections.Generic; using System.Linq; namespace Plusa.Dal { internal class DalT : IDalT where T : People { private readonly ListT _List new ListT(); public void Add(T item) { bool isFlag _List.Any(x x.Name item.Name); if (isFlag) { throw new InvalidOperationException($该学生已存在{item.Name}); } _List.Add(item); } public IEnumerableT GetAll() { return _List.ToArray(); } public T FindOne(string name) { return _List.FirstOrDefault(x x.Name name); } public bool Remove(string name) { T item _List.FirstOrDefault(x x.Name name); if (item null) return false; return _List.Remove(item); } public void Update(string name, T newItem) { T oldItem _List.FirstOrDefault(x x.Name name); if (oldItem null) { throw new KeyNotFoundException($未找到学生{name}); } int index _List.IndexOf(oldItem); _List[index] newItem; } } }代码的部分解析internal class DalT:IdalTwhereT:People这一句乍一看很绕一部分一部分理解首先是访问权限internal限定了只能在当前程序集访问外部只能通过接口IDalT来访问。然后是类名DalT 继承IDalT泛型约束T下面是private readonly ListT _List new ListT();这个就是储存数据用的List数组了。定义了一个私有的ListT数组_list意思就是这个数据就只能在当前类的内部访问外来操作必须通过该类所提供的方法来实现对这个数据的操作(这个_list是数组的名字可以自拟)readonly只读表示引用不可变但内容可删改意思就是这个_List就固定死了只能指向这个ListT的内存空间不可以重新赋值更换但是_List所指向的内存空间的内容是可以修改的像是Add等都可以正常使用。好处是避免了在别的地方不小心把整个列表弄丢_List被重新指向别的。还是增强安全性。下面是方法体这里以Add为例用了LINQ,附解析public void Add(T item) { bool isFlag _List.Any(x x.Name item.Name); if (isFlag) { throw new InvalidOperationException($该学生已存在{item.Name}); } _List.Add(item); }注意这里传入的参数T item解释一下这个妙处这里传入的是一个具体的,Boy或者Girl类的对象然后下面去判断传入的这个对象item的 name属性有没有和列表里面重复如果没有直接调用List列表的Add方法添加这个对象到List列表直接对整个过程进行了封装这一层不需要用户进行交互只执行数据的调用另外因为我们的T会被设置为Boy或者Girl在后面的Manager类里面会有这样两行代码private readonly IDalBoy _boyDal new DalBoy(); private readonly IDalGirl _girlDal new DalGirl();第一行 new DalBoy,在内存里创建了一个对象这个对象内部的List类型是_ListBoy第二行是一个_ListGirl两者完全独立虽然他们都是_List但是他们属于两个不同的对象实例程序会根据泛型的不同创建两个独立的List列表也就是说T是Boy时整个程序就会对Boy的List列表进行操作Girl也是一样两个列表各自储存各自的数据这也就是为什么整个程序能做到一套代码两种数据男生女生数据隔离。这就是泛型的力量只写了一套DalT,但是通过不同的T系统自动生成了两份完全不同的副本LINQ的解析首先是用Linq语句来检查有没有同名学生先说说LINQ是什么翻译成中文是 语言集成查询我的理解是之前查询等就是从条件开始写ifforelseswitch等判断什么返回什么这是我自己一行一行写的查询而LINQ相当于搜索引擎我直接告诉电脑我要从XXX里面去查找YYY电脑会自动执行执行完告诉我搜索的结果。下面具体解析首先是定义了一个bool值 isFlag 这个用来接住LINQ语句返回的东西_List 上文创建的那个ListT类型的列表里面存了一堆Boy或者Girl类的对象.Any 这个就是LINQ语句的具体方法了 意思是是否至少存在一个这个是LINQ预设的一个方法放到这里就是Boy或者Girl里面有没有存在哪怕只有一个人满足我要的条件如果有返回true如果没有返回false。Any后面的圆括号里面放的就是我要电脑检索的内容也就是 x x.Nameitem.Name?第一个x很抽象就像是for循环里面的i一样是我们自己随便起的一个名字放到这里就是代表一个Boy或者一个Girl就是List列表里面的某一个对象箭头后面就是我要的具体条件有没有一个x的Name属性和我传入的item的Name属性一样呢把整行代码翻译成中文就是在数据列表_List里面有没有存在一个学生X使得X的名字等同于传入的新学生item的名字回到我们的代码如果有此时我们定义的bool值变为true就抛出一个提示告诉使用者这个名字已经有了阻止添加。否则就是没有这个人然后通过_List.Add直接添加这个人进入到List列表。到这里第二部分数据的访问层完成这一部分一个是接口IDalT,正如同名字一样这是一个接口外部只可以通过这个接口来对数据进行操作。并且约束后面的DalT里面必须完全按照接口的样子来完成接口预设好的五个方法。DalT 实现 这个是对数据进行操作的所有的数据都在这个列表内外部通过接口写好的方法接入到这里然后对数据进行操作外界不知道_List是什么只能通过AddRemove等方法去操作封装的本质就是操作者不需要知道里面到底是什么东西就好像我们输入密码的时候不需要知道密码是怎么和数据库匹配的只需要按照要求去输入就好了。这就是封装第三部分第六步 定义StManagerusing Plusa.Dal; using Plusa.Model; namespace Plusa.Manager { public class StManager { private readonly IDalBoy _boyDal new DalBoy(); private readonly IDalGirl _girlDal new DalGirl(); public StManager() { _boyDal.Add(new Boy(张三, 18, 88.5)); _girlDal.Add(new Girl(张四, 18, 88.5)); } public IDalT GetDalT() where T : People { if (typeof(T) typeof(Boy)) { return (IDalT)_boyDal; } if (typeof(T) typeof(Girl)) { return (IDalT)_girlDal; } throw new NotSupportedException(不支持的类型必须是 People 的派生); } } }这部分是调度层这个项目有两种数据男女Manager的核心作用就是根据用户的输入来决定后续操作哪种数据。具体根据传入的TBoy或者Girl来返回对应的Dal实例在这一层的操作者不需要知道Dal是什么怎么创建的只需使用就好了。这里第一行第二行上文已经解析过声明并实例化一个男生仓库这里的类型是接口 IDalBoy,实例对象是DalBoy。下面是核心代码。public StManager创建了一个 构造函数用于初始化一些示例数据另外还定义了一个泛型方法根据类型的参数返回对应的IDal实例。public IDalT GetDalT() where T : People公共泛型方法 IDalT .然后根据T的具体类型返回一个对应的Del这里 IdalT 就像是public int nun 的这个int一样是告诉编译器这个方法会返回一个IdalT接口类型的对象。GetDalT 是这个方法自己的名字就像是num一样自己定义的但这个T是泛型参数我可以把这里改成AAAAAT,这个泛型参数不能丢因为他要告诉编译器这是一个泛型方法。为什么这里返回的是接口而不是Dal这个类因为这里只需要知道我拿到了一个东西这个东西可以实现增删改查具体是怎么实现在那里实现那我全都不用管。后面的where是约束条件约束T这个泛型必须是People的或者是他的子类的这些的共同点就是 全部都实现了IdalT这个接口满足这个代码可以正常返回举个例子 我调用了GetDal Boy这里的T被我设为了Boy所以方法内部会进入第一个if返回一个_boyDal,他的类型是 IDalBoy和方法声明的完全一致类型匹配转换成功拿到返回值之后就可以进一步的通过这个接口内部的 五个方法来操作数据无法直接访问DalBoy中间隔着一层接口这就是接口的隔离作用。第七步 Program 表现层using Plusa.Dal; using Plusa.Manager; using Plusa.Model; using System; namespace Plusa { internal class Program { private static readonly StManager _stm new StManager(); static void Main(string[] args) { Console.Clear(); Console.WriteLine(------欢迎使用学生管理系统------); Console.WriteLine(1.男生); Console.WriteLine(2.女生); Console.WriteLine(9.退出); int userin 0; while (true) { Console.Write(输入对应序号执行功能); string input Console.ReadLine(); if (int.TryParse(input, out userin)) break; else Console.WriteLine(输入无效请输入数字); } switch (userin) { case 1: RunSubMenuBoy(); break; case 2: RunSubMenuGirl(); break; case 9: return; default: break; } } private static void RunSubMenuT() where T : People { var dal _stm.GetDalT(); while (true) { Console.Clear(); Console.WriteLine(${(typeof(T) typeof(Boy) ? 男生 : 女生)}列表); foreach (var s in dal.GetAll()) Console.WriteLine(s); Console.WriteLine(----------------); Console.WriteLine(1新增2删除3修改4查询5返回); switch (Console.ReadLine()) { case 1: AddT(dal); break; case 2: RemoveT(dal); break; case 3: UpdateT(dal); break; case 4: FindOneT(dal); break; case 5: return; default: break; } } } private static void AddT(IDalT dal) where T : People { Console.Write(姓名); string name Console.ReadLine(); Console.Write(年龄); int age int.Parse(Console.ReadLine()); Console.Write(分数); double score double.Parse(Console.ReadLine()); T item typeof(T) typeof(Boy) ? (T)(object)new Boy(name, age, score) : (T)(object)new Girl(name, age, score); dal.Add(item); Console.WriteLine(${name} 添加成功); Console.ReadKey(); } private static void RemoveT(IDalT dal) where T : People { Console.Write(输入要删除的名字); string name Console.ReadLine(); bool su dal.Remove(name); Console.WriteLine(su ? ${name} 删除成功 : $删除失败{name} 未找到); Console.ReadKey(); } private static void UpdateT(IDalT dal) where T : People { Console.Write(输入要修改的名字); string oldName Console.ReadLine(); Console.Write(新姓名); string newName Console.ReadLine(); Console.Write(新年龄); int age int.Parse(Console.ReadLine()); Console.Write(新分数); double score double.Parse(Console.ReadLine()); T newItem typeof(T) typeof(Boy) ? (T)(object)new Boy(newName, age, score) : (T)(object)new Girl(newName, age, score); dal.Update(oldName, newItem); Console.WriteLine(修改成功); Console.ReadKey(); } private static void FindOneT(IDalT dal) where T : People { Console.Write(输入要查找的名字); string name Console.ReadLine(); T result dal.FindOne(name); if (result ! null) Console.WriteLine($查找成功{result}); else Console.WriteLine($查找失败{name} 未找到); Console.ReadKey(); } } }这里是整个程序的最后一部分表现层也是跟用户进行交互的界面这一层的目的是为了把 界面逻辑 和 数据操作 完全分离开数据逻辑全部在内层进行操作外层就是单纯面向用户的操作简单讲这部分的关键代码RunSunMenuT 这是一个泛型方法和前面的操作逻辑结合起来就是用户输入1传入Boy就显示男生列表输入2就传入Girl显示女生列表根据传入值的不同从StManager拿到的Dal也不同然后在这个Dal的基础上和用户去进行进一步的交互。这套代码里面没有用任何一个if来判断现在是在操作男生还是女生整个流程完全是依靠着泛型驱动菜单标题也是用的typeof来判断这就是抽象的好处。只需要搭建好框架就能通吃所有类型。可能在这个案例上好处不算明显并没有节约多少反而逻辑上非常抽象大大提高了代码的难度。但是假如说我不光要男生女生我还要老师还要家长还要朋友难道我每一个都要重新再写一遍流程吗如果用泛型 抽象 就只需要写一遍主题然后多创建几个类现在这个结构完全不用大改。这就大大增加了这个代码的可维护性可拓展性。总结一下这套结构严格来说总共分为4层model层用来定义所有要用到的数据 数据实体Del层用来定义数据要怎么存怎么操作如何去操作 数据操作接口Manager层定义了怎么去根据用户的操作找到对应的数据 调度分发Program层定义用户如何去操作界面交互这套结构每一层各司其职互不干扰。整个顺序是从底往上走的这是一个比较小的项目编写过程是 先有数据再有操作接口再有实现再有调度最后才是面向用户的界面。每一层都只依赖邻近的下一层上层不关心下层内部如何具体实现只需要传好值做好对接就行。这就是多人合作编写代码分层架构的核心原则下面是一些个人的想法吧。能学到这里学这个代码学的这么深一般就不只是因为自己的乐趣而学习了而是为了工作为了以后的项目为了多一些技能多一些赚钱的本事。等真的到了用这东西工作吃饭的时候可能会面对的是一个综合性很强的几千行甚至更多的大项目那就已经不是自己一个人完成的工作而是和别人一起合作分部分完成了。编写一个大的项目的时候一般是每个人负责一部分内容然后大家做好对接一个团队一起去完成。总不能大家都在Program里面你写一段我写一段全部堆在一起吧(摊手以上全部内容仅为个人拙见如有遗漏或缺陷还请指出。