欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  新闻

面向对象设计思想是什么(面向对象方法的基本思想)

程序员文章站 2022-09-09 08:13:35
主流的编程范式有三种:面向过程、面向对象和函数式编程,我们现在使用的主流编程语言 c# 或 java,都是面向对象语言,所以常常说的设计模式也是在面向对象语言这个前提之下。面向对象的基础知识和一些设计...

主流的编程范式有三种:面向过程、面向对象和函数式编程,我们现在使用的主流编程语言 c# 或 java,都是面向对象语言,所以常常说的设计模式也是在面向对象语言这个前提之下。

面向对象的基础知识和一些设计原则,我认为是学习设计模式的基础,本文就聊下这些基础知识。

在面试时,一问到面向对象,几乎每个人都能脱口而出:封装、继承、多态。但大部分只能说出一个简单的概念,而多态还有很多连概念都说不清楚。我们学习面向对象,不止需要了解概念,更需要知道每个特性存在的意义和目的。

对于面向对象的特性,面向对象的语言都会给出相应的支持,不同语言可能会有细微差别,下面的示例以 c# 语言为主。

封装

我们先来思考下,平时写代码时有哪些是属于封装,是不是会有下面的一些场景:

1、将一些属性字段放到一个类中;

2、将一些方法放到一个类中

3、将某些类组织到某个特定的命名空间下。

而在 c# 9.0 版本中还提供了属性的 init 特性,可以更方便地提供封装性:

public class userinfo
{
    public string name { get; init; }
}
userinfo user = new userinfo { name = "oec2003" };
//当 user 初始化完了之后就不能再改变 name 的值
user.name = "oec2004";

除了属性、方法和类也有对应的访问修饰符,这些访问修饰符的灵活运用就达到了封装的目的,用来隐藏信息或进行数据的保护。

试想一下,如果我们对类中属性或方法全部都使用 public ,调用方可以任意修改属性和调用方法,这样会使代码变得不可控,属性可能被很多地方以不同的方式进行修改,代码难以维护。而且不熟悉业务的开发人员如果随意改动了一些关键属性,可能引发严重的问题。

从另一个方面来说,类的共有属性和方法暴露的越多,对于调用者来说就会越复杂,越容易出现问题,合理地进行封装,可以提高可读性、可维护性,减少出错。

这时,你是不是可以想想,平时写代码时,属性、方法、类如果要让外部进行调用,都统一写上 public 了呢?

继承

目前面向对象的语言基本都支持继承特性,只是语法上有些细微的差别,比如 c# 语言是使用冒号,java 语言使用 extends 关键字。但都是标识 is-a 的关系。

在 c# 中一个类可以继承多个接口,但只能继承一个父类,我们通常说的 c# 只支持单继承指的是 c# 只能继承一个父类,但在 c++ 、python 等语言中类是可以继承多个类的。

我们经常会跟开发人员讲,不要到处复制代码,代码要做到能够复用,发现同一个逻辑在两个不同的类中的时候,可以抽象出来一个父类,让这两个类继承这个父类。这个思路没有问题,也确实能解决我们的实际问题,提升代码质量。

但随着功能的增加,我们需要对类的属性和方法进行扩展,会发现需要新添加的属性或方法放在父类或子类都不合适,只能继续进行抽象,长此下去,继承关系会变得非常复杂,变得难以维护。有条设计原则是这么说的:组合优于继承,其实就是为了解决这个问题。

组合和继承的选择是一种权衡和选择,当涉及的类经常变化可能导致继承层级向着复杂化演化时,需要考虑采用组合的方式,如果相关类比较稳定,继承层级不深(一般不超过 3 层),就可以放心使用继承。

在具体的模式中,组合模式、策略模式等就是使用组合的方式实现,模板模式使用的是继承方式实现。

多态

多态的字面意思就是同样的一个语法调用,能够表达多个不同的意思。如果说继承的最大好处是复用,那么多态的好处就是方便扩展。

在 c# 语言中两个比较典型的多态场景就是方法的重写和方法的重载:

  • 重写:存在继承关系的类或接口,在子类中对父类的方法进行重新构建逻辑,但调用方法、参数、返回值保持一致,通常有下面几种情况: 普通的父类中有用 virtual 关键字标识的虚方法,在子类中使用 override 关键字进行重写;子类对抽象类的抽象方法进行重写;子类对接口中的方法进行实现。
  • 重载:类中的多个方法,方法名相同,但参数个数或类型不相同,称之为重载方法。例如 c# 中的 file 类的 open 方法就有三个重载,如下图:
面向对象设计思想是什么(面向对象方法的基本思想)

方法的重写,在实际应用中非常常见,比如零代码平台中的消息组件会有多种发送消息的方式,下面用一个示例代码演示下:

public interface imessage
{
    void send(string msg);
}

public class emailmessage : imessage
{
    public void send(string msg)
    {
        console.writeline($"send email message {msg}");
    }
}
public class wechatmessage : imessage
{
    public void send(string msg)
    {
        console.writeline($"send wechat message {msg}");
    }
}
class program
{
    static void main(string[] args)
    {
        list<imessage> messagelist = new list<imessage>();
        messagelist.add(new emailmessage());
        messagelist.add(new wechatmessage());
        
        messagelist.foreach(s=>s.send("test message"));
    }
}

为什么说能提高扩展性呢?如果这时消息组件需要扩展发送短信的消息种类,只需要编写短信类型的消息类实现 imessage 接口的 send 方法即可。

还有一种场景,比如登陆的时候,有基于用户名密码的认证、企业微信的认证、钉钉的认证、和对接第三方的认证,又应该怎么设计呢?

我们虽然都在使用着面向对象的语言,但很多的时候思维还是面向过程的,具体体现在:

  • 实体类的属性直接定义为 public ,set 和 get 都安排上,外部可以任意获取和赋值,很多时候使用代码生产工具直接生产实体类,默认的 set 和 get 都是 public ,也没有去依据具体的业务进行修改,严重破坏了封装特性;
  • 数据和行为的分离,也就是所谓的贫血模式,但真正的对象是数据行为在一起的,我们可能每天都在写这样的代码,一种面向过程式的代码;
  • 为了代码复用,代码中会存在大量的 helper 类或者 utils、common 类,这些类通常是静态类,里面有各种各样的静态方法,在往里面添加方法时需要思考下,真的需要放到这里吗?
  • 按照功能驱动,比如页面上的一个按钮操作,对应了一个 api 接口,不管你的代码时如何设计和分层,一层层往下知道数据库访问。

所以不要以为使用了面向对象的语言就是在使用面向对象编程,重要的是抽象的思维,这种抽象需要我们去思考,去全盘考虑,相比较面向过程显得更难,所以懒惰的程序员更容易写出面向过程的代码。

这些面向对象的基础知识是学习设计模式的根基,掌握基础知识,然后愿意去思考,总结才能够学习好设计模式,并将其应用到实际的工作中。下一篇将介绍面向对象中的常用设计原则,设计模式也都是基于这些设计原则演化而来。