代码该怎么写——设计原则

初学者学会编程语言后开始写代码,当我们实现一个功能后会有成就感,但是看了别人写的代码又大感困惑,他为什么把代码写得那么复杂?明明一个简单的功能,为什么要这样做?

还有人即使学会了编程语言,仍然不知道怎么下手写代码,哪里该创建一个类,哪里又该创建一个方法?

现代社会,文盲率很低,人人识字,但为什么不是人人都能当作家呢?

因为,我们只学识字是不够的,我们还得学习写作的技巧和套路,并且还要有一定的人生经历,这样才能成长为一个作家。简而言之,会写代码,和写好代码,是两个层面!

面向对象编程语言的设计原则和设计模式,就是程序员需要学习的写作套路。这是很多前辈采坑总结的血泪教训,学习这些套路,能避免后来者踩同样的坑,犯相同的错。

现在,就让我们来用Dart 语言来学习这些设计原则和设计模式。

六大设计原则

这些是程序员编程时应当遵守的原则,它们也是设计模式的基础(依据)

单一职责原则(SRP)

单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则。

对类来说,即一个类应该只负责一项职责。假如有类A负责了两个不同职责:职责a1和职责a2。当职责a1因需求变更而改变了A类时,就可能造成职责a2发生错误, 所以需要将类A的粒度拆分为两个类:A1和 A2。

示例

假如某公司开发一视频网站,现在实现视频服务,模拟代码如下:

void main(List<String> arguments) {VideoService().play('1');
}// 创建视频服务类
class VideoService{// 播放资源void play(String resId){print("开始播放ID为: $resId 的视频");}
}

刚开始网站的视频都是随便播放,后来随着公司商业化发展,视频网站实现用户分级制,用户被分为三个级别,普通用户(免费),普通会员,超级会员。不同级别,播放的视频的清晰度、网络带宽是不一样的,这时候,如何在代码中增加该功能,支持业务的发展呢?

