图片中的OOP

OOP(面向对象编程)已经成为许多现代项目开发中不可或缺的一部分,但是尽管流行,但这种范例还远远不是唯一的。 如果您已经知道如何使用其他范例,并且想熟悉OOP的神秘性,那么前面还有一点长远,还有两兆字节的图片和动画。 变压器将作为示例。



要做的第一件事是为什么? 面向对象的思想是为了将实体的行为与其数据以及项目实际对象和业务流程连接到程序代码中而进行的。 人们认为这样的代码更容易被人阅读和理解,因为人们倾向于将周围的世界感知为相互影响的,符合特定分类的多个对象。 思想家是否有可能实现这一目标,很难一概而论,但事实上,我们有很多项目需要程序员进行OOP。

您不应该认为OOP会以某种方式奇迹般地加快程序编写的速度,并期望Villaribo的居民已经推出OOP项目并开始工作,而Villabaggio的居民仍在清洗繁琐的意大利面条代码。 在大多数情况下,情况并非如此,节省的时间不是在开发阶段,而是在支持阶段(扩展,修改,调试和测试),也就是说,从长远来看。 如果您需要编写不需要后续支持的一次性脚本,那么此任务中的OOP很可能没有用。 但是,大多数现代项目生命周期的重要部分恰恰是支持和扩展。 单独使用OOP并不能使您的体系结构完美无缺,反之亦然会导致不必要的复杂性。

有时您可能会遇到对OOP程序性能的批评。 确实存在少量开销,但它微不足道,以至于在大多数情况下可以忽略,而转而使用优点。 但是,在瓶颈中每秒要在一个线程中创建或处理数百万个对象的瓶颈中,至少应修改对OOP的需求,因为即使这种数量的最小开销也会显着影响性能。 分析将帮助您捕获差异并做出决定。 例如,在其他情况下,如果速度的最大份额取决于IO,则放弃对象将是过早的优化。

就其本质而言,最好用示例来说明面向对象的编程。 如所承诺的,我们的患者将成为变形金刚。 我不是变形金刚,也没有读过漫画,因此,在示例中,我将以维基百科和幻想为指导。

类和对象


立刻抒情离题:不使用类就可以实现面向对象的方法,但是我想为双关语道歉,经典的方案,即类是我们的一切。

最简单的解释是:一个类是一个转换器的图形,而该类的实例是特定的转换器,例如Optimus Prime或Oleg。 尽管它们是根据一张图纸组装的,但是它们可以以相同的方式行走,变形和射击,它们都有自己独特的状态。 状态是一系列变化的属性。 因此,在同一个类别的两个不同对象中,我们可以观察到不同的名称,年龄,位置,充电级别,弹药量等。这些属性及其类型的存在本身在该类别中进行了描述。

因此,类是对对象将拥有的属性和行为的描述。 对象是具有这些属性自身状态的实例。

我们说的是“属性和行为”,但这听起来有点抽象和难以理解。 对于程序员来说,听起来更像这样:“变量和函数”。 实际上,“属性”是相同的普通变量,它们只是某些对象的属性(它们称为对象字段)。 类似地,“行为”是对象的功能(它们称为方法),也是对象的属性。 对象的方法和通常的功能之间的区别仅在于方法可以通过字段访问其自身的状态。

总的来说,我们有作为属性的方法和属性。 如何使用属性? 在大多数PL中,属性引用运算符就是重点(PHP和Perl除外)。 看起来像这样(伪代码):

//       class class Transformer(){ //   x int x //    (     0) function constructor(int x){ //   x // (  0    ) this.x = x } //   run function run(){ //      this this.x += 1 } } //    : //        0 optimus = new Transformer(0) optimus.run() //    print optimus.x //  1 optimus.run() //      print optimus.x //  2 

在图片中,我将使用以下符号:



我没有使用UML图,尽管它们更灵活,但考虑到它们在视觉上的不足。


动画编号1

我们从代码中看到什么?

1. 是一个特殊的局部变量(方法内部),它允许对象从其方法访问其自己的属性。 我提请您注意的是,仅当您自己时,即在转换器调用其自己的方法或更改其自己的状态时。 如果调用从外部看起来像optimus.x ,那么从内部开始,如果Optimus希望自己引用他的字段x,则在他的方法中调用将看起来像this.x ,即“ 我(Optimus)引用我的属性x ”。 在大多数语言中,此变量称为this,但也有例外情况(例如self)

2. 构造函数是一种特殊的方法,在创建对象时会自动调用该方法。 构造函数可以接受任何参数,就像其他任何方法一样。 在每种语言中,构造函数均以其名称表示。 这些在某些地方是特殊保留的名称,例如__construct或__init__,而在某些地方构造函数的名称必须与类的名称匹配。 构造函数的目的是初始化对象,填写必填字段。

3. new是必须用于创建类的新实例的关键字。 此时,将创建一个对象并调用构造函数。 在我们的示例中,将0传递给构造函数作为变压器的起始位置(这是上面的初始化)。 在某些语言中缺少new关键字,并且在尝试将类作为函数调用时会自动调用构造函数,例如:Transformer()。

4.构造函数和run 方法 使用内部状态,但是在所有其他方面与普通函数没有区别 。 甚至声明的语法都匹配。

5.类可能拥有不需要状态的方法,因此可以创建一个对象。 在这种情况下,该方法被设置为static

Srp


(单一责任原则/第一SOLID原则)。 您可能已经从其他范例中熟悉了它:“一个功能只能执行一个已完成的动作”。 该原则对类也有效:“一个类必须负责任何一项任务。” 不幸的是,对于班级来说,定义违反该原则需要越界的难度更大。

尝试通过用一个句子不加任何形式描述一类的目的来形式化此原则,但这是一种很有争议的技术,因此请相信您的直觉,不要急于求成。 您不需要从一门课上制造一把瑞士刀,但是用一种方法生产一百万课也很愚蠢。

协会


传统上,在对象的字段中,不仅可以存储标准类型的普通变量,还可以存储其他对象。 这些对象又可以存储其他一些对象,依此类推,从而形成对象的树(有时是图形)。 这种关系称为关联。

假设我们的变压器装有枪。 虽然没有,但最好用两把枪。 每只手。 枪支是相同的(它们属于同一类,或者,如果您愿意,根据一张图纸制造),两者都可以以相同的方式射击和装填,但是每支枪都有自己的弹药储存库(自己的状态)。 现在如何在OOP中描述它? 通过协会:

 class Gun(){ //    int ammo_count //    function constructor(){ //  this.reload() //    "" } function fire(){ //    "" this.ammo_count -= 1 //      } function reload(){ //   "" this.ammo_count = 10 //     } } class Transformer(){ //    Gun gun_left //   " "   Gun gun_right //   " "    /*            ,    */ function constructor(Gun gun_left, Gun gun_right){ this.gun_left = gun_left //      this.gun_right = gun_right //      } //    "",   ... function fire(){ //  ,    "" this.gun_left.fire() //    ,     "" this.gun_right.fire() } } gun1 = new Gun() //    gun2 = new Gun() //    optimus = new Transformer(gun1, gun2) //  ,     


动画编号2

this.gun_left.fire()和this.gun_right.fire()是对子对象的调用,它们也通过点进行。 首先,我们转到自己的属性(this.gun_right),获取枪支对象,在第二点,我们转到枪支对象的方法(this.gun_right.fire())。

底线:制造了机器人,发出了服务武器,现在我们将弄清楚这里发生了什么。 在此代码中,一个对象已成为另一对象的组成部分。 这是协会。 反过来,它有两种类型:

1. 组成 -在变压器工厂收集擎天柱时,两把枪都用钉子牢牢地钉在他的手上,在擎天柱死后,这些枪与他一起死了。 换句话说,孩子的生命周期与父母的生命周期相同。

2. 集合 -枪支以枪支形式发行,在擎天柱去世后,可以由他的奥列格(Oleg)同志拿起,然后拿到手里,或变成当铺。 也就是说,子对象的生命周期不取决于父对象的生命周期,并且可以被其他对象使用。

东正教OOP教会向我们传授基本的三位一体- 封装,多态性和继承 ,这是整个面向对象方法的基础。 让我们对它们进行排序。



传承


继承是一种系统机制,无论听起来有多矛盾,它都可以通过某些类继承其他类的属性和行为,以进行进一步扩展或修改。

如果我们不想在相同的变压器上盖章,而是想制作一个通用的框架,但又使用不同的车身套件怎么办? OOP通过将逻辑划分为相似点和不同点,然后消除父类中的相似性和后代类中的差异,使我们可以恶作剧。 看起来像什么?

Optimus Prime和Megatron都是变压器,但是一个是Autobot,另一个是霸天虎。 假设汽车人与霸天虎之间的差异仅在于汽车人被改造成汽车,霸天虎-变成了航空。 所有其他属性和行为不会有任何区别。 在这种情况下,可以按以下方式设计继承系统:通用功能(运行,射击)将在Transformer基类中进行描述,而差异(转换)将在两个子类Autobot和Decepticon中进行描述。

 class Transformer(){ //   function run(){ // ,    } function fire(){ // ,    } } class Autobot(Transformer){ //  ,   Transformer function transform(){ // ,      } } class Decepticon(Transformer){ //  ,   Transformer function transform(){ // ,      } } optimus = new Autobot() megatron = new Decepticon() 


动画编号3

此示例说明了继承如何成为使用父类对代码进行重复数据删除的一种方法( DRY原理 ),同时为后代类提供了进行突变的机会。

超载


如果您在父类的父类中重写了现有方法,则重载将起作用。 这使我们不必补充父类的行为,而可以对其进行修改。 在调用方法或访问对象的字段时,对属性的搜索从子级到根级本身(即父级)进行。 也就是说,如果在Autobot上调用fire()方法,则首先在后代类-Autobot中搜索该方法,由于不存在该类,因此搜索会比Transformer类高出一步-将在该类中对其进行检测和调用。

使用不当


很好奇的是,继承层次太深会导致相反的效果-试图弄清楚从谁继承谁以及在这种情况下调用哪种方法时会很复杂。 此外,并非所有架构要求都可以使用继承来实现。 因此,继承时应避免狂热。 在适当的情况下,有一些建议要求采用优先于继承的结构。 当继承被用作“ 金锤”时,我遇到的对继承的任何批评都以不成功的例子来强化。 但这根本不意味着继承原则上总是有害的。 我的麻醉学家说,第一步是要承认您依赖继承。

描述两个实体的关系时,如何确定何时合适继承以及何时合适合成? 您可以使用流行的备忘单:问问自己, 实体A是实体B吗? 如果是这样,则最有可能的继承是适当的。 如果实体A是实体B的一部分 ,那么我们的选择是组合。

关于我们的情况,听起来像这样:

  1. 是汽车人变形金刚吗? 是的,那么我们选择继承。
  2. 枪是变形金刚的一部分吗? 是的,这意味着组成。

对于自检,请尝试反向组合,否则会产生垃圾。 该备忘单在大多数情况下会有所帮助,但是在组成和继承之间进行选择时,您还应依赖其他因素。 此外,可以将这些方法组合起来以解决不同类型的问题。

继承是静态的


继承与组合之间的另一个重要区别是,继承本质上是静态的,并且仅在解释/编译阶段才建立类关系。 正如我们在示例中看到的那样,组合使您可以在运行时即时更改实体之间的关系-有时这非常重要,因此在选择关系时需要记住这一点(当然,除非有使用元编程的愿望)。

多重继承


我们研究了从同一个后代继承两个类的情况。 但是在某些语言中,您可以做相反的事情-从两个或多个父级继承一个类,并结合其属性和行为。 从多个类而不是一个类继承的能力是多重继承。



通常,在光明会圈子中,人们认为多重继承是一种罪过,它伴随着钻石状的问题以及与设计师的困惑。 另外,可以由多重继承解决的任务可以由其他机制解决,例如接口机制(我们还将讨论)。 但是公平地说,应该注意,多重继承方便用于实现杂质

抽象类


除普通类外,某些语言还存在抽象语言。 它们与普通类的不同之处在于,您无法创建此类的对象。 读者会问为什么我们需要这样的课? 它是必需的,以便可以从其继承后代-可以创建其对象的普通类。

抽象类以及通常的方法包含没有实现的抽象方法(带有签名,但是没有代码),计划创建后代类的程序员必须实现这些抽象方法。 不需要抽象类,但是抽象类有助于建立需要实现一组特定方法的协定,以保护内存不足的程序员免受实现错误的影响。

多态性


多态是一种系统属性,可让您对一个接口进行多种实现。 不清楚。 让我们来看一下变形金刚。

假设我们有三个变压器:擎天柱,威震天和奥列格。 变形金刚具有战斗力,因此拥有Attack()方法。 玩家通过按下操纵杆上的“战斗”按钮,告诉游戏在玩家正在使用的变压器上调用Attack()方法。 但是由于变形金刚不同,而且游戏很有趣,所以每个变形金刚都会以某种方式进攻。 假设Optimus是Autobot类的对象,并且Autobots配备了带有p弹头的大炮(是的,变形金刚的粉丝并不生气)。 威震天是霸天虎,用等离子枪射击。 奥列格(Oleg)是贝斯手,他称呼他为名字。 有什么用?

该示例中多态性的优点在于,游戏代码对请求的实现一无所知,谁应该攻击该用户如何攻击,它的任务只是调用Attack()方法,其签名对于所有角色类都是相同的。 这使您可以添加新的角色类,或更改现有方法而无需更改游戏代码。 这很方便。

封装形式


封装是对对象的字段和方法的访问的控制。 访问控制不仅意味着可能/无关紧要,还意味着各种验证,加载,计算和其他动态行为。

在许多语言中,数据加密是封装的一部分。 为此,有访问修饰符(我们描述了几乎所有OOP语言中的修饰符):

  • publi-任何人都可以访问该属性
  • private-此类的唯一方法可以访问该属性
  • 受保护的-与私有的一样,只有该类的继承人才能访问,包括

 class Transformer(){ public function constructor(){ } protected function setup(){ } private function dance(){ } } 

如何选择访问修饰符? 在这种最简单的情况下:如果该方法应可由外部代码访问,则选择public。 否则为私有。 如果存在继承,那么如果不应从外部调用方法,而应由后代调用方法,则可能需要保护。

访问器(获取器和设置器)


Getter和Setter是方法,其任务是控制对字段的访问。 getter读取并返回该字段的值,相反,setter将该值作为参数并将其写入字段。 这使得可以为此类方法提供额外的处理。 例如,设置器在将值写入对象字段时可以检查类型或值是否在有效值范围内(验证)。 在getter中,如果实际值实际位于数据库中,则可以添加延迟初始化或缓存。 有很多应用程序。

某些语言使用语法糖,使此类访问器可以被屏蔽为属性,这使访问对外部代码透明,这不怀疑它不适用于字段,而是可以执行SQL查询或从后台读取文件的方法。 这就是实现抽象和透明的方式。

介面


接口的任务是减少实体之间的依赖性,从而增加更多抽象。

并非所有语言都具有这种机制,但是在没有静态类型的OOP语言中,如果没有它们,那将是非常糟糕的。 上面,我们考虑了抽象类,涉及有义务实现某些抽象方法的合同主题。 因此,接口看起来非常像一个抽象类,但它不是一个类,而只是一个带有抽象方法枚举的虚拟对象(没有实现)。 换句话说,该接口本质上是声明性的,即没有任何代码的干净协定。

通常,具有接口的语言不具有多个类继承,但是存在多个接口继承。 这使类可以列出它承担实现的接口。

具有接口的类由多对多的关系组成:一个类可以实现多个接口,而每个接口又可以由多个类实现。

该接口有两个用途:

  1. 接口的一侧是实现此接口的类。
  2. 另一方面,使用此接口作为他们(消费者)使用的数据类型的描述的消费者。

例如,如果可以序列化除基本行为以外的对象,则让其实现Serializable接口。 如果可以克隆该对象,则让它实现另一个接口-“ Cloned”。 而且,如果我们有某种通过网络传输对象的传输模块,它将接受实现Serializable接口的任何对象。

想象一下,变压器框架配备了三个插槽:一个用于武器,一个能量发生器和某种扫描仪的插槽。 这些插槽具有某些接口:每个插槽中只能安装合适的设备。 您可以在用于武器的插槽中安装火箭发射器或激光大炮,在用于发电机的插槽中安装核反应堆或RTG(放射性同位素热电发生器),并在用于扫描仪的插槽中安装雷达或激光雷达。 最重要的是,每个插槽都有一个通用连接接口,并且特定的设备应与此接口相对应。 例如,主板上使用了几种类型的插槽:处理器插槽可让您连接适合此插槽的各种处理器,而SATA插槽可让您连接任何SSD或HDD驱动器甚至CD / DVD。

我提请您注意这样一个事实,即由此产生的变压器插槽系统就是使用组合的一个例子。 如果插槽中的设备在变压器的使用寿命内是可更换的,则这已经是合计的。 , , , «» : IWeapon, IEnergyGenerator, IScanner.

 //  : interface IWeapon{ function fire() {} //    .   } interface IEnergyGenerator{ //    ,     : function generate_energy() {} //  function load_fuel() {} //  } interface IScanner{ function scan() {} } // ,  : class RocketLauncher() : IWeapon { function fire(){ //    } } class LaserGun() : IWeapon { function fire(){ //    } } class NuclearReactor() : IEnergyGenerator { function generate_energy(){ //      } function load_fuel(){ //     } } class RITEG() : IEnergyGenerator { function generate_energy(){ //     } function load_fuel(){ //   - } } class Radar() : IScanner { function scan(){ //    } } class Lidar() : IScanner { function scan(){ //     } } //  - : class Transformer() { // , : IWeapon slot_weapon //      . IEnergyGenerator slot_energy_generator //     , IScanner slot_scanner //     /*         ,      ,   : */ function install_weapon(IWeapon weapon){ this.slot_weapon = weapon } function install_energy_generator(IEnergyGenerator energy_generator){ this.slot_energy_generator = energy_generator } function install_scanner(IScanner scanner){ this.slot_scanner = scanner } } //   class TransformerFactory(){ function build_some_transformer() { transformer = new Transformer() laser_gun = new LaserGun() nuclear_reactor = new NuclearReactor() radar = new Radar() transformer.install_weapon(laser_gun) transformer.install_energy_generator(nuclear_reactor) transformer.install_scanner(radar) return transformer } } //  transformer_factory = new TransformerFactory() oleg = transformer_factory.build_some_transformer() 


№4

, , , .

- . , : () Transformer, - () ( Radar, RocketLauncher, NuclearReactor . .)

在此代码中,我们可以为变压器创建新的组件,而不会影响变压器本身的图纸。同时,反之亦然,我们可以通过组合现有组件来创建新的转换器,或者在不更改现有组件的情况下添加新组件。

鸭打字


, , : - , , , , — .

, : - , , . , .

, , , . , ! , . , :



ISP

(Interface Segregation Principle / / SOLID) . , , .


. , , - (, , ). : , , . , , . , , .


:
— , .

, . ? . , — . , , . , , , , .

:

  1. , , , ( )
  2. , , , , . ( )



, , . - , - — . , . — . , . . — .

«-». , , .

. , . , , .

. ( , , ), . , .

. ( , ). , . , , - .

. , , . , . , , ! , . , .



, , , . , . , , , .




, , , . , , , , , , .

, , , . , , , . ! , .

— , , . , , , . , , .

— . , « », .

结论


class -. (, , . .), , . - . , .

. , , , . « — » « ».

我是认真的 , , . . .

, , , . .

Source: https://habr.com/ru/post/zh-CN463125/


All Articles