阅读 70

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

全文共9402字,预计学习时长19分钟

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

图片来源:https://unsplash.com/@marcelheil

Python是数据科学中一种十分常用的编程语言。对一些人来讲,它的语言灵活、可读性强,对另一些人来讲,它简单易上手,对大多数人来讲,是由于它的多面性。

我们将Python称为多面语言,因为它允许使用四种不同的编码规范进行编码:功能性、命令性、面向对象和面向过程。这些编码风格被正式称为编程范例(https://en.wikipedia.org/wiki/Programming_paradigm),代表了一种根据语言特性对语言进行分类的方法。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

本文将对面向对象编程( OOP )(https://en.wikipedia.org/wiki/Object-oriented_programming)进行研究。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

什么是面向对象编程( OOP )?

OOP是一种基于对象概念的编程范式。在计算机科学中,“对象”一词可以指代不同概念,但基本上,它是标识符所引用的内存值(https://en.wikipedia.org/wiki/Object_(computer_science))。

在OOP的执行环境中,对象指状态(变量)和行为(方法)的组合。面向对象方法的目标是创建可重复使用的软件,以下四个特征使其更易维护:封装、抽象、继承和多态。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

还可以进一步区分面向对象语言,例如,基于类和基于原型。

在基于类的OOP中,对象是类的实例。类是关于如何定义某些内容的蓝图,但它不会提供内容本身——它只是提供结构。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

学习策略

有许多方法可以用来学习和练习OOP,因必须选择其中一种,所以笔者决定采用莎士比亚的《罗密欧与朱丽叶》的故事线,制作一个简单的基于文本的游戏。具体步骤如下:

1. 写下故事

2. 指出问题

3. 确定实体——这些是类别

4. 创建实体层次结构

5. 确定实体的功能

6. 编写测试

7. 检查测试是否成功——因未编写过任何代码,所以最初会出现错误!

8. 编写代码

9. 重复!重构!精炼!

这个过程不是一成不变的,也不是注定要被它牵着鼻子走。这一系列步骤帮助笔者开始了编程之路。面向对象不仅仅是一种编程范式,还是一种解决问题的方法,尽管它并非没有非议,但在构建复杂系统时却是一个很好的选择。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

兔子洞(The Rabbit Hole)

请注意步骤6:编写测试。现在,这个不是笔者最初的步骤。笔者正计划给各类别编码。但在研究OOP时,笔者发现了测试驱动开发( TDD )这一概念。

TDD是一种编程实践,开始于程序每个功能所进行的测试的设计和开发。这样,在开始编写代码之前,你就不得不考虑其规范、要求或设计。换句话说,在编写任何代码之前,都要编写代码来测试代码。

是不是感到迷惑不解?笔者也是。但做这个练习是完全值得的。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

测试驱动开发和单元测试

TDD的过程非常简单:

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

图片来自Kanchan Kulkarni的TDD教程(https://www.guru99.com/test-driven-development.html)

1. 写出测试

2. 运行测试

3. 写下代码

4. 运行测试

5. 重构代码

6. 重复步骤

在这个例子中,笔者使用单元测试(http://softwaretestingfundamentals.com/unit-testing/)进行TDD。单元测试是软件测试的第一级,其目的是验证程序中的每个单元是否按设计执行。可以使用不同的框架来执行单元测试。

人们对于TDD持有两种不同观点。就我个人的经历而言,TDD的优点如下:

· 在毫无方向地开始输入代码之前,TDD迫使人们必须考虑要解决的问题。

· 在基于类别的OOP的特殊情况下,它能帮助理解每个类别内容。如职责是什么?必须知道什么?——目标是低耦合和高内聚性时,这变得更加息息相关。

· 尽管一开始它可能会减慢速度,但从长远来看,它通过最小化调试时间来节省时间。

· 它鼓励更好的设计,使代码更容易维护、减少冗余(不写重复代码!),并在需要时安全地重构。

· 它是一个动态文档——只需查看测试,就能理解每个单元应该做什么,如此一来,代码就能自证其明。

换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

罗密欧与朱丽叶——代码与测试

在考虑了游戏的故事性之后,笔者决定采用两个不同的故事线:经典桥段和另类桥段。第一个是人们熟知的罗密欧与朱丽叶,第二个故事对我们来说则较为陌生。

故事安排(创建不同类别的引用)如下:

· 场景:蒙面舞会、阳台、决斗,安排、药剂师、卡普莱特墓和另类结局。该“场景”有两个主要职责,即为玩家描述场景,以及以是非问题提示玩家从而获取信息输入。

· 地图:地图就像一个有限状态机(https://en.wikipedia.org/wiki/Deterministic_finite_automaton)。它具有有限的状态(场景)、转换函数(从一个场景移动到另一个场景)和开始状态(第一个场景)。

· 故事线:定义两个唯一常量值。

从场景的定义中可以看到,所有场景都有相同的职责,只有它们的内容发生了变化(场景的描述和提示)。这就是为什么要利用继承的概念。这个概念允许定义一个类,该类别从另一个类中继承了所有方法和属性;在这种情况下,不重复写代码至关重要。

class Storyline(Enum): 
CLASSIC = "classic" 
ALTERNATIVE = "alternative"复制代码

对于故事线这一类别,笔者使用了Python’s enumeration type(https://docs.python.org/3/library/enum.html)或enum。在文档中,它们的定义为“一组绑定到唯一常量值的符号名(成员)。在枚举中,成员可以按标识进行比较,枚举本身可以循环访问。”

接下来,我们有Sence类和MockSence类。测试代码中有两个值得注意的特征:1.使用MockMap类;2.创建TestScene类以测试Scene类。在单元测试阶段,将创建一个Test类为每个类编写测试。

Class Scene(object):
 a_map = None 

 def __init__(self, a_map):
 self.a_map = a_map
 def get_message(self):
 return """ 
This scene is yet to be initialized
 """ 
def get_prompt(self):
 return """ 
This scene is yet to be initialized
 """ 

 def enter(self): 
self.print_description() 
self.prompt_user()  

def print_description(self): 
print(dedent(self.get_message())) 
def prompt_user(self):
 input_from_user = input(self.get_prompt()).lower() 
if input_from_user == "yes":
 self.a_map.advance_scene(Storyline.CLASSIC) 
 elif input_from_user == "no":
 self.a_map.advance_scene(Storyline.ALTERNATIVE) 
 self.a_map.play()复制代码


import unittest
from unittest.mock import patch, mock_open
import sys
import io
from romeo_and_juliet import *
class MockMap(Map):
 storyline = None
 play_executed = False
 def advance_scene(self, a_storyline):
 self.storyline = a_storyline
 def play(self):
 self.play_executed = True

 class TestScene(unittest.TestCase):
 def test_print_description(self):
 a_scene = Scene(Map())
 # Capturing the standard output as a test harness.
 capturedOutput = io.StringIO()
 sys.stdout = capturedOutput
 a_scene.print_description()
 self.assertEqual(capturedOutput.getvalue(), dedent("""
 This scene is yet to be initialized\n
 """))
 # Releasing standard output.
 sys.stdout = sys.__stdout__

  def test_prompt_user(self):
 a_map = MockMap()
 a_scene = Scene(a_map)
 with patch("builtins.input", return_value = "yes"):
 a_scene.prompt_user()
 self.assertEqual(a_map.storyline, Storyline.CLASSIC)
 with patch("builtins.input", return_value = "no"):
 a_scene.prompt_user()
 self.assertEqual(a_map.storyline, Storyline.ALTERNATIVE)
 self.assertTrue(a_map.play_executed)复制代码


最后,来看看Map类和TestMap类。和往常一样,我们会创建一个模拟测试,但在本例中,是为Scene类创建一个MockScene类。

class Map(object):
 scenes = None current_scene = None
 def __init__(self):
 self.scenes = {
 "the_masked_ball": TheMaskedBall(self),
 "the_balcony": TheBalcony(self),
 "the_duel": TheDuel(self),
 "the_arrangement": TheArrangement(self),
 "the_apothecary": TheApothecary(self),
 "the_capulet_tomb": TheCapuletTomb(self),
 "the_alternative_ending": TheAlternativeEnding(self)
 }
 self.current_scene = self.scenes["the_masked_ball"]
 def get_current_scene(self):
 return self.current_scene
 def play(self):
 self.current_scene.enter()
 def advance_scene(self, storyline):
 if storyline == Storyline.CLASSIC:
 if self.current_scene == self.scenes["the_masked_ball"]:
 self.current_scene = self.scenes["the_balcony"]
 elif self.current_scene == self.scenes["the_balcony"]:
 self.current_scene = self.scenes["the_duel"]
 elif self.current_scene == self.scenes["the_duel"]:
 self.current_scene = self.scenes["the_arrangement"]
 elif self.current_scene == self.scenes["the_arrangement"]:
 self.current_scene = self.scenes["the_apothecary"]
 elif self.current_scene == self.scenes["the_apothecary"]:
 self.current_scene = self.scenes["the_capulet_tomb"]
 elif self.current_scene == self.scenes["the_capulet_tomb"]:
 raise Exception
 if storyline == Storyline.ALTERNATIVE:
 if self.current_scene == self.scenes["the_masked_ball"]:
 self.current_scene = self.scenes["the_alternative_ending"]
 elif self.current_scene == self.scenes["the_balcony"]:
 self.current_scene = self.scenes["the_alternative_ending"]
 elif self.current_scene == self.scenes["the_duel"]:
 self.current_scene = self.scenes["the_alternative_ending"]
 elif self.current_scene == self.scenes["the_arrangement"]:
 self.current_scene = self.scenes["the_alternative_ending"]
 elif self.current_scene == self.scenes["the_apothecary"]:
 self.current_scene = self.scenes["the_alternative_ending"]
 elif self.current_scene == self.scenes["the_alternative_ending"]:
 raise Exception复制代码


class MockScene(Scene):
 was_entered = False
 def enter(self):
 self.was_entered = True
class TestMap(unittest.TestCase):
 def test_play(self):
 a_map = Map()
 mock_scene = MockScene(a_map)
 a_map.current_scene = mock_scene
 a_map.play()
 self.assertTrue(mock_scene.was_entered)
  def test_initial_state(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)

  def test_advance_scene_classic(self):
  a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheBalcony)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheDuel)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheArrangement)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheApothecary)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheCapuletTomb)
 with self.assertRaises(Exception):
 a_map.advance_scene(Storyline.CLASSIC)
 def test_advance_scene_alternative_one(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.ALTERNATIVE)
 self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding)
 with self.assertRaises(Exception):
 a_map.advance_scene(Storyline.ALTERNATIVE)

  def test_advance_scene_alternative_two(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheBalcony)
 a_map.advance_scene(Storyline.ALTERNATIVE)
 self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding)
 with self.assertRaises(Exception):
 a_map.advance_scene(Storyline.ALTERNATIVE)
 def test_advance_scene_alternative_three(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheBalcony)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheDuel)
 a_map.advance_scene(Storyline.ALTERNATIVE)
 self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding)
 with self.assertRaises(Exception):
 a_map.advance_scene(Storyline.ALTERNATIVE)
 def test_advance_scene_alternative_four(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheBalcony)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheDuel)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheArrangement)
 a_map.advance_scene(Storyline.ALTERNATIVE)
 self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding)
 with self.assertRaises(Exception): 
a_map.advance_scene(Storyline.ALTERNATIVE)
  def test_advance_scene_alternative_five(self):
 a_map = Map()
 self.assertIsInstance(a_map.get_current_scene(), TheMaskedBall)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheBalcony)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheDuel)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheArrangement)
 a_map.advance_scene(Storyline.CLASSIC)
 self.assertIsInstance(a_map.get_current_scene(), TheApothecary)
 a_map.advance_scene(Storyline.ALTERNATIVE)
 self.assertIsInstance(a_map.get_current_scene(), TheAlternativeEnding)
 with self.assertRaises(Exception):
 a_map.advance_scene(Storyline.ALTERNATIVE)复制代码
换个姿势读罗密欧与朱丽叶,带你了解 OOP与TDD

留言 点赞 关注

我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”


(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)



关注下面的标签,发现更多相似文章
评论