(四)继承与组合

1.松耦合设计思想

高内聚、低耦合是衡量一个软件代码的标准,可以判断软件设计的好坏。

耦合,coupling,是一种测量各种类和子系统关系的方法。
image
如何实现低耦合:

  • encapusolate 封装,对业务逻辑实现细节的隐藏。
  • 类的关联性。类之间的关系错综复杂,有依赖、关联、聚合、组合、泛化等等。
  • 使用接口。接口是面向对象开发的思想核心,而面向接口编程则是解决耦合问题的基本思路。

2. 【拓展】类关系与UML

参考文献:https://blog.csdn.net/K346K346/article/details/59582926

在学习面向对象设计时,类关系涉及依赖、关联、聚合、组合和泛化这五种关系,耦合度依次递增。关于耦合度,可以简单地理解为当一个类发生变更时,对其他类造成的影响程度,影响越小则耦合度越弱,影响越大耦合度越强。

2.1 依赖(Dependency)

依赖关系使用虚线加箭头表示,如下图所示:
image

学生在学习生活中经常使用电脑,于是对电脑产生了依赖。依赖关系是五种关系中耦合最小的一种关系。类A要完成某个功能引用了类B,则类A依赖类B。依赖在代码中主要体现为类A的某个成员函数的返回值、形参、局部变量或静态方法的调用,则表示类A引用了类B。以Student类和Computer类为例,C#代码如下:

public class Computer {
	public static void start(){
        Console.WirteLine("电脑正在启动");
	} 
};
public class Student {
	//返回值构成依赖
	public Computer program();
	
    //形参构成依赖
	public void program(Computer computer);

	public void playGame() {
        //局部变量构成依赖
		Computer computer = new Computer();
		...
		//静态方法调用构成依赖
		Computer.star();
	}
};

2.2 关联(Association)

关联关系使用实线加箭头表示,类之间的关系比依赖要强。学生与老师是关联的,学生可以不用电脑,但是学生不能没有老师。如下图所示:
image
关联与依赖的对比:

  • 相似之处:
    关联暗示了依赖,二者都用来表示无法用聚合和组合表示的关系。
  • 区别:
  1. 发生依赖关系的两个类都不会增加属性。其中的一个类作为另一个类的方法的参数或者返回值,或者是某个方法的变量而已。
    发生关联关系的两个类,类A成为类B的属性,而属性是一种更为紧密的耦合,更为长久的持有关系。 在代码中的表现如下:
public class Teacher {
    ...
};

public class Student {
    public Teacher teacher;  //成员变量
    public void study();
};
  1. 从关系的生命周期来看,依赖关系是仅当类的方法被调用时而产生,伴随着方法的结束而结束。关联关系当类实例化的时候产生,当类对象销毁的时候关系结束。相比依赖,关联关系的生存期更长。

关联关系有单向关联、双向关联、自身关联、多维关联等等。其中后三个可以不加箭头。
单向关联:
image

双向关联:
image

自身关联:
image

多维关联:
image

2.3 聚合(Aggregation)

聚合关系使用实线加空心菱形,用来表示集体与个体之间的关联关系。例如班级与学生之间存在聚合关系,类图表示如下:
image

聚合关系在代码上与关联关系表现一致,类Student将成为类Classes的成员变量。代码如下:

public class Student {
    ...
};

public class Class {
    private Student _student;  //成员变量
    public Class(Student student) {
        _student = student
    }
};

2.4 组合(复合,Composition)

复合关系使用实线加实心菱形表示。组合又叫复合,用来表示个体与组成部分之间的关联关系。例如学生与心脏之间存在复合关系,类图表示如下:
image
组合关系在代码上与关联关系表现一致,类Heart将成为类Student的成员变量。代码如下:

public class Heart {
    ...
};

public class Student {
    private StudeHeartnt _heart;  //成员变量
    public Student() {
        _heart = new Heart();
    }
};

