面向对象设计的基本概念 面向对象设计的概念



文章插图
面向对象设计的基本概念 面向对象设计的概念

文章插图
主流的编程范式有三种:面向过程、面向对象和函数式编程 , 我们现在使用的主流编程语言 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 接口 , 不管你的代码时如何设计和分层 , 一层层往下知道数据库访问 。
所以不要以为使用了面向对象的语言就是在使用面向对象编程 , 重要的是抽象的思维 , 这种抽象需要我们去思考 , 去全盘考虑 , 相比较面向过程显得更难 , 所以懒惰的程序员更容易写出面向过程的代码 。
【面向对象设计的基本概念 面向对象设计的概念】这些面向对象的基础知识是学习设计模式的根基 , 掌握基础知识 , 然后愿意去思考 , 总结才能够学习好设计模式 , 并将其应用到实际的工作中 。下一篇将介绍面向对象中的常用设计原则 , 设计模式也都是基于这些设计原则演化而来 。