使用SOLID编写灵活的代码



译者的话: Severin Peres为您发布有关在编程中使用SOLID原理的文章 本文中的信息对于有经验的初学者和程序员都是有用的。

如果您是开发人员,则可能听说过SOLID原则。 它们使程序员能够编写干净,结构良好且易于维护的代码。 值得注意的是,在编程中,有几种方法可以正确执行此工作。 不同的专家对“正确的方法”有不同的想法和理解,这完全取决于每个人的经验。 但是,SOLID中宣告的想法几乎被IT社区的所有代表所接受。 它们成为许多良好的开发管理实践的出现和发展的起点。

让我们看看SOLID原理是什么以及它们如何帮助我们。

Skillbox建议:实用课程“ Mobile Developer PRO”

我们提醒您: 对于所有“哈勃”读者来说,使用“哈勃”促销代码注册任何Skillbox课程时均可享受10,000卢布的折扣。

什么是SOLID?


该术语是缩写,该术语的每个字母都是某个原则名称的开头:
  • 责任原则。 一个模块可能只有一个更改原因。
  • O笔/封闭原理 。 类和其他元素必须打开才能进行扩展,但必须关闭才能进行修改。
  • 里斯科夫替代原理 。 使用基本类型的函数应该能够在不知道基本类型的情况下使用基本类型的子类型。
  • 界面隔离原则 。 软件实体不应依赖于它们不使用的方法。
  • 相对反转原理 。 上级模块不应依赖于下级模块。

唯一责任原则


单一责任原则(SRP)指出,程序中的每个类或模块应仅负责该程序功能的一部分。 此外,应将此责任的要素分配给他们的班级,而不是在无关的班级之间分配。 SRP的开发者兼首席传教士Robert S. Martin将责任描述为改变的原因。 最初,他将该术语作为其工作的要素之一“面向对象设计的原理”提出。 该概念包括Tom Demarco先前定义的许多连接模式。

该概念还包括David Parnassus提出的几个概念。 主要的两个是封装和信息隐藏。 帕纳苏斯(Parnassus)辩称,将系统划分为单独的模块不应基于对流程图或执行流程的分析。 任何模块都应包含特定的解决方案,以向客户提供最少的信息。

顺便说一句,马丁给公司的高级经理(COO,CTO,CFO)提供了一个有趣的例子,他们每个人都将特定的软件用于不同目的的业务。 结果,他们中的任何一个都可以在不影响其他管理人员利益的情况下实施软件更改。

神物


像往常一样,学习SRP的最好方法是看到一切。 让我们看一看程序中不符合分担责任原则的部分。 这是描述空间站行为和属性的Ruby代码。

查看示例并尝试确定以下内容:
在SpaceStation类中声明的那些对象的职责。
那些对空间站的工作感兴趣的人。

class SpaceStation def initialize @supplies = {} @fuel = 0 end def run_sensors puts "----- Sensor Action -----" puts "Running sensors!" end def load_supplies(type, quantity) puts "----- Supply Action -----" puts "Loading #{quantity} units of #{type} in the supply hold." if @supplies[type] @supplies[type] += quantity else @supplies[type] = quantity end end def use_supplies(type, quantity) puts "----- Supply Action -----" if @supplies[type] != nil && @supplies[type] > quantity puts "Using #{quantity} of #{type} from the supply hold." @supplies[type] -= quantity else puts "Supply Error: Insufficient #{type} in the supply hold." end end def report_supplies puts "----- Supply Report -----" if @supplies.keys.length > 0 @supplies.each do |type, quantity| puts "#{type} avalilable: #{quantity} units" end else puts "Supply hold is empty." end end def load_fuel(quantity) puts "----- Fuel Action -----" puts "Loading #{quantity} units of fuel in the tank." @fuel += quantity end def report_fuel puts "----- Fuel Report -----" puts "#{@fuel} units of fuel available." end def activate_thrusters puts "----- Thruster Action -----" if @fuel >= 10 puts "Thrusting action successful." @fuel -= 10 else puts "Thruster Error: Insufficient fuel available." end end end 

实际上,我们的空间站无法正常工作(我认为在不久的将来我不会接到NASA的电话),但是有一些事情需要分析。

因此,SpaceStation类具有几个不同的职责(或任务)。 所有这些都可以分为几种类型:
  • 感测器
  • 供应(消耗品);
  • 燃料;
  • 加速器。

尽管在班级中没有定义任何车站员工,但我们可以轻松想象谁对什么负责。 最有可能的是,科学家控制传感器,物流师负责资源的供应,工程师负责燃料的供应,飞行员控制加速器。

