抽象类与接口
solid
设计原则
五个基本设计原则的首字母,然后通过这五种基本的设计原则,衍生出很多种的设计模式
抽象类
就是五个基本设计原则当中的
o
即open or close
某些软性规则需要服从,不然就会给自己或者他人带来麻烦
抽象类:至少有一个成员完全没有被实现的类
抽象类具有以下功能:
-
抽象类不能实例化。
正因为这个抽象成员没有被实现,所以调用的时候,就调用不了。所以是不允许实例化一个抽象类的。
-
抽象类可能包含抽象方法和属性。
也就是说,有抽象成员的类一定是被
abstract
所修饰,但是抽象类不一定有抽象成员c#abstract class BaseExample { public int MyProperty { get; set; } } abstract class SecondeClass { protected abstract void Run(); }
-
无法使用 sealed 修饰符来修改抽象类,因为两个修饰符的含义相反。
sealed
修饰符阻止类被继承,而abstract
修饰符要求类被继承。 -
派生自抽象类的非抽象类,必须包含全部已继承的抽象方法和访问器的实际实现。
-
没有被实现的抽象成员,不能用
private
来修饰,因为需要子类可以看到去实现这个成员 -
当抽象类从基类继承虚方法时,抽象类可以使用抽象方法重写该虚方法。
c#using System; using System.Collections.Generic; using System.Linq; namespace AbstractExample { class BaseExample { internal virtual void Dowork() { Console.WriteLine("I'm doing work."); } } abstract class SecondClass : BaseExample { internal abstract override void Dowork(); } class ThirdClass : SecondClass { internal override void Dowork() { Console.WriteLine("Yes, I'm studying."); } } internal class Program { static void Main(string[] args) { ThirdClass thi = new ThirdClass(); thi.Dowork(); } } }
抽象成员
抽象方法具有以下功能:
-
抽象方法是隐式的虚拟方法。
纯粹的虚拟方法,连方法体都没有
-
只有抽象类中才允许抽象方法声明。
-
由于抽象方法声明不提供实际的实现,因此没有方法主体;方法声明仅以分号结尾,且签名后没有大括号 ({ })。 例如:
csharppublic abstract void MyMethod();
实现由方法 override 提供,它是非抽象类的成员。
-
在抽象方法声明中使用 static 或 virtual 修饰符是错误的。
因为会冲突:
virtual
表示可被子类所重写,但是抽象方法是必须被子类所重写static
表示类的静态方法,是不能被重写的
除了声明和调用语法方面不同外,抽象属性的行为与抽象方法相似。
- 在静态属性上使用
abstract
修饰符是错误的。 - 通过包含使用 override 修饰符的属性声明,可在派生类中重写抽象继承属性。
综合使用
抽象类的综合使用
c#
using System;
using System.Collections.Generic;
using System.Linq;
/* 为做基类而生的抽象类 与 开放/关闭原则
* 都在上面有介绍
*/
namespace AbstractExample {
internal class Program {
static void Main(string[] args) {
Vehicle v = new RaceCar();
v.Run();
Console.ReadLine();
}
}
abstract class Vehicle {
//交通工具公有的方法,并且不会改变的
public virtual void Stop() {
Console.WriteLine("Stopped!");
}
public virtual void Fill() {
Console.WriteLine("Pay and fill...");
}
//公有的Run方法,但是不同的交通工具的具体行为不同
//这个方法在Vehicle中用不到,就可以变为抽象方法
//于是类就变成抽象类
public abstract void Run();
}
class Car : Vehicle {
public override void Run() {
Console.WriteLine("Car is running...");
}
}
class Truck : Vehicle {
public override void Run() {
Console.WriteLine("Truck is running...");
}
}
class RaceCar : Vehicle {
public override void Run() {
Console.WriteLine("Race Car is running...");
}
}
}
下面是完全抽象类,一般都不会出现完全抽象类,因为这种完全抽象类被接口代替了
c#
using System;
using System.Collections.Generic;
using System.Linq;
/* 为做基类而生的抽象类 与 开放/关闭原则
* 都在上面有介绍
*/
namespace AbstractExample {
internal class Program {
static void Main(string[] args) {
Vehicle v = new RaceCar();
v.Run();
Console.ReadLine();
}
}
//纯抽象类
//在C#中,这种纯抽象类,就是接口,会被接口替代
//一般不会这么写
abstract class VehicleBase {
public abstract void Stop();
public abstract void Run();
public abstract void Fill();
}
abstract class Vehicle : VehicleBase{
//交通工具公有的方法,并且不会改变的
public override void Stop() {
Console.WriteLine("Stopped!");
}
public override void Fill() {
Console.WriteLine("Pay and fill...");
}
//公有的Run方法,但是不同的交通工具的具体行为不同
//这个方法在Vehicle中用不到,就可以变为抽象方法
//于是类就变成抽象类
//public abstract void Run();
//当继承一个抽象类,但是不实现某个抽象方法时,也就不用写出来了
}
class Car : Vehicle {
public override void Run() {
Console.WriteLine("Car is running...");
}
}
class Truck : Vehicle {
public override void Run() {
Console.WriteLine("Truck is running...");
}
}
class RaceCar : Vehicle {
public override void Run() {
Console.WriteLine("Race Car is running...");
}
}
}
开闭原则
开闭原则:
- 如果不是为了修Bug,或者是添加新的功能的话,闲着没事,不要老去修改一个类的代码,特别是类当中的函数成员的代码
- 也就是说,我们应该去封装一些,不变的、稳定的、固定的、确定的成员而把那个不确定的,有可能改变的声明为抽象成员并且留给子类去实现所以抽象类和开闭原则,天生就是一对
一个完全抽象的类的上位替代就是接口:
三个示例对比着看。
c#
using System;
using System.Collections.Generic;
using System.Linq;
/* 为做基类而生的抽象类 与 开放/关闭原则
* 抽象类,在文档中有介绍
*
* 开闭原则:
* 如果不是为了修Bug,或者是添加新的功能的话,
* 闲着没事,不要老去修改一个类的代码,
* 特别是类当中的函数成员的代码
* 也就是说,我们应该去封装一些,不变的、稳定的、固定的、确定的成员
* 而把那个不确定的,有可能改变的声明为抽象成员
* 并且留给子类去实现
* 所以抽象类和开闭原则,天生就是一对
*/
namespace AbstractExample {
internal class Program {
static void Main(string[] args) {
Vehicle v = new RaceCar();
v.Run();
Console.ReadLine();
}
}
/* 改成接口
* 接口本身,就包含了纯抽象类的这个含义
* 里面的成员是不需要用abstract 和 public来修饰的
* 因为默认为public和abstract的,避免重复
* 加了反而会报错
*/
interface IVehicle {
void Stop();
void Run();
void Fill();
}
abstract class Vehicle : IVehicle{
//此时就是实现接口
public void Stop() {
Console.WriteLine("Stopped!");
}
//不再是重写,而是实现
public void Fill() {
Console.WriteLine("Pay and fill...");
}
//对于想留给子类实现的接口,可以声明为abstact
public abstract void Run();
}
class Car : Vehicle {
public override void Run() {
Console.WriteLine("Car is running...");
}
}
class Truck : Vehicle {
public override void Run() {
Console.WriteLine("Truck is running...");
}
}
class RaceCar : Vehicle {
public override void Run() {
Console.WriteLine("Race Car is running...");
}
}
}
接口
抽象类是未完全实现逻辑的类。
接口是完全未实现逻辑的类。
接口的成员一定是private
的,这决定了接口的本质。接口的本质就是:服务的调用者 与 服务的提供者 之间的契约 ,既然是契约就必须是透明的,并且同时约束供需双方。
记住:
当类实现一个接口的时候,类与接口之间的关系也是紧耦合的。
当两个类之间的耦合通过一个接口来连接的时候,实际上是把两个类之间的耦合变成了,类---接口---类,这样的三者之间的耦合关系。两个类之间的关系是松了,但是和接口之间的关系紧了。
自顶向下的是需要非常了解技术、非常了解业务逻辑的人。
更多的时候,我们是在写代码的过程当中不断的重构代码,发现某个地方需要一个抽象类,然后就抽象出来。发现某个地方需要一个接口,就把接口抽象出来。慢慢这样自底向上建立起来的。
需求场景:
使用接口前:
c#
using System;
using System.Collections;
namespace InterfaceExample {
/* 对一组整数进行求和、求平均值
*/
internal class Program {
static void Main(string[] args) {
//整数存放的容器还不一样
int[] nums1 = new int[]{1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList() {1, 2, 3, 4, 5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
//如果此时我们想要对ArrayList类型的数组也进行求和,
//那么就必须得再把方法写一遍,并且更改参数列表
Console.ReadKey();
}
//求和
static int Sum(int[] nums) {
int sum = 0;
foreach (int i in nums) {
sum += i;
}
return sum;
}
//求平均值,返回类型得是double
static double Avg(int[] nums) {
double sum = Sum(nums);
int count = 0;
foreach (int i in nums) {
count++;
}
return sum / count;
}
//ArrayList类型的求和
static int Sum(ArrayList nums) {
int sum = 0;
foreach (int i in nums) {
sum += (int)i;//拆箱
}
return sum;
}
//ArrayList类型的求平均值
static double Avg(ArrayList nums) {
double sum = Sum(nums);
int count = 0;
foreach (int i in nums) {
count++;
}
return sum / count;
}
}
}
使用接口后:
c#
using System;
using System.Collections;
namespace InterfaceExample {
/* 对一组整数进行求和、求平均值
*/
internal class Program {
static void Main(string[] args) {
/* 改用接口
* 对需方的约束就是,提供的数组必须可以被迭代
*/
int[] nums1 = new int[]{1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList() {1, 2, 3, 4, 5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
//如果此时我们想要对ArrayList类型的数组也进行求和,
//那么就必须得再把方法写一遍,并且更改参数列表
Console.ReadKey();
}
//接口对供方的约束,就是遵守IEnumerable接口
static int Sum(IEnumerable nums) {
int sum = 0;
foreach (int i in nums) {
sum += i;
}
return sum;
}
//求平均值,返回类型得是double
static double Avg(IEnumerable nums) {
double sum = Sum(nums);
int count = 0;
foreach (int i in nums) {
count++;
}
return sum / count;
}
}
}
依赖关系的产生
依赖 的同时就出现了耦合
c#
using System;
using System.Collections;
namespace InterfaceExample {
/* 展示依赖与耦合的关系
*/
internal class Program {
static void Main(string[] args) {
var engine = new Engine();
var car = new Car(engine);
car.Run(3);
Console.WriteLine(car.Speed);
Console.ReadLine();
}
}
class Engine {
public int RPM { get; private set; }
//外界不能设置这个属性
public void Work(int gas) {//控制油门大小
this.RPM = 1000 * gas;
}
}
class Car {
private Engine _engine;
/* 因为这个字段是一个确定的类型
* 此时Car这个类就和Engine紧耦合在了一起
* 如果基础类Engine类出了问题,
* 不管Car类写得再正确,也不会正确工作
* 紧耦合要是出了问题,不仅程序不好调试,而且还是影响团队工作
*/
public Car(Engine engine) {
_engine = engine;
}
public int Speed { get; private set; }
public void Run(int gas) {
_engine.Work(gas);
this.Speed = _engine.RPM / 100;
}
}
}
因为使用了具体的类型,而产生了紧耦合的关系。
所以我们可以不使用一个具体的类型,而使用一个模糊的类型(该类型,可以在调用的时候,确定自己的类型,即接口类型),这样就没有了紧密的依赖关系。
引入接口解决耦合度过高
因为传入的类型是一个模糊的类型,这个类型只有在对象实例化的时候,才会确定传入的是什么类型。
这样的话,如果其中一个类出了问题,也不会那么难才能找到问题的出处了。
所以就巧妙的将两个类紧密的依赖关系,松开了一些。
这个耦合已经相当的松了。
c#
using System;
using System.Collections;
namespace InterfaceExample {
/* 展示依赖与耦合的关系
*/
internal class Program {
static void Main(string[] args) {
var user = new PhoneUser(new NokiaPhone());
user.UsePhone();
Console.ReadKey();
}
}
interface IPhone {
//手机的四个功能
void Dail();
void PickUp();
void Receive();
void Send();
}
//用户类
class PhoneUser {
private IPhone _phone;
/* 此时字段类型就不是某一个具体的类型了
* 而是一个接口的类型,这样就可以解除紧耦合
*/
public PhoneUser(IPhone phone)
{
_phone = phone;
}
public void UsePhone() {
_phone.Dail();
_phone.PickUp();
_phone.Send();
_phone.Receive();
}
}
class NokiaPhone : IPhone {
public void Dail() {
Console.WriteLine("Nokia calling...");
}
public void PickUp() {
Console.WriteLine("Hello! This is Tim!");
}
public void Receive() {
Console.WriteLine("Nokia message ring...");
}
public void Send() {
Console.WriteLine("Hello!");
}
}
class EricssonPhone : IPhone {
public void Dail() {
Console.WriteLine("Ericsson calling...");
}
public void PickUp() {
Console.WriteLine("Hi! This is Max!");
}
public void Receive() {
Console.WriteLine("Ericsson ring...");
}
public void Send() {
Console.WriteLine("Hi!");
}
}
}
还有更松的耦合,就是通过反射实现的,后面会讲。
-
只要代码中有可以替换的地方,那么就一定会有接口的存在。接口就是为了解耦而生的。
-
松耦合最大的好处就是,可以让功能的提供方变得可以替换。
从而降低紧耦合的时候功能的提供方所带来的 高风险与高成本
依赖反转原则
五种原则当中的
d
解耦在代码当中的表现就是依赖反转
自顶向下逐步求精的思维方式:将一个大问题,分成多个小问题,然后通过解决小问题,从而最后解决大问题。
在紧耦合的情况下,就会形成这种金字塔形。
依赖反转原则就是给我们一种思路用来平衡这种,自顶向下逐步求精的单一的思维方式。
依赖关系变化图:
依赖反转,从这个图中来看,就是箭头的反转。
原本是Driver
指向Car
类,现在变成了Driver
类指向了IVehicle
接口,Car
类也指向了Ivehicle
接口
再深入,问号那个地方就是设计模式了
单元测试
单元测试 就是 依赖反转原则在开发当中的直接应用 和直接收益者
接口、解耦、依赖反转原则被单元测试所应用:
背景:生产电扇的厂商,电扇中有电源。转得快就需要高的电流,转得慢就需要低的电流,有一个电流保护的功能,电流过大就会警告或者断开。
紧耦合:
c#
using System;
using System.Collections;
namespace InterfaceExample {
internal class Program {
static void Main(string[] args) {
var fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
Console.ReadLine();
}
}
class PowerSupply {
public int GetPower() {
return 100;
}
}
class DeskFan {
private PowerSupply _powerSupply;
//这里就是紧耦合
public DeskFan(PowerSupply powerSupply)
{
_powerSupply = powerSupply;
}
public string Work() {
int power = _powerSupply.GetPower();
if (power <= 0){
return "Won't work.";
}else if(power < 100) {
return "Slow";
}else if(power < 200) {
return "Work fine";
} else {
return "Warning!";
}
}
}
}
使用接口松耦合:
c#
using System;
using System.Collections;
namespace InterfaceExample {
internal class Program {
static void Main(string[] args) {
var fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
Console.ReadLine();
}
}
interface IPowerSupply {
int GetPower();
}
/* 现在就是自底向上式的
* 现在发现需要一个接口来解耦
*/
class PowerSupply : IPowerSupply {
public int GetPower() {
/* 可以专门建立一个用于测试的类,
* 专门输出超出范围的电流
* 而这个过程,应该在单元测试中完成
*/
return 100;
}
}
class DeskFan {
private IPowerSupply _powerSupply;
//这里就是紧耦合
public DeskFan(IPowerSupply powerSupply)
{
_powerSupply = powerSupply;
}
public string Work() {
int power = _powerSupply.GetPower();
if (power <= 0){
return "Won't work.";
}else if(power < 100) {
return "Slow";
}else if(power < 200) {
return "Work fine";
} else {
return "Warning!";
}
}
}
}
单元测试
给测试项目添加被测试项目的依赖
不知道为什么使用xUnit
运行不成功,换了一个Unit
类型的就测试成功了。
测试项目中的代码
也就是在松耦合后,创建测试项目,然后测试原项目。
c#
using InterfaceExample;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace UnitTestProject1 {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
var fan = new DeskFan(new PowerSupplyLowerThanZero());
var expected = "Won't work.";
var actual = fan.Work();
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestMethod2() {
var fan = new DeskFan(new PowerSupplyHigherThan200());
var expected = "Warning!";
var actual = fan.Work();
Assert.AreEqual(expected, actual);
}
}
class PowerSupplyLowerThanZero : IPowerSupply {
public int GetPower() {
return 0;
}
}
class PowerSupplyHigherThan200 : IPowerSupply {
public int GetPower() {
return 220;
}
}
}
每次测试,都要创建一个类,就不太方便。
我们可以使用moq
插件来帮我们生成所需要类型的实例,这样就不用每次都去创建类了。
使用Moq
插件后的测试代码:
c#
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using Moq;
namespace MoqExample.Tests {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
//使用Moq来创建实例
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(-1);
/* 创建一个实现IPowerSupply接口的名叫ps的实例
* 然后设置ps的GetPower方法的返回值为0
* 最后的()=>0是一个lambda1表达式
* 也支持不适用表达式赋值,可以直接赋值
*/
var fan = new DeskFan(mock.Object);
var expected = "Won't work.";
var actual = fan.Work();
Assert.AreEqual(expected, actual);
}
[TestMethod]
public void TestMethod2() {
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(() => 220);
var fan = new DeskFan(mock.Object);
var expected = "Warning!";
var actual = fan.Work();
Assert.AreEqual(expected, actual);
}
}
//class PowerSupplyLowerThanZero : IPowerSupply {
// public int GetPower() {
// return 0;
// }
//}
//class PowerSupplyHigherThan200 : IPowerSupply {
// public int GetPower() {
// return 220;
// }
//}
}
显式实现接口
显式实现接口的场景:
当一个类实现了多个接口时,在不同接口中有相同的方法需要实现,这时候就可以用到显式实现接口。显式实现接口的特点是要在实现接口方法时在方法名前面加上接口名,在调用时也不能直接使用类对象的引用,而是只能使用接口的对象引用去调用,不然根本找不到相应的方法(因为有冲突)。
如果一个类实现的两个接口包含签名相同的成员,则在该类上实现此成员会导致这两个接口都将此成员用作其实现。
c#
using System;
namespace ExplicitInterface {
internal class Program {
static void Main(string[] args) {
Simple simple = new Simple();
IControl control = simple;
ISurface surface = simple;
simple.paint();
control.paint();
surface.paint();
Console.ReadLine();
//这就是对接口的实现
//这就是对接口的实现
//这就是对接口的实现
}
}
public interface IControl {
void paint();
}
public interface ISurface {
void paint();
}
class Simple : IControl, ISurface {
public void paint() {
Console.WriteLine("这就是对接口的实现");
}
}
}
若要调用不同的实现,根据所使用的接口,可以显式实现接口成员。 显式接口实现是一个类成员,只通过指定接口进行调用。 通过在类成员前面加上接口名称和句点可命名该类成员。
类成员 IControl.Paint
仅通过 IControl
接口可用,ISurface.Paint
仅通过 ISurface
可用。 这两个方法实现相互独立,两者均不可直接在类上使用。
c#
using System;
namespace ExplicitInterface {
internal class Program {
static void Main(string[] args) {
Simple simple = new Simple();
IControl control = simple;
ISurface surface = simple;
//simple.paint();
/* 类成员 IControl.Paint 仅通过 IControl 接口可用,
* ISurface.Paint 仅通过 ISurface 可用。
* 这两个方法实现相互独立,两者均不可直接在类上使用。
*/
control.paint();
surface.paint();
Console.ReadLine();
}
}
public interface IControl {
void paint();
}
public interface ISurface {
void paint();
}
class Simple : IControl, ISurface {
void IControl.paint() {
Console.WriteLine("这就是对控制接口的实现");
}
void ISurface.paint() {
Console.WriteLine("这就是对表面接口的实现");
}
}
}
-
显式接口声明中不允许使用该关键字。在这种情况下,请从显式接口声明中删除关键字。
public
public
显式接口实现没有访问修饰符,因为它不能作为其定义类型的成员进行访问。 而只能在通过接口实例调用时访问。 如果为显式接口实现指定访问修饰符,将收到编译器错误 CS0106。
-
显式接口声明中不允许使用 abstract 关键字,因为显式接口实现永远无法重写。
注:显式实现还用于处理两个接口分别声明名称相同的不同成员(例如属性和方法)的情况。 若要实现两个接口,类必须对属性 P
或方法 P
使用显式实现,或对二者同时使用,从而避免编译器错误。
在接口中声明的成员定义一个实现。 如果类从接口继承方法实现,则只能通过接口类型的引用访问该方法。 继承的成员不会显示为公共接口的一部分。 (这个功能好像是新功能,在某些版本不能使用,就不展示了,反正在微软的文档上面抄的 显式接口实现 - C# 编程指南 - C# | Microsoft Learn)
另外,显式实现接口还有一种用法,就是隐藏方法 。当一个类在实现接口的过程中不想实现其中的某个方法时 ,可以使用显式实现的方式实现此方法 ,并提供一个空实现,这样类的实例在调用方法时就看不到这个不想被使用的方法了。
c#
using System;
namespace ExplicitInterface {
internal class Program {
static void Main(string[] args) {
var simple = new SampleClass();
simple.Run();
Console.ReadLine();
}
}
public interface IControl {
void Paint();
void Run();
}
public class SampleClass : IControl {
void IControl.Paint() {
}
//隐式实现接口必须,带public修饰符
//不然会报错
public void Run() {
Console.WriteLine("韩跑跑");
}
}
}
总结
这一张中,比较重要的就是抽象类与开闭原则,还有接口与依赖反转原则了,还有单元测试。
抓主要作用,显式接口这种,不是特别重要的,可以粗略看看就行了。
承的成员不会显示为公共接口的一部分。 (这个功能好像是新功能,在某些版本不能使用,就不展示了,反正在微软的文档上面抄的 显式接口实现 - C# 编程指南 - C# | Microsoft Learn)
另外,显式实现接口还有一种用法,就是隐藏方法 。当一个类在实现接口的过程中不想实现其中的某个方法时 ,可以使用显式实现的方式实现此方法 ,并提供一个空实现,这样类的实例在调用方法时就看不到这个不想被使用的方法了。
c#
using System;
namespace ExplicitInterface {
internal class Program {
static void Main(string[] args) {
var simple = new SampleClass();
simple.Run();
Console.ReadLine();
}
}
public interface IControl {
void Paint();
void Run();
}
public class SampleClass : IControl {
void IControl.Paint() {
}
//隐式实现接口必须,带public修饰符
//不然会报错
public void Run() {
Console.WriteLine("韩跑跑");
}
}
}
总结
这一张中,比较重要的就是抽象类与开闭原则,还有接口与依赖反转原则了,还有单元测试。
抓主要作用,显式接口这种,不是特别重要的,可以粗略看看就行了。