在没有学习设计原则和设计模式之前,我们首先想到的可能是在play方法中写大量if-else判断:

  void play(String resId,int userType){if(userType == 1){ // 普通用户print("开始播放ID为: $resId 的 480P 视频,");}else if(userType == 2){ // vip会员print("开始加速播放ID为: $resId 的 1080P 高清视频,");}else{  // 超级会员print("开始加速播放ID为: $resId 的 2K 超清视频");}}

这是一种非常糟糕的代码设计,随着业务发展,功能增多,最后大量的if判断会变成屎山代码。并且这种代码违背了单一职责原则,因为类负责了多个职责,而且我们修改了原有逻辑,这种修改,甚至可能导致以前正常的功能(普通用户播放免费视频)发生错误,

现在按照单一职责原则,拆分类重构代码:

void main(List<String> arguments) {OrdinaryVideoService().play('1');
}// 普通用户视频服务
class OrdinaryVideoService{void play(String resId){print("开始播放ID为: $resId 的 480P 视频,");}
}
// VIP用户视频服务
class VIPVideoService{void play(String resId){print("开始加速播放ID为: $resId 的 1080P 高清视频,");}
}
// 超级VIP用户视频服务
class SuperVIPVideoService{void play(String resId){print("开始加速播放ID为: $resId 的 2K 超清视频");}
}

我们拆分了三个类来分别为不同用户提供视频服务。重构后,我们的逻辑更加清晰易懂,可维护性,可扩展性更高。以后如果出现了在超级VIP之上的新用户级别,我们也可以在不修改已有代码情况下扩展,因为我们只需要创建一个新的类即可,而不是像if判断那样去修改原逻辑。

总结:

  1. 降低类的复杂度,一个类只负责一项职责。

  2. 提高类的可读性,可维护性

  3. 降低修改引发的风险

开闭原则(OCP)

开闭原则(Open-Close Principle,OCP)规定软件中的对象、类、模块和函数对于扩展(提供者)应该是开放的,但对于修改(使用者)是封闭的。这意味着应该用抽象定义结构,用具体实现扩展细节,以此确保软件系统开发和维护过程的可靠性。

对于外部的调用者来说,体现开闭原则需要面向抽象编程。

示例

上面单一职责原则讲的示例看起来仍然比较粗糙,因为我们仅运用了单一职责原则,这还不够,现在我们再结合开闭原则继续重构上例:

void main(List<String> arguments) {VideoService service = OrdinaryVideoService();// VideoService service = VIPVideoService();service.play('1');
}// Dart中的抽象接口
abstract class VideoService{void play(String resId);
}// 普通用户视频服务,实现抽象接口
class OrdinaryVideoService implements VideoService{@overridevoid play(String resId){print("开始播放ID为: $resId 的 480P 视频,");}
}
// VIP用户视频服务,实现抽象接口
class VIPVideoService implements VideoService{@overridevoid play(String resId){print("开始加速播放ID为: $resId 的 1080P 高清视频,");}
}
// 超级VIP用户视频服务,实现抽象接口
class SuperVIPVideoService implements VideoService{@overridevoid play(String resId){print("开始加速播放ID为: $resId 的 2K 超清视频");}
}

首先作为功能模块的提供者,我们编写了三个类提供视频服务,功能的使用者调用这三个类实现需求。也许很多时候,功能提供者和使用者都是我们自己,但是在大团队开发时,可能我们只是编写接口给别人用的,因此在开发时,大脑中需要具有提供者、使用者的思维区分。

对于使用者,不需要知道功能的具体细节,只需要面向抽象接口编程。因此使用者只需要调用抽象接口VideoServiceplay方法。注意,Dart中没有提供专门声明接口的关键字,其抽象类就相当于Java的接口。对于不同的用户级别,接口只需要切换不同的实现类即可。

经过我们上面的重构,就实现了开闭原则。对于新增功能,提供者只需要创建新的类来实现VideoService接口,这就是所谓对扩展开放。使用者则面向接口编程,它并不知道具体实现细节,也无从修改,这就是对修改封闭。

总结:

当软件需要变化时,尽量通过扩展软件的行为来实现变化,而不是通过修改已有的代码来实现变化 。需要注意的是,开闭原则是编程中最基础、最重要的设计原则 。

依赖倒置原则(DIP)

依赖倒置原则(Dependence Inversion Principle,DIP)是指在设计代码架构时,高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更易读,且便于传承。

示例

现在有一个学生,他正在学习Dart和Java编程,代码实现如下:

void main(List<String> arguments) {var student = Student();student.studyDart();
}class Student{void studyDart(){print('我在学习Dart编程');}void studyJava(){print('我在学习Java编程');}
}

后面随着该学生的发展,他又想学习Go语言,怎么添加功能呢?难道继续修改Student类,添加一个studyGo方法吗?显然已经不符合开闭原则,对扩展开放,对修改关闭。同时,代码也违背了依赖倒置原则!

依据依赖倒置原则重构代码:

void main(List<String> arguments) {var student = Student();// 依赖注入student.study(DartCourse());student.study(JavaCourse());student.study(GoCourse());
}class Student{// 依赖抽象接口Course,而不是具体实现void study(Course course){course.study();}
}// 课程接口
abstract class Course{void study();
}class DartCourse implements Course{@overridevoid study() {print('我在学习Dart编程');}}class JavaCourse implements Course{@overridevoid study() {print('我在学习Java编程');}
}class GoCourse implements Course{@overridevoid study() {print('我在学习Go编程');}
}

现在,不论该学生后续想学习多少新课程,都能在不修改原有代码的情况下很简便的扩展。

需要注意的是,当我们在main函数中调用Studentstudy方法传递参数时,就是所谓的依赖注入!

如何理解依赖?一般说的依赖某个类,就是指需要使用某个类。

依赖注入的方式有三种:

  • 通过类的构造方法将需要用到的类传入
  • 通过类的Setter方法,将需要用到的类传入
  • 通过具体使用的接口,将依赖的类传入

上例中显然使用的是第三种方式注入依赖。我们在具体调用study接口时,才将依赖的类传入进去。

总结:

  1. 抽象不应该依赖细节,细节应该依赖抽象
  2. 依赖倒置的中心思想是面向接口编程
  3. 相对于细节的多变性,抽象的东西要稳定得多。以抽象为基础搭建的架构比以细节为基础的架构要稳定得多
  4. 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成
  5. 变量的声明类型尽量是抽象类或接口, 这样变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化

里氏替换原则(LSP)

里氏替换原则(Liskov Substitution Principle,LSP)是由麻省理工学院计算机科学系教授Barbara Liskov 女士于 1987 年提出。她提出:继承必须确保超类所拥有的性质在子类中仍然成立。

原则:如果S是T的子类型,那么所有T类型的对象都可以在不破坏程序的情况下被S类型的对象替换。

简单来说,子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:当子类继承父类时,除添加新的方法且完成新增功能外,尽量不要重写父类的方法。

概括为4点:

  • 子类可以实现父类的抽象方法,但不应该覆盖父类的非抽象方法。
  • 子类可以增加自己特有的方法。
  • 当子类重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松。
  • 当子类实现父类的方法(重写、重载或实现抽象方法)时,方法的后置条件(即方法的输出或返回值)要比父类的方法更严格或与父类的方法相等。

示例

void main(List<String> arguments) {var eagle = Eagle();eagle.fly();var ostrich = Ostrich();ostrich.feature();ostrich.fly();
}// 老鹰
class Eagle{void feature(){print('体覆羽毛,有双翼');}void fly(){print('翱翔天空!');}
}// 鸵鸟
class Ostrich extends Eagle{@overridevoid fly(){print('不会飞!');}void run(){print('奔跑如飞!');}
}

如上例,我们先有老鹰这个类,后面需要鸵鸟类。程序员考虑到动物界,老鹰和鸵鸟同属于鸟类,有很多共性,为了复用代码,少写代码,直接让鸵鸟类继承老鹰类。继承完了,发现鸵鸟不会飞,于是重写父类的fly方法,屏蔽了鸵鸟的飞这个功能。以上代码确实做到了复用,复用了feature方法。但是这样的代码设计是糟糕的,不符合里氏替换原则!

在后续的业务扩展中,我们会逐渐发现,有一部分鸟类是不会飞的,譬如鸡,企鹅,同属鸟类但都不会飞。如果都这样继承,代码只会越写越蹩脚。

为了符合里氏替换原则,我们需要重构代码。一般的解决方法,是抽象出一个共同基类,而不是直接去继承业务类:

void main(List<String> arguments) {var eagle = Eagle();eagle.feature();eagle.fly();var ostrich = Ostrich();eagle.feature();ostrich.run();
}// 抽象基类:鸟类
abstract class Birds{void feature(){print('体覆羽毛,有双翼');}
}// 老鹰
class Eagle extends Birds{void fly(){print('翱翔天空!');}
}// 鸵鸟
class Ostrich extends Birds{void run(){print('奔跑如飞!');}
}

我们将这些类的共同特性抽象到一个单独的基类——Birds中,然后再让这些业务类去继承抽象基类。这样,具体子类只需要创建自己特有的方法,而不需要去重写父类的方法来达到需求。既复用了代码,也遵循了里氏替换原则。

总结:

  1. 约束继承泛滥,同时也是开闭原则的一种体现。
  2. 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

举个生活中的例子,我们经常与USB插口打交道,计算机依赖抽象USB插口去读取数据,至于具体接入什么设备,计算机不必关心,可以是键盘,也可以是扫描仪,只要是兼容USB接口的设备就可以对接。这便实现了多种USB设备的里氏替换,让系统功能模块可以灵活替换,功能无限扩展,这种可替换、可延伸的软件系统才是有灵魂的设计。

迪米特法则(LOD)

迪米特法则(Law of Demeter,LOD)又称为最少知道原则(Least KnowledgePrinciple,LKP),是指一个对象类对于其他对象类来说,知道得越少越好。也就是说,两个类之间不要有过多的耦合关系,保持最少关联性。

迪米特法则还有个更简单的定义:只与直接的朋友通信

所谓直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现在成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

示例

现在有三个类,商品、员工和老板。需求是让老板调度员工去统计商品的数量,代码实现如下:

void main(List<String> arguments) {var boss = Boss();var employee = Employee();boss.scheduleEmployee(employee);
}// 商品
class Goods{}// 员工
class Employee{// 检查商品数量void checkNumber(List<Goods> goodsList){print('检查到的商品数量是:${goodsList.length}');}
}// 老板
class Boss{// 调度员工做事void scheduleEmployee(Employee employee){var goodsList = List.filled(10, Goods());employee.checkNumber(goodsList);}
}

上例显然没有遵循迪米特法则!老板调度员工干活,只需要知道结果,而不关心过程。也就是说,在老板类的scheduleEmployee方法中不应该出现Goods类。Goods类不是Boss类的直接朋友。

按照迪米特法则重构代码:

// 员工
class Employee{List<Goods> _getAllGoods(){return List.filled(10, Goods());}// 检查商品数量void checkNumber(){var goodsList = _getAllGoods();print('检查到的商品数量是:${goodsList.length}');}
}// 老板
class Boss{// 调度员工做事void scheduleEmployee(Employee employee){employee.checkNumber();}
}

我们在Employee的内部封装一个方法获取所有商品,然后在checkNumber方法内部去调用。Boss类中,只需调用雇员的checkNumber功能即可,Boss类是不需要和Goods类耦合的。

总结:

迪米特法则要求,一个类对自己依赖(使用)的类知道得越少越好。也就是说,对于被依赖(使用)的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供公共方法,不对外泄露任何信息。 就像我们上面的Employee类,不管其功能多复杂,都应该封装在类的内部,而不应该让Boss类知道。

举个生活中的例子,假如我们买了一台游戏机,其内部集成了非常复杂的电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理,但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装。除了封装起来的黑盒子游戏主机,手柄是另一个封装好的模块,它们之间只是通过一根线来传递信号,至于主机内部的各种复杂逻辑,手柄一无所知。

接口隔离原则(ISP)

接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

因为客户不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口之上!

示例

现在有人抽象了一个动物接口:

// 动物接口
abstract class Animal{void eat();void fly();void run();void swim();
}// 狗
class Dog implements Animal{@overridevoid eat() {}@overridevoid fly() {}@overridevoid run() {}@overridevoid swim() {}
}// 金鱼
class Goldfish  implements Animal{@overridevoid eat() {}@overridevoid fly() {}@overridevoid run() {}@overridevoid swim() {}
}

如上例,当我们用狗类去实现动物接口时,因为狗不会飞,因此只能空实现一个它不需要的方法。用金鱼去实现动物接口时,金鱼不会跑,也不会飞,得空实现两个不需要的方法。这显然违背了接口隔离原则!

这说明我们在抽象Animal接口时存在问题,抽象的接口太多了,没有建立在最小的接口之上。

根据接口隔离原则重构代码:

// 动物接口
abstract class Animal{void eat();
}// 飞行动物接口
abstract class FlyAnimal extends Animal{void fly();
}// 陆地动物接口
abstract class TerrestrialAnimal extends Animal{void run();
}// 水生动物接口
abstract class WaterAnimal extends Animal{void swim();
}// 狗
class Dog implements TerrestrialAnimal{@overridevoid eat() {}@overridevoid run() {}
}// 金鱼
class Goldfish  implements WaterAnimal{@overridevoid eat() {}@overridevoid swim() {}
}

如上例,我们将之前很大的接口Animal拆分成颗粒度更低的众多小接口,然后让狗类去实现陆地动物接口,让金鱼实现水生动物接口,这样,它们就不需要空实现根本不需要的方法。需要注意,接口之间也是可以继承的,以达到复用代码的目的。我们在Animal中抽象出了eat接口,这是所有动物都具有的行为,然后让其他接口继承它。

总结:

  1. 一个类对另一个类的依赖应该建立在最小接口上。
  2. 建立单一接口,不要建立庞大臃肿的接口
  3. 尽量细化接口,接口中的方法尽量少(当然不是越少越好,要适度)

另外需要注意,Dart语法中提出了一个新的概念 mixin,它在功能上有些类似于接口,使用mixin在一定程度上就遵循了接口隔离原则。Dart不是很强调使用接口这个概念,但它实际上就是将一个大的接口拆分成多个小的 mixin混入,达到依赖最小接口的目的。

其他

以上六大设计原则,也被称为SOLID 原则,这是根据它们英文的首字母缩写而来。其实在这六大原则之外,一些其他书籍资料中,还有一种原则被称为合成复用原则。

合成复用原则(Composite/Aggregate Reuse Principle,CARP)指尽量使用对象组合(has-a)或对象聚合(contanis-a)的方式实现代码复用,而不是用继承关系达到代码复用的目的。合成复用原则可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较小。继承,又被称为白箱复用,相当于把所有实现细节暴露给子类。组合/聚合又被称为黑箱复用,对类以外的对象是无法获取实现细节的。

那么什么是组合关系,什么又是聚合关系呢?这里我们有必要对依赖、组合、聚合等概念做一个总结。

依赖关系

  • 在类中使用到了对方

  • 是类的成员属性

  • 是方法的返回类型

  • 是方法接收的参数类型

  • 在方法中使用到

关联关系

是类与类之间的联系,他是依赖关系的特例。关联具有导航性:即双向关系或单向关系

聚合关系

表示的是整体和部分的关系, 整体与部分可以分开。 聚合关系是关联关系的特例,所以他具有关联的导航性与多重性。

例如,一台电脑由键盘、显示器,鼠标等组成;组成电脑的各个配件是可以从计算机上分离出来的 :

// 鼠标
class Mouse{}// 显示器
class Monitor{}// 计算机
class Computer{Mouse? _mouse;Monitor? _monitor;set mouse(Mouse m){_mouse = m;}set monitor(Monitor m){_monitor = m;}
}

如上例,显示器,鼠标是可以从计算机上分离的,即可以从外部传进来。

组合关系

也是整体与部分的关系,但是整体与部分不可以分开。

我们仍然用上面的例子来描述:

// 鼠标
class Mouse{}// 显示器
class Monitor{}// 计算机
class Computer{Mouse mouse = Mouse();Monitor monitor = Monitor();
}

这里我们运用了组合来描述计算机与鼠标、显示器的关系。我们认为计算机与鼠标、显示器是不可分离的,因此mousemonitor成员不能由外部传入,只能在内部创建实例。

设计的核心思想

  • 找出应用中需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  • 针对接口编程,而不是针对实现编程。
  • 为了交互对象之间的松耦合设计而努力

关注公众号:编程之路从0到1

代码该怎么写——设计原则相关推荐

  1. 代码精进之路-设计原则

    设计原则是前辈的总结,为后来人提供经验,写出更好的代码,降低系统复杂度,提高代码的稳定性,可维护性. 有时候你觉得这个方案这样设计也可以,那样设计也没问题,犹豫不决,这时不妨参考下设计原则,也许你心中 ...

  2. java 七大设计原则之依赖倒置,里氏替换原则(文字代码相结合理解)

    java 七大设计原则之依赖倒置,里氏替换原则,文字代码相结合理解 七大设计原则有哪些? 为什么要使用七大设计原则? 依赖倒置原则 里氏替换原则 喜欢就争取,得到就珍惜,错过就忘记.人生也许不尽完美, ...

  3. 程序员应知道这十大面向对象设计原则

    面向对象设计原则是OOPS编程的核心, 但我见过的大多数Java程序员热心于像Singleton (单例) . Decorator(装饰器).Observer(观察者) 等设计模式, 而没有把足够多的 ...

  4. 第六周 Java语法总结_设计原则_工厂模式_单例模式_代理模式(静态代理_动态代理)_递归_IO流_网络编程(UDP_TCP)_反射_数据库

    文章目录 20.设计原则 1.工厂模式 2.单例模式 1)饿汉式 2)懒汉式 3.Runtime类 4.代理模式 1)静态代理 2)动态代理 动态代理模板 21.递归 22.IO流 1.File 2. ...

  5. 掌握设计原则,你就是光(25个问题,你会几个)

    25个问题,你会几个 如何理解单一职责原则? 如何判断职责是否足够单一? 职责是否设计得越单一越好? 什么是开闭原则? 修改代码就一定意味着违反开闭原则吗? 怎样的代码改动才被定义为扩展或者说是修改? ...

  6. Java程序员最应该学习的几个面向对象的设计原则

    2019独角兽企业重金招聘Python工程师标准>>> 面向对象的设计原则是OOP编程的核心,但我已经看到大多数Java程序员追逐设计模式,如Singleton模式,Decorato ...

  7. 设计原则_LOD原则

    文章目录 1.迪米特法则 2.高内聚 3.松耦合 4.代码 5.总结 1.迪米特法则 迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD. 单从这个名字上来看,我们完全猜不出这个原则 ...

  8. 代码质量评判标准、设计模式、面向对象设计原则速查表

    文章目录 代码质量评判标准 软件腐化的原因 提高系统可复用性的几点原则 可维护性与可复用性并不完全一致 面向对象设计原则 1. 面向对象设计的六大设计原则表 2. 图解面向对象涉及的六大原则 1. 开 ...

  9. 答网友问题:职业化代码设计原则讨论

    这是今天早上刚一上班,一个网友请教问题带出来的话题,个人感觉比较有普遍性,所以把QQ留言做了抄录,整理成一篇文章. 话题不大,不过,我想这里面体现出来的职业化程序员在进行Coding的时候所秉持的一些 ...

  10. SOLID 设计原则 In C# 代码实现

    [S] Single Responsibility Principle (单一职责原则) 认为一个对象应该仅只有一个单一的职责 namespace SingleResponsibilityPrinci ...

最新文章

  1. 2021年普高考成绩查询,山东2021年高考成绩改为6月26日前公布
  2. Matplotlib基础全攻略
  3. Javascript 构造函数模式、原型模式
  4. 字典排序什么意思_列表及字典的排序
  5. Java正则之Unicode属性匹配的那些事
  6. MYSQL的随机查询的实现方法
  7. 谷歌 .dev 顶级域名正式开放
  8. 高级GIS-0.整体裁剪
  9. 感觉最近有多个机器人给吾博客评论
  10. 惠普z800工作站bios设置_HP工作站 BIOS说明 适用Z228 Z440 Z230 Z640 Z840 Z800 Z620 Z420 Z820主板设置 -...
  11. python博弈论_基于原生python的进化博弈实现
  12. linux下重装显卡驱动
  13. 【unity3D】 分享学习路上的一些坑(二)——人物血条在行走时发生旋转;
  14. vcenter报esxi主机 上行链路网络冗余丢失或网络冗余已降级
  15. 【CV】细粒度图像分割 (FGIS)
  16. 【Windows 问题系列第 12 篇】Windows 10 如何显示文件名后缀
  17. 讯飞社区android 源码,android 讯飞语音 demo
  18. unix 增强工具_适用于任何UNIX系统的10种出色工具
  19. mysql数据导入报错1265
  20. xxxxxxxxxxxxxxxxxxxxxxxxxxxxxcccccccccccc

热门文章

  1. 在兼容系统上升级DELL SATA硬盘的固件
  2. 司机秘书:让司机省心的违章查询助手
  3. 运营天猫商城的注意事项
  4. 从总线式以太网到SDN交换机OpenVSwitch
  5. 解决iOS 12.4 (16G77), which may not be supported by this version of Xcode
  6. 安装最新版SopCast 0.4.1
  7. Windows10快速切换后台程序的快捷键!
  8. [4G5G专题-59]:L3 RRC层-RRC层概述与总体架构、ASN.1消息、无线承载SRB, DRB、终端三种状态、MIB, SIB,NAS消息类型
  9. 手机电视标准对峙激化
  10. 华为RS1 企业的网络架构