我们可以说这个程序不符合SRP吗? 当然可以 但是,SpaceStation类是一个典型的“神圣对象”,它什么都知道,任何事都做。 这是面向对象编程中的主要反模式。 对于初学者而言,此类对象极难维护。 到目前为止,该程序非常简单,是的,但是请想象一下,如果我们添加新功能,将会发生什么。 也许我们的空间站将需要医疗中心或会议室。 功能越多,SpaceStation的增长量就越大。 好吧,由于此对象将与其他对象联系在一起,因此整个系统的维护将变得更加复杂。 结果,我们可能会破坏例如加速器的工作。 如果研究人员要求对传感器的使用进行更改,那么这很可能会影响工作站的通信系统。

违反SRP原则可以在短期上取得战术上的胜利,但是最终我们将“输掉战争”,将来为这样的怪物提供服务将非常困难。 最好将程序分为单独的代码部分,每个部分负责执行特定的操作。 考虑到这一点,让我们更改SpaceStation类。

分担责任

上面,我们确定了由SpaceStation类控制的四种类型的操作。 重构时,我们会牢记它们。 更新的代码与SRP更好地匹配。

 class SpaceStation attr_reader :sensors, :supply_hold, :fuel_tank, :thrusters def initialize @supply_hold = SupplyHold.new @sensors = Sensors.new @fuel_tank = FuelTank.new @thrusters = Thrusters.new(@fuel_tank) end end class Sensors def run_sensors puts "----- Sensor Action -----" puts "Running sensors!" end end class SupplyHold attr_accessor :supplies def initialize @supplies = {} end def load_supplies(type, quantity) puts "----- Supply Action -----" puts "Loading #{quantity} units of #{type} in the supply hold." if @supplies[type] @supplies[type] += quantity else @supplies[type] = quantity end end def use_supplies(type, quantity) puts "----- Supply Action -----" if @supplies[type] != nil && @supplies[type] > quantity puts "Using #{quantity} of #{type} from the supply hold." @supplies[type] -= quantity else puts "Supply Error: Insufficient #{type} in the supply hold." end end def report_supplies puts "----- Supply Report -----" if @supplies.keys.length > 0 @supplies.each do |type, quantity| puts "#{type} avalilable: #{quantity} units" end else puts "Supply hold is empty." end end end class FuelTank attr_accessor :fuel def initialize @fuel = 0 end def get_fuel_levels @fuel end def load_fuel(quantity) puts "----- Fuel Action -----" puts "Loading #{quantity} units of fuel in the tank." @fuel += quantity end def use_fuel(quantity) puts "----- Fuel Action -----" puts "Using #{quantity} units of fuel from the tank." @fuel -= quantity end def report_fuel puts "----- Fuel Report -----" puts "#{@fuel} units of fuel available." end end class Thrusters def initialize(fuel_tank) @linked_fuel_tank = fuel_tank end def activate_thrusters puts "----- Thruster Action -----" if @linked_fuel_tank.get_fuel_levels >= 10 puts "Thrusting action successful." @linked_fuel_tank.use_fuel(10) else puts "Thruster Error: Insufficient fuel available." end end end 

有很多更改,该程序现在看起来肯定更好。 现在,我们的SpaceStation类已经变成了一个容器,可以在其中启动对相关零件的操作,包括一组传感器,消耗品供应系统,燃油箱和增压器。

现在,对于任何变量,都有一个对应的类:传感器; SupplyHold; 油箱 推进器。

此版本的代码有几个重要更改。 事实是,各个功能不仅被封装在自己的类中,而且它们的组织方式也变得可预测和一致。 我们将功能相似的元素分组以遵循连通性原则。 现在,如果我们需要通过从哈希结构切换为数组来更改系统原理,只需使用SupplyHold类,我们就无需接触其他模块。 因此,如果负责后勤的人员在其所在区域进行了某些更改,则该站的其余部分将保持不变。 同时,SpaceStation类甚至不会意识到这些更改。

我们的空间站人员可能会对这些更改感到满意,因为他们可以要求他们所需要的内容。 请注意,该代码在SupplyHold和FuelTank类中包含诸如report_supplies和report_fuel之类的方法。 如果地球要求改变报告的生成方式,会发生什么? 您将需要更改两个类SupplyHold和FuelTank。 但是,如果您需要改变交付燃料和消耗品的方式怎么办? 您可能必须再次更改所有相同的类。 这违反了SRP原则。 让我们修复它。

 class SpaceStation attr_reader :sensors, :supply_hold, :supply_reporter, :fuel_tank, :fuel_reporter, :thrusters def initialize @sensors = Sensors.new @supply_hold = SupplyHold.new @supply_reporter = SupplyReporter.new(@supply_hold) @fuel_tank = FuelTank.new @fuel_reporter = FuelReporter.new(@fuel_tank) @thrusters = Thrusters.new(@fuel_tank) end end class Sensors def run_sensors puts "----- Sensor Action -----" puts "Running sensors!" end end class SupplyHold attr_accessor :supplies attr_reader :reporter def initialize @supplies = {} end def get_supplies @supplies end def load_supplies(type, quantity) puts "----- Supply Action -----" puts "Loading #{quantity} units of #{type} in the supply hold." if @supplies[type] @supplies[type] += quantity else @supplies[type] = quantity end end def use_supplies(type, quantity) puts "----- Supply Action -----" if @supplies[type] != nil && @supplies[type] > quantity puts "Using #{quantity} of #{type} from the supply hold." @supplies[type] -= quantity else puts "Supply Error: Insufficient #{type} in the supply hold." end end end class FuelTank attr_accessor :fuel attr_reader :reporter def initialize @fuel = 0 end def get_fuel_levels @fuel end def load_fuel(quantity) puts "----- Fuel Action -----" puts "Loading #{quantity} units of fuel in the tank." @fuel += quantity end def use_fuel(quantity) puts "----- Fuel Action -----" puts "Using #{quantity} units of fuel from the tank." @fuel -= quantity end end class Thrusters FUEL_PER_THRUST = 10 def initialize(fuel_tank) @linked_fuel_tank = fuel_tank end def activate_thrusters puts "----- Thruster Action -----" if @linked_fuel_tank.get_fuel_levels >= FUEL_PER_THRUST puts "Thrusting action successful." @linked_fuel_tank.use_fuel(FUEL_PER_THRUST) else puts "Thruster Error: Insufficient fuel available." end end end class Reporter def initialize(item, type) @linked_item = item @type = type end def report puts "----- #{@type.capitalize} Report -----" end end class FuelReporter < Reporter def initialize(item) super(item, "fuel") end def report super puts "#{@linked_item.get_fuel_levels} units of fuel available." end end class SupplyReporter < Reporter def initialize(item) super(item, "supply") end def report super if @linked_item.get_supplies.keys.length > 0 @linked_item.get_supplies.each do |type, quantity| puts "#{type} avalilable: #{quantity} units" end else puts "Supply hold is empty." end end end iss = SpaceStation.new iss.sensors.run_sensors # ----- Sensor Action ----- # Running sensors! iss.supply_hold.use_supplies("parts", 2) # ----- Supply Action ----- # Supply Error: Insufficient parts in the supply hold. iss.supply_hold.load_supplies("parts", 10) # ----- Supply Action ----- # Loading 10 units of parts in the supply hold. iss.supply_hold.use_supplies("parts", 2) # ----- Supply Action ----- # Using 2 of parts from the supply hold. iss.supply_reporter.report # ----- Supply Report ----- # parts avalilable: 8 units iss.thrusters.activate_thrusters # ----- Thruster Action ----- # Thruster Error: Insufficient fuel available. iss.fuel_tank.load_fuel(100) # ----- Fuel Action ----- # Loading 100 units of fuel in the tank. iss.thrusters.activate_thrusters # ----- Thruster Action ----- # Thrusting action successful. # ----- Fuel Action ----- # Using 10 units of fuel from the tank. iss.fuel_reporter.report # ----- Fuel Report ----- # 90 units of fuel available. 

在该程序的最新版本中,职责分为两个新类,即FuelReporter和SupplyReporter。 他们都是记者类的孩子。 另外,我们在SpaceStation类中添加了实例变量,以便在必要时初始化必要的子类。 现在,如果地球决定改变其他东西,那么我们将对子类而不是主类进行更改。

当然,这里的某些类仍然相互依赖。 因此,SupplyReporter对象取决于SupplyHold,而FuelReporter对象取决于FuelTank。 当然,增压器应连接到燃油箱。 但是,这里的一切看起来合乎逻辑,并且进行更改将不会特别困难-编辑一个对象的代码不会对另一个对象产生太大影响。

因此,我们创建了模块化代码,其中精确定义了每个对象/类的职责。 使用这样的代码不是问题;对其进行维护将是一个简单的任务。 我们已经将整个“神圣对象”转换为SRP。

Skillbox建议:

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


All Articles