抽象类和接口
接口和抽象类是面向对象编程中最精密的部分
软件经得起测试、程序健康状态能被监控以及发现问题后好修复都有赖于接口和抽象类的正确使用
学好solid设计原则和接口与抽象类是学习设计模式的条件
solid是五个面向对象基本设计原则的缩写S-单一职责原则,O-开放关闭原则,L-替换原则,I-接口隔离原则,D-依赖反转原则
初识:
抽象类
感性认识
先对抽象类有点感性认识:
抽象方法:被abstract修饰,没有方法体,没有花括号。也就是没有被实现的方法
一旦一个类有了抽象方法或抽象成员后,这个类就成了抽象类。那么类也必须用abstract修饰。
如图:
抽象类和开闭原则
抽象类指的是:函数成员没被完全实现的类,用abstract修饰,并且抽象成员不能用private修饰。
如果函数成员全被实现的话那么它就不是抽象类,即具体类
如果抽象成员被private修饰,就意味着不能再被访问,更不可能被实现,无意义。
编译器不允许去实例化一个抽象类,那么抽象类还剩两种作用:
①当成基类,让派生类去实现抽象方法
②抽象类作为基类可以去声明变量,让该变量去引用一个子类的实例,而子类中已经实现了基类中抽象方法。(多态)
开闭原则:指的是封装那些不变的、确定的成员,而把那些不确定、可能改变的成员声明为抽象类,留给子类去实现
开闭原则的核心思想是:软件实体(模块、类、函数等)应该对扩展开放,而对修改关闭。
这意味着我们应当尽量设计系统,使得当需要添加新的功能时,不需要去修改现有的代码,而是通过扩展已有结构来实现。
具体来说,开闭原则有三个要点:
1、开放:允许增加新的行为或功能,而不改变已有的代码结构。
2、封闭:已经完成的功能不应该因为添加新功能而被修改。
3、子类可以替代它们的基类,不影响程序的行为。
示例
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Vehicle vehicle = new Car();
vehicle.Run();
}
}
abstract class Vehicle {//基类,抽象类
public void Stop() {
Console.WriteLine("停车");
}
public abstract void Run();
}
class Car:Vehicle {
public override void Run()
{
Console.WriteLine("Car在开动");
}
}
class Truck : Vehicle
{
public override void Run()
{
Console.WriteLine("Truck在开动");
}
}
}
给Vehicle添加子类Truck不用修改原来的代码,符合开闭原则。
子类实现抽象类时,抽象成员要加override修饰符。
可以看出抽象类简直就是为基类而是的
再识:
由抽象类到接口
在这里突发奇想设置一个成员变量全是抽象的类(纯虚类)即:
abstract class VehicleBase
{
abstract public void Run();
abstract public void Fill();
abstract public void Run();
}
实际上在C#中全抽象类就是接口
现在将此代码改成接口型:
形式上abstract class替换成interface
接口要求所有成员都必须是public的,而且接口的成员都是抽象的
所以方法体中的abstract、public要删去
因为abstract去掉了,所以实现过程中的override得去掉。就成了类实现接口
interface VehicleBase
{
void Run();
void Fill();
void Run();
}
另外看下实现接口的抽象类:
abstract class Vehicle
{//不完全实现接口,留一个不实现
public void Stop()
{
Console.WriteLine("停车");
}
public void Fill()
{
Console.WriteLine("加油");
}
abstract public void Run();//不实现接口中该方法,让其子类再去实现
}
接口
接口是由抽象类进化而来,且接口里的“方法”必须是public,而抽象类的方法只要求不是private。
接口里的“方法”必须是public,所以不用写修饰符,默认是public
用接口提高代码复用性
以下将写两段代码,一段不用接口,一段接口。突出接口的作用:
现有一个整形数组,以及一整数集合ArrayList,求它们的和及平均值,请编写代码:
代码一(不使用接口):
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1, 2, 3, 4, 5 };
ArrayList nums2= new ArrayList { 1,2,3,4,5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(int[] nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += n;
}
return sum;
}
static int Sum(ArrayList nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;//因为ArrayList数据是object类型,所以要类型转换
}
return sum;
}
static double Avg(int[] nums)
{
int sum = 0; double count = 0;
foreach (var n in nums)
{
sum += n;
count++;
}
return sum / count;
}
static double Avg(ArrayList nums)
{
int sum = 0; double count = 0;
foreach (var n in nums)
{
sum += (int)n;
count++;
}
return sum / count;
}
}
}
因为数组和ArrayList是不同类型,导致要多写一段,来实现同样的功能。其实也说明了用于处理数组的方法与数组是紧耦合的,不能用来处理ArrayList集合。
第二段代码(含接口):
接口类似于合同,就是供需双方要遵守的规则,这里面需求者(求和、求平均方法)只要求供应者(数组和集合)能够迭代。而供应者满足。可用接口
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1, 2, 3, 4, 5 };
ArrayList nums2= new ArrayList { 1,2,3,4,5};
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(IEnumerable nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;//因为ArrayList数据是object类型,所以要类型转换
}
return sum;
}
static double Avg(IEnumerable nums)
{
int sum = 0; double count = 0;
foreach (var n in nums)
{
sum += (int)n;
count++;
}
return sum / count;
}
}
}
该方法现在可以接受整形数组和ArrayList两种类型,不用为不同类型各写一个方法。
IEnumerable是一个接口,在.NET框架中,它表示一个序列,可以逐个返回元素而不是一次性获取所有元素。实现了IEnumerable接口的数据结构可以被foreach循环遍历。
用接口解耦
人不是万能的,人之间需要合作。类与类也是需要合作的,那么合作就产生了依赖关系,类之间就耦合了,依赖强那么耦合就高。
耦合(Coupling)是指软件系统中两个模块之间的相互关联程度,即一个模块对另一个模块的功能、数据或状态的依赖程度。高耦合意味着如果一个模块发生变化,可能会导致其他模块也受到影响,增加维护的复杂性和困难。
解耦(Decoupling)则是设计原则之一,它的目标是降低模块间的耦合度,使得改动一个模块不会直接影响到其他模块,提高系统的灵活性和稳定性。通过良好的设计,比如使用接口、抽象类、事件驱动等技术,可以减少模块之间的直接联系。
依赖(Dependency)则是指一个模块需要另一个模块的存在才能正常运行。它描述了模块间功能上的从属关系,一个模块可能依赖于另一个模块提供的服务。然而,好的依赖应该是轻量级的,尽量避免循环依赖和过度依赖。
三者区别在于,耦合强调的是模块之间的连接紧密程度,而依赖则更侧重于功能层面的关系;解耦则是为了降低这两个概念,使得系统的各个部分能够独立变化和升级。简单来说,低耦合对应着松散的依赖,而高的模块内聚和低的模块间耦合则是理想的设计状态。
示例
以下两段代码,前段代码用来体现什么是依赖和耦合,后段代码用来体现接口的解耦。
第一段:
表示汽车依赖于发动机,汽车和发动机紧耦合,没有发动机,或者说发动机出现故障。汽车都无法正常运行。
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
var engine=new Engine();
var car = new Car(engine);
car.Run(3);
Console.WriteLine(car.Speed);
}
}
//汽车的运行依赖于发动机
class Engine//发动机
{
public int RPM { get; private set; }//发动机每分钟多少转,且无法从外界设置set
public void Work(int gas)
{ //发动机运行时,给的gas(油门)大小
this.RPM = gas * 1000;//规定发动机转速等于油门*1000
}
}
class Car//汽车
{
private Engine engine;//产生依赖,汽车必须有发动机
public Car(Engine engine)//产生汽车时,必须有发动机
{
this.engine = engine;
}
public int Speed { get; private set; }//汽车的速度
public void Run(int gas) {//汽车运行
engine.Work(gas);//汽车运行,发动机工作
this.Speed = engine.RPM/100;//运行速度
}
}
}
代码二:
引入接口,可以有效的降低耦合。
理解接口:接口就是契约,用来约束一组功能。功能的调用者是被约束的,只能调用接口中包含的功能。并且让调用者放心调用,不用担心功能内部是怎么实现的。
老型手机,对于用户来说,可以去放心使用它的打电话、发短信、接电话,收短信功能,不用管它怎么实现的。对于厂商来说,必须要实现手机的打电话、发短信、接电话,收短信功能。
而丢了诺基亚的手机,不会让用户不会使用爱立信手机。因为这些手机都是那几个主要功能。所以手机和用户是解耦的。
using System;
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
var user=new User(new NoliaPhone());
//如果诺基亚手机坏了,那么只需修改上方的NoliaPhone为ErissonPhone
//就变成用爱立信手机。无需该其他地方
user.UsePhone();
}
}
class User {
private Iphone _iphone;
public User(Iphone iphone) {
_iphone = iphone;
}
public void UsePhone()
{
_iphone.Dail();
_iphone.Receive();
_iphone.Send();
_iphone.PickUp();
}
}
interface Iphone
{
void Dail();
void PickUp();
void Send();
void Receive();
}
class NoliaPhone : Iphone
{
public void Dail()
{
Console.WriteLine("诺基亚手机正在打电话");
}
public void PickUp()
{
Console.WriteLine("诺基亚手机正在接电话");
}
public void Receive()
{
Console.WriteLine("诺基亚手机正在收短信");
}
public void Send()
{
Console.WriteLine("诺基亚手机正在发短信");
}
}
class ErissonPhone:Iphone
{
public void Dail()
{
Console.WriteLine("爱立信手机正在打电话");
}
public void PickUp()
{
Console.WriteLine("爱立信手机正在接电话");
}
public void Receive()
{
Console.WriteLine("爱立信手机正在收短信");
}
public void Send()
{
Console.WriteLine("爱立信手机正在发短信");
}
}
}
总结
像后面的代码,一个类坏了,只需修改让另一个类替换(学了反射,这也不用改),相关耦合类仍能正常使用,松耦合。而前面的代码,一类坏了,另一个类就不能用了,除非调试找出bug。可知:接口就是让功能的提供方可以被替换。所以有替换的地方,就有接口。
依赖反转原则和单元测试
解耦在代码中的表现就是依赖反转,单元测试是依赖反转的应用者和受益者。
自顶向下逐步求精(紧耦合),是人解决问题的一种思维方式。在非面向对象编程里,就是函数之间的关系。在面向对象编程中就是,类之间的关系。如图:
依赖反转(依赖倒置)思维是来平衡这种单一的思维方式
左图是不用依赖倒置原则、接口。也就是自顶向下思维。上面的司机依赖于下面的车
图示意思:Diver司机有个Car类字段。当司机要开车时,调用Drive方法。Drive就调用Car中的Run方法。其他两个同理。
那么此时我让Driver司机调用卡车Truck方法能行吗?很明显传不过去,因为Driver只有Car字段,也就是Driver类和Car紧耦合。
此时我就是让Driver司机能调用卡车Truck,如何实现?右图即可
右图用来依赖倒置原则、接口。下面的类都依赖于上面要实现的接口。这就是依赖反转。
图示中:有个IVehicle接口,让Car类、Truck类都实现接口的Run方法。并且让Driver中的字段不再是具体的(如Car、Truck),而是IVehicle类型的字段。那么我们可以声明个IVehicle类型的变量,再实例Car或Truck类型(多态),调用它们中的方法。那么就实现了Driver可以用Car的Run,或Truck的Run.
多个(服务的使用者),和多个(服务的提供者),都遵循同一个接口,就能实现多种配对。
如下:Car类和Truck类是接口中Run方法的提供者。而Driver和AiDriver类是使用者。就可以实现多种配对方式,如Driver用Car、AiDriver用Car、Driver用Truck、AiDriver用Truck。
解释图示:由上方右图升级而来,变化之处在于:左边是一个抽象父类,外加两个子类Driver、AiDriver,让Driver中的字段不再是具体的(如Car、Truck),而是IVehicle类型的字段。那么我们可以声明个IVehicle类型的变量,再实例Car或Truck类型(多态),然后传入使用者的构造方法中,就可以调用拥有者的方法。
示例
目的:展示依赖反转、接口、解耦是怎样被单元测试所应用的。
该项目代码描述的是生成电扇的厂商。电扇转的快,需要大电流。转的慢,则需要小电流。同时,电流过大,就会给出安全警告:
using System;
namespace ConsoleApp3
{
internal class Program
{
static void Main(string[] args)
{
var fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
}
}
class PowerSupply{//电源
public int GetPower() {//一次给100伏特
return 100;
}
}
class DeskFan { //风扇
private PowerSupply _powerSupply;
public DeskFan(PowerSupply powerSupply)//建立依赖,传递电源构造风扇
{
_powerSupply = powerSupply;
}
public string Work()//根据电流大小反应工作状态
{
int power=_powerSupply.GetPower();
if (power <= 0)
{
return "电扇关闭";
}
else if (power <= 100)
{
return "一档风力";
}
else if (power <= 200)
{
return "二档风力";
}
else {
return "电流过大,危险!!!";
}
}
}
}
该代码的问题:给不同的电源值,需要在代码里修改,这是极其不好的。
接口设置一般是自底向上(重构),还有高手用的自顶向下(设计)。
这里用用自底向上设置接口,来解耦
添加接口,实现接口:
public interface IPowerSupply
{//电源接口
int GetPower();
}
public class PowerSupply:IPowerSupply{//电源
public int GetPower() {//一次给100伏特
return 100;
}
}
添加测试项目,添加依赖,可以在测试项目中实现接口,提供任意电源值,测试程序能否正常运行。如果测试代码没问题,但是测试结果不成功,说明源代码有问题可以改
using ConsoleApp3;
namespace TestProject1
{
public class DeskFanTest1
{
[Fact]//特征、特性
public void Test1()//测试为0的电力
{
var fan = new DeskFan(new le_text1());
Console.WriteLine(fan.Work());
}
}
class le_text1 : IPowerSupply//需要产生电力为0的电源,实现接口就行。不需要改代码
{
public int GetPower()
{
return 1;
}
}
}
下载第二个,可以直接创建实现了我们接口方法的实例,越过创建类的这一步。
就可以把代码改成如下,简化
using ConsoleApp3;
using Moq;
using System.Net.NetworkInformation;
//第一步:引入Moq名称空间
//第二步:new一个Mock<IPowerSupply>(),将IPowerSupply接口传入
namespace TestProject1
{
public class DeskFanTest1
{
[Fact]//特征、特性
public void Test1()//测试为0的电力
{
// 添加如下两行
var mork = new Mock<IPowerSupply>();
mork.Setup(ps=>ps.GetPower()).Returns(()=>0);
// var fan = new DeskFan(new le_text1());修改为以下
var fan = new DeskFan(mork.Object);
var expected = "电扇关闭";
var actual = fan.Work();
Assert.Equal(expected, actual);
}
}//下面的类可以删除了
//class le_text1 : IPowerSupply//需要产生电力为0的电源,实现接口就行。不需要改代码
//{
// public int GetPower()
// {
// return 0;
// }
//}
}
几种方法归纳图示:
版权归原作者 陌上明苏 所有, 如有侵权,请联系我们删除。