聚合与组合的对比:

  1. 聚合关系没有组合紧密。
  • 学生不会因为班级的解散而无法存在,聚合关系的类具有不同的生命周期;而学生如果没有心脏将无法存活,组合关系的类具有相同的生命周期。
  • 这个从构造函数可以看出。聚合类的构造函数中包含另一个类的实例作为参数,因为构造函数中传递另一个类的实例,因此学生可以脱离班级体独立存在。组合类的构造函数包含另一个类的实例化。因为在构造函数中进行实例化,因此两者紧密耦合在一起,同生同灭,学生不能脱离心脏而存在。
  1. 信息的封装性不同。
  • 在聚合关系中,客户端可以同时了解Classes类和Student类,因为他们是独立的。
  • 在组合关系中,客户端只认识Student类,根本不知道Heart类的存在,因为心脏类被严密地封装在学生类中。

理解聚合与复合的区别,主要在于聚合的成员可独立,复合的成员必须依赖于整体才有意义。

2.5 泛化(Generalization)

泛化是学术名称,通俗来讲,泛化指的是类与类之间的继承关系和类与接口之间的实现关系。

继承关系使用直线加空心三角形表示。类图结构如下:
image

类接口的实现关系使用虚线加空心三角形表示。类图结构如下:
image

2.6 小结

  • 依赖、关联、聚合、组合与泛化代表类与类之间的耦合度依次递增。
  • 依赖关系实际上是一种比较弱的关联,聚合是一种比较强的关联,组合是一种更强的关联,泛化则是一种最强的关联,所以笼统的来区分的话,实际上这五种关系都是关联关系。
  • 依赖关系比较好区分,它是耦合度最弱的一种,在编码中表现为类成员函数的局部变量、形参、返回值或对静态方法的调用。
  • 关联、聚合与组合在编码形式上都以类成员变量的形式来表示,所以只给出一段代码我们很难判断出是关联、聚合还是组合关系,我们需要从上下文语境中来判别。
  • 关联表示类之间存在联系,不存在集体与个体、个体与组成部分之间的关系。
  • 聚合表示类之间存在集体与个体的关系。
  • 组合表示个体与组成部分之间的关系。
  • 依赖、关联、聚合与组合是逻辑上的关联,泛化是物理上的关联。物理上的关联指的是类体的耦合,所以类间耦合性最强。

3. 继承

  • 继承描述的是两个class之间的关系,
  • 通过继承可以允许一个类获得另一个类的所有代码。
  • 通常我们使用 is-a来描述他们之间的关系

3.1 继承的好处

  1. 省去绝大部分重复性的代码,提高了代码的复用性。
  2. 继承还产生了多态,也就是可以在继承对象的某一个行为具有多个不同表现形式或形态的能力

3.2 UML

image

3.3 语法结构

  1. 首先,需要创建一个基础类,PresentationObject,用来处理所有显示对象公有的属性和行为。
  2. 然后,我们继续创建一个文字类Text class,继承与PresentationObject。在c#中,继承关系使用冒号来表示,冒号后面连接父类

4. 复合

好处
与继承一样,类的复合也同样可以解决代码复用的问题,而且从某种意义上来说,类复合的灵活度更高;通过类复合,我们可以完成对象之间的依赖注入,用松耦合的开发思路设计出各种复杂的系统,非常适合大型项目的开发。

例子:
image
UML
image

  • 首先,在DbMigrator class中创建一个私有的、logger类型的成员字段,我们可以叫它 _logger。
  • 然后,创建构造函数,把logger系统从外部、通过参数传递到数据库迁移系统内部,并且赋值给私有成员变量 _logger。
  • 在实际工作中,我们都会在引用对象的过程中加上readonly,把这种引用关系确定下来,防止意外的产生。

5.访问修饰符(protected与internal)

protected
先看定义:protected,中文保护,只要是被protected声明的变量或者方法,只能被自己访问、或者被继承于自己的子类访问,其他一切外部操作都无法访问。

internal:

  1. 打开项目的文件管理器,鼠标右键点击解决方案,选择添加,新项目,然后选择类库,也就是libriay项目。在类库项目编译过程中将会生成DLL文件,而这个dll可以被其他项目动态引用。给这个类库起个名字,叫做Carlibray,然后创建。这个新项目就创建成功了。
  2. 我们把Car class剪切到 car 类库中。同样的方法,把五菱宏光也剪切复制到car 类库中。
  3. 回到main方法,现在,我们看到代码报错了,这是因为新项目还没有建立引用关系。请同学们右键单击helloworld项目,选择添加,然后添加项目引用。
  4. 勾选 carlib,然后确定。现在,两个项目的引用关系就建立起来了。 如果展开文件浏览器中hellochat项目的依赖项,我们就能看见依赖添加成功了。
  5. 然后,回到main方法,在文件的顶部,我们使用using来引入carlib的命名空间。现在,报错就解决了。

使用了 internal 修饰符以后,相关的代码只能在同一个程序集或者说同一个项目中使用,任何一个外部项目都没办法从外部对它进行访问。

同样的道理,我们甚至可以给项目中一个普通的方法或者字段也加上internal的限制。比如,五菱宏光还原public,不过把漂移方法改为internal,回到main方法,现在,五菱宏光初始化不报错了,给五菱宏光加速也可以访问,直到执行内部方法漂移的时候才会报错。

6.构造函数的继承

  • 第一,在初始化一个对象的时候,基类base class的构造方法总是会首先运行。
  • 第二,基类的构造方法不会被继承,在派生类中需要我们重新定义新的构造方法。
public class Staff
{
    public Staff()
    {
        Console.WriteLine("员工类初始化");
    }
}

public class Manager : Staff
{
    public Manager()
    {
        Console.WriteLine("经理类初始化");
    }
}

子类父类的构造方法都执行了

public class Staff
{
    public Staff()
    {
        Console.WriteLine("员工类初始化");
    }

    public int Number { get; set; }
    public Staff(int number)
    {
        Number = number;
    }
}

public class Manager : Staff
{
    public Manager()
    {
        Console.WriteLine("经理类初始化");
    }
    public Manager(int number)
    {
 Console.WriteLine("{number}经理初始化");
    }
}

image

父类无参构造执行,有参构造未执行,子类有参无参构造均执行
image
image
有了base正确选择了需要继承的构造方法
base解释
base就是基础、代表的就是基类

  • 使用 base 关键字可以从派生类中访问基类的成员:
  • 指定创建派生类实例时应调用的基类构造函数。
  • 仅允许基类访问在构造函数、实例方法或实例属性访问器中进行。
  • 注意:静态方法中不能使用 base 关键字。

7. 向上转型与向下转型

  • 向上转型(upcasting)指的是把一个派生类类型转换为他的基类。
  • 向下转型(downcasting)则恰好相反,指的是把一个基类转换为他的某一个派生类。
  • 在c#中,我们需要使用as关键词来完成类型转换。

image

  • as
    image
    当我们使用as来转化对象的时候,如果转化失败,编译器不会在这里抛出异常,而是把转化失败的对象car设置为悬空指针null。
    然后,我们就可以根据转化对象的数据是否为空来进行接下来的逻辑处理了。
  • is
    使用is关键词,我们可以检查对象的类型。通过先进行类型验证,再执行对象转换,这样可以极大的提高我们代码的安全程度。

类代码

public  class  Shape
{
public  int  Width { get; set; }
public  int  Height { get; set; }
public  int  X { get; set; }
public  int  Y { get; set; }
public  void  Draw()
{
Console.WriteLine($"Width: {Width}, Height: {Height}, position: ({X}, {Y})");
}
}


public  class  Text : Shape
{
public  int  FontSize { get; set; }
public  string  FontName { get; set; }
}

C#的向上转换非常强大,在运行过程中shape和text对象都同时指向同一个内存地址,更准确的来说,shape和text对象都引用了同一个地址,也就是说他们所代表的数据是一模一样的。但是,同样的数据却展现出了不同的表现形式。

当我们使用text的时候,可以访问它所包含的所有属性,比如继承与基类的长宽数据、以及坐标信息,也可以访问text class所独有的属性,比如字体和大小。

q:在我们真是开发中,经常还没想好数据类型就要开始写代码了,所以这时候我们更希望的是列表能有更高的宽容度,让我们能存放各种各样的数据。

  • 于是,c#就推出了一种能够保存动态数据类型的列表,ArrayList。
  • 使用ArrayList需要引入System.Collection这个命名空间。
  • 而这个ArrayList可以承载的元素的类型就是Object对象类型。
  • 而ArrayList的实现原理就是我们刚刚学到的向上转型(upcasting)。
  • 所以通过向上转型,可以让我们的代码更加灵活,语言处理更加丰富,而且还能带给我们更加强大的容错能力。

转型操作代码

	//向上
	var text = new Text();
	Shape shape = text;

	//ArrayList什么都能装
	var array =new ArrayList();
	array.Add(1);
	array.Add("dddd");
	array.Add(shape);
	array.Add(text);
	//List泛型列表,然后使用泛型来制定每一个元素的类型
	//每一个元素的类型都是shape。
	var shapeList =new List<Shape>();
	shapeList.Add(new Shape());
	shapeList.Add(text);

	//向下转型
	Shape shap1 = shapeList[1];//明明是text现在放进去shape列表就变成了shape

	var textList=new List<Text>();
	shapeList.ForEach(s =>
	{
		if (s is Text)//用is增强代码健壮性
		{
			textList.Add((Text)s);//显式向下转
		}
	});

8.拆箱与装箱

8.1 值类型和引用类型

在c#中所有的类型被分为两种,值类型和引用类型。程序在执行的过程中会把内存被分为若干个区域,其中最常见也最重要的两区域就是stack和heap两个部分。

值类型

  • stack的内存空间有限,由编译器自动分配,用来保存最简单最基本的数据类型。
  • 值类型的数据值就保存在这里,值引用的数据类型包括byte、int、float、char、bool在内的基本数据类型以及struct这些结构简单、长度有限的数据类型。
  • 保存在stack中数据的生命周期一般都比较短,在程序运行过程中,一旦保存在stack中的数据使用完毕则会立刻被编译器自动清理删除掉,腾出空间给保存接下来的数据。

引用类型

  • 引用类型则保存在heap内存中
  • 保存在heap中的数据长度是不固定的,数据类型、数据结构也非常复杂
  • 而保存在heap中的数据生命周期也不固定,什么时候释放内存完全取决于程序猿的设计。不过对于类型java和c#这样的高级语言,它们有自动垃圾回收机制
  • 一般来说,任何对象、任何一个类,一个class都会保存在heap中。

Why?
虽然,对于类似c#或java这样的高级语言来说,值类型和引用类型在使用过程中并没有感受到明显的不同,但他们却是机器语言最基本的概念之一。那他们为什么这么重要?
image

8.2 装箱boxing

  • 从一个值类型转化一个引用类型的过程,我们就把它称为装boxing。
int i = 10;
object o = i;
// or
Object o2 = 10;

image

8.3 拆箱

  • 而拆箱unboxing就是完全相反的过程,就是把基本数据类型从对象中打开取出来。
object obj =10;
int number =(int) obj;
  • 当一个对象类型转化为基本数据类型的时候,这个数据就会从内存的heap部分转移到stack中。

8.4 对比

  • 不管是装箱还是拆箱,都会有比较明显的性能问题
  • 因为这些操作都涉及额外的对象的创建和销毁。

image

在实际工作中进行数据类型转化的时候一定要注意装箱和拆箱的所带来的性能问题,尽量使用泛型来取代装箱和拆箱的工作。

posted @ 2023-09-13 16:47  huihui不会写代码  阅读(109)  评论(1)    收藏  举报