Python 工匠:写好面向对象代码的原则!

从Gitee上部署视觉工程到Linux虚拟机 (使用SVM来识别装甲板的编号,此处手机上的图片中只有灯条没有数字,SVM无法识别出编号。这两行代码的意义是,当SVM识别不出或者识别不到编号时,把装甲板的编号默认为2号。找到自瞄程序中,数字识别类下的这两行代码,把注释去掉。这样,每次打开新的终端时,环境变量都会自动设置。(2)查看虚拟机设置选项,检查虚拟机USB接口是否为3.0及以上的接口。如果不是,要更改好。这样,每次打开新的终端时,环境变量都会自动设置。后,每次打开终端时自动设置这些环境变量,你可以将。打开第一个终端,用来挂ROS的内核启动页面。 阅读详情

 Python 是一门支持多种编程风格的语言,面对相同的需求,拥有不同背景的程序员可能会写出风格迥异的 Python 代码。比如一位习惯编写 C 语言的程序员,通常会定义一大堆函数来搞定所有事情,这是 “过程式编程” 的思想。而一位有 Java 背景的程序员则更倾向于设计许多个相互关联的类 (class) ,这是 “面向对象编程(后简称 OOP)” 。

虽然不同的编程风格各有特点,无法直接比较。但是 OOP 思想在现代软件开发中起到的重要作用应该是毋庸置疑的。

很多人在学习如何写好 OOP 代码时,会选择从那 23 种经典的“设计模式” 开始。不过对于 Python 程序员来说,我认为这并非是一个最佳选择。

Python 对 OOP 的支持

Python学习交流群:1004391443

Python 语言虽然拥有类、继承、多态等核心 OOP 特性,但和那些完全基于 OOP 思想设计的编程语言 (比如 Java) 相比,它在 OOP 支持方面做了很多简化工作。比如它 没有严格的类私有成员,没有接口(Interface)对象 等。

而与此同时,Python 灵活的函数对象、鸭子类型等许多动态特性又让一些在其他语言中很难做到的事情变得非常简单。这些语言间的差异共同导致了一个结果: 很多经典的设计模式到了 Python 里,就丢失了那个“味道”,实用性也大打折扣。

拿大家最熟悉的单例模式来说。你可以花上一大把时间,来学习如何在 Python 中利用 __new__ 方法或元类 (metaclass) 来实现单例设计模式,但最后你会发现,自己 95% 的需求都可以通过直接定义一个模块级全局变量来搞定。

所以,与具体化的 设计模式 相比,我觉得一些更为抽象的 设计原则 适用性更广、更适合运用到 Python 开发工作中。而谈到关于 OOP 的设计原则,“SOLID” 是众多原则中最有名的一个。

SOLID 设计原则

著名的设计模式书籍 《设计模式:可复用面向对象软件的基础》 出版于 1994 年,距今已有超过 25 年的历史。而这篇文章的主角: “SOLID 设计原则”同样也并不年轻。

早在 2000 年, Robert C. Martin 就在他的文章 "Design Principles and Design Patterns" 中整理并提出了 “SOLID” 设计原则的雏型,之后又在他的经典著作 《敏捷软件开发 : 原则、模式与实践》 中将其发扬光大。“SOLID” 由 5 个单词组合的首字母缩写组成,分别代表 5 条不同的面向对象领域的设计原则。

在编写 OOP 代码时,如果遵循这 5 条设计原则,就更可能写出可扩展、易于修改的代码。相反,如果不断违反其中的一条或多条原则,那么很快你的代码就会变得不可扩展、难以维护。

接下来,让我用一个真实的 Python 代码样例来分别向你诠释这 5 条设计原则。

写在最前面的注意事项:

  1. “原则”不是“法律”,它只起到指导作用,并非不可以违反
  2. “原则”的后两条与接口(Interface)有关,而 Python 没有接口,所以对这部分的诠释是我的个人理解,与原版可能略有出入
  3. 文章后面的内容含有大量代码,请做好心理准备 :coffee:️
  4. 为了增强代码的说明性,本文中的代码使用了 Python3 中的 类型注解特性

SOLID 原则与 Python

Hacker News (后简称 HN) 是一个在程序员圈子里很受欢迎的站点。在它的首页,有很多由用户提交后基于推荐算法排序的科技相关内容。

我经常会去上面看一些热门文章,但我觉得每次打开浏览器访问有点麻烦。所以,我准备编写一个脚本,自动抓取 HN 首页 Top5 的新闻标题与链接,并用纯文本的方式写入到文件。方便自己用其他工具阅读。

 

 

 

 

图:Hacker News 首页截图

编写爬虫几乎是 Python 天生的拿手好戏。利用 requests、lxml 等模块提供的好用功能,我可以轻松实现上面的需求。下面是我第一次编写好的代码:

import io
import sys
from typing import Generator
import requests
from lxml import etree
class Post:
    """HN(https://news.ycombinator.com/) 上的条目
    :param title: 标题
    :param link: 链接
    :param points: 当前得分
    :param comments_cnt: 评论数
    """
    def __init__(self, title: str, link: str, points: str, comments_cnt: str):
        self.title = title
        self.link = link
        self.points = int(points)
        self.comments_cnt = int(comments_cnt)
class HNTopPostsSpider:
    """抓取 HackerNews Top 内容条目
    :param fp: 存储抓取结果的目标文件对象
    :param limit: 限制条目数,默认为 5
    """
    ITEMS_URL = 'https://news.ycombinator.com/'
    FILE_TITLE = 'Top news on HN'
    def __init__(self, fp: io.TextIOBase, limit: int = 5):
        self.fp = fp
        self.limit = limit
    def fetch(self) -> Generator[Post, None, None]:
        """从 HN 抓取 Top 内容
        """
        resp = requests.get(self.ITEMS_URL)
        # 使用 XPath 可以方便的从页面解析出你需要的内容,以下均为页面解析代码
        # 如果你对 xpath 不熟悉,可以忽略这些代码,直接跳到 yield Post() 部分
        html = etree.HTML(resp.text)
        items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]')
        for item in items[:self.limit]:
            node_title = item.xpath('./td[@class="title"]/a')[0]
            node_detail = item.getnext()
            points_text = node_detail.xpath('.//span[@class="score"]/text()')
            comments_text = node_detail.xpath('.//td/a[last()]/text()')[0]
            yield Post(
                title=node_title.text,
                link=node_title.get('href'),
                # 条目可能会没有评分
                points=points_text[0].split()[0] if points_text else '0',
                comments_cnt=comments_text.split()[0]
            )
    def write_to_file(self):
        """以纯文本格式将 Top 内容写入文件
        """
        self.fp.write(f'# {self.FILE_TITLE}\n\n')
        # enumerate 接收第二个参数,表示从这个数开始计数(默认为 0)
        for i, post in enumerate(self.fetch(), 1):
            self.fp.write(f'> TOP {i}: {post.title}\n')
            self.fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n')
            self.fp.write(f'> 地址:{post.link}\n')
            self.fp.write('------\n')
def main():
    # with open('/tmp/hn_top5.txt') as fp:
    #     crawler = HNTopPostsSpider(fp)
    #     crawler.write_to_file()
    # 因为 HNTopPostsSpider 接收任何 file-like 的对象,所以我们可以把 sys.stdout 传进去
    # 实现往控制台标准输出打印的功能
    crawler = HNTopPostsSpider(sys.stdout)
    crawler.write_to_file()
if __name__ == '__main__':
    main()

你可以把上面的代码称之为符合 OOP 风格的,因为在上面的代码里,我定义了两个类:

Post
HNTopPostsSpider

如果你本地的 Python 环境配置正常,那么可以尝试执行一下上面这段代码,它会输出下面这样的内容:

❯ python news_digester.py
> TOP 1: Show HN: NoAgeismInTech – Job board for companies fighting ageism in tech
> 分数:104 评论数:26
> 地址:https://noageismintech.com/
------
> TOP 2: Magic Leap sues former employee who founded the China-based Nreal for IP theft
> 分数:17 评论数:2
> 地址:https://www.bloomberg.com/news/articles/2019-06-18/secretive-magic-leap-says-ex-engineer-copied-headset-for-china
------
... ...

这个脚本基于面向对象的方式编写 (换句话说,就是定义了一些 class :unamused:) ,可以满足我的需求。但是从设计的角度来看,它却违反了 SOLID 原则的第一条:“Single responsibility principle(单一职责原则)”,让我们来看看是为什么。

S:单一职责原则

SOLID 设计原则里的第一个字母 S 来自于 “Single responsibility principle(单一职责原则)” 的首字母。这个原则认为: “一个类应该仅仅只有一个被修改的理由。” 换句话说,每个类都应该只有一种职责。

而在上面的代码中, HNTopPostsSpider 这个类违反了这个原则。因为我们可以很容易的找到两个不同的修改它的理由:

  • 理由 1 : HN 网站的程序员突然更新了页面样式,旧的 xpath 解析算法从新页面上解析不到内容,需要修改 fetch 方法内的解析逻辑。
  • 理由 2 : 用户 (也就是我) 突然觉得纯文本格式的输出不好看,想要改成 Markdown 样式。需要修改 write_to_file 方法内的输出逻辑。

所以, HNTopPostsSpider 类违反了“单一职责原则”,因为它有着多个被修改的理由。而这背后的根本原因是因为它承担着 “抓取帖子列表” 和 "将帖子列表写入文件" 这两种完全不同的职责。

违反“单一职责原则”的坏处

如果某个类违反了“单一职责原则”,那意味着我们经常会因为不同的原因去修改它。这可能会导致不同功能之间相互影响。比如,可能我在某天调整了页面解析逻辑,却发现输出的文件格式也全部乱掉了。

另外,单个类承担的职责越多,意味着这个类的复杂度也就越高,它的维护成本也同样会水涨船高。违反“单一职责原则”的类同样也难以被复用,假如我有其他代码想复用 HNTopPostsSpider 类的抓取和解析逻辑,会发现我必须要提供一个莫名其妙的文件对象给它才行。

那么,要如何修改代码才能让它遵循“单一职责原则”呢?办法有很多,最传统的是: 把大类拆分为小类 。

拆分大类为多个小类

为了让 HNTopPostsSpider 类的职责更纯粹,我们可以把其中与“写入文件”相关的内容拆分出去作为一个新的类:

class PostsWriter:
    """负责将帖子列表写入到文件
    """
    def __init__(self, fp: io.TextIOBase, title: str):
        self.fp = fp
        self.title = title
    def write(self, posts: List[Post]):
        self.fp.write(f'# {self.title}\n\n')
        # enumerate 接收第二个参数,表示从这个数开始计数(默认为 0)
        for i, post in enumerate(posts, 1):
            self.fp.write(f'> TOP {i}: {post.title}\n')
            self.fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n')
            self.fp.write(f'> 地址:{post.link}\n')
            self.fp.write('------\n')

而在 HNTopPostsSpider 类里,可以通过调用 PostsWriter 的方式来完成之前的工作:

class HNTopPostsSpider:
    FILE_TITLE = 'Top news on HN'
    <... 已省略 ...>
    def write_to_file(self, fp: io.TextIOBase):
        """以纯文本格式将 Top 内容写入文件
        实例化参数文件对象 fp 被挪到了 write_to_file 方法中
        """
        # 将文件写入逻辑托管给 PostsWriter 类处理
        writer = PostsWriter(fp, title=self.FILE_TITLE)
        writer.write(list(self.fetch()))

通过这种方式,我们让 HNTopPostsSpider 和 PostsWriter 类都各自满足了“单一职责原则”。我只会因为解析逻辑变动才去修改 HNTopPostsSpider 类,同样,修改 PostsWriter 类的原因也只有调整输出格式一种。这两个类各自的修改可以单独进行而不会相互影响。

另一种方案:使用函数

“单一职责原则”虽然是针对类说的,但其实它的适用范围可以超出类本身。比如在 Python 中,通过定义函数,同样也可以让上面的代码符合单一职责原则。

我们可以把“写入文件”的逻辑拆分为一个新的函数,由它来专门承担起将帖子列表写入文件的职责:

def write_posts_to_file(posts: List[Post], fp: io.TextIOBase, title: str):
    """负责将帖子列表写入文件
    """
    fp.write(f'# {title}\n\n')
    for i, post in enumerate(posts, 1):
        fp.write(f'> TOP {i}: {post.title}\n')
        fp.write(f'> 分数:{post.points} 评论数:{post.comments_cnt}\n')
        fp.write(f'> 地址:{post.link}\n')
        fp.write('------\n')

而对于 HNTopPostsSpider 类来说,改动可以更进一步。这次我们可以直接删除其中和文件写入相关的所有代码。让它只负责一件事情:“获取帖子列表”。

class HNTopPostsSpider:
    """抓取 HackerNews Top 内容条目
    :param limit: 限制条目数,默认为 5
    """
    ITEMS_URL = 'https://news.ycombinator.com/'
    def __init__(self, limit: int = 5):
        self.limit = limit
    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>

相应的,类和函数的调用方 main 函数就需要稍作调整,它需要负责把 write_posts_to_file 函数和 HNTopPostsSpider 类之间协调起来,共同完成工作:

def main():
    crawler = HNTopPostsSpider()
    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)

将“文件写入”职责拆分为新函数是一个 Python 特色的解决方案,它虽然没有那么 OO (面向对象) ,但是同样满足“单一职责原则”,而且在很多场景下更灵活与高效。

O:开放-关闭原则

O 来自于 “Open–closed principle(开放-关闭原则)” 的首字母,它认为: “类应该对扩展开放,对修改封闭。” 这是一个从字面上很难理解的原则,它同样有着另外一种说法: “你应该可以在不修改某个类的前提下,扩展它的行为。”

这原则听上去有点让人犯迷糊,如何能做到不修改代码又改变行为呢?让我来举一个例子:你知道 Python 里的内置排序函数 sorted 吗?

如果我们想对某个列表排序,可以直接调用 sorted 函数:

>>> l = [5, 3, 2, 4, 1]
>>> sorted(l)
[1, 2, 3, 4, 5]

现在,假如我们想改变 sorted 函数的排序逻辑。比如,让它使用所有元素对 3 取余后的结果来排序。我们是不是需要去修改 sorted 函数的源码?当然不用,只需要在调用 sort 函数时,传入自定义的排序函数 key 参数就行了:

>>> l = [8, 1, 9]
# 按照元素对 3 的余数排序,能被 3 整除的 9 排在了最前面,随后是 1 和 8
>>> sorted(l, key=lambda i: i % 3)
[9, 1, 8]

通过上面的例子,我们可以认为: sorted 函数是一个符合“开放-关闭原则”的绝佳例子,因为它:

  • 对扩展开放 :你可以通过传入自定义 key 函数来扩展它的行为
  • 对修改关闭 :你无需修改 sort 函数本身

如何违反“开放-关闭原则”

现在,让我们回到爬虫小程序。在使用了一段时间之后,用户 (还是我) 觉得每次抓取到的内容有点不合口味。我其实只关注那些来自特定网站,比如 github 上的内容。所以我需要修改 HNTopPostsSpider 类的代码来对结果进行过滤:

class HNTopPostsSpider:
    # <... 已省略 ...>
    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        counter = 0
        for item in items:
            if counter >= self.limit:
                break
            # <... 已省略 ...>
            link = node_title.get('href')
            # 只关注来自 github.com 的内容
            if 'github' in link.lower():
                counter += 1
                yield Post(... ...)

完成修改后,让我们来简单测试一下效果:

❯ python news_digester_O_before.py
# Top news on HN
> TOP 1: Mimalloc – A compact general-purpose allocator
> 分数:291 评论数:40
> 地址:https://github.com/microsoft/mimalloc
------
> TOP 2: Olivia: An open source chatbot build with a neural network in Go
> 分数:53 评论数:19
> 地址:https://github.com/olivia-ai/olivia
------
<... 已省略 ...>

看上去新加的过滤代码起到了作用,现在只有链接中含有 github 的内容才会被写入到结果中。

但是,正如某位哲学家的名言所说: “这世间唯一不变的,只有变化本身。” 某天,用户 (永远是我) 突然觉得,来自 bloomberg 的内容也都很有意思,所以我想要把 bloomberg 也加入筛选关键字逻辑里。

这时我们就会发现:现在的代码违反了"开放-关闭原则"。因为我必须要修改现有的 HNTopPostsSpider 类代码,调整那个 if 'github' in link.lower() 判断语句才能完成我的需求。

“开放-关闭原则”告诉我们,类应该通过扩展而不是修改的方式改变自己的行为。那么我应该如何调整代码,让它可以遵循原则呢?

使用类继承来改造代码

继承是面向对象理论中最重要的概念之一。它允许我们在父类中定义好数据和方法,然后通过继承的方式让子类获得这些内容,并可以选择性的对其中一些进行重写,修改它的行为。

使用继承的方式来让类遵守“开放-关闭原则”的关键点在于: 找到父类中会变动的部分,将其抽象成新的方法(或属性),最终允许新的子类来重写它以改变类的行为。

对于 HNTopPostsSpider 类来说。首先,我们需要找到其中会变动的那部分逻辑,也就是 “判断是否对条目感兴趣” ,然后将其抽象出来,定义为新的方法:

class HNTopPostsSpider:
    # <... 已省略 ...>
    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            post = Post( ... ... )
            # 使用测试方法来判断是否返回该帖子
            if self.interested_in_post(post):
                counter += 1
                yield post
    def interested_in_post(self, post: Post) -> bool:
        """判断是否应该将帖子加入结果中
        """
        return True

如果我们只关心来自 github 的帖子,那么只需要定义一个继承于 HNTopPostsSpider 子类,然后重写父类的 interested_in_post 方法即可。

class GithubOnlyHNTopPostsSpider(HNTopPostsSpider):
    """只关心来自 Github 的内容
    """
    def interested_in_post(self, post: Post) -> bool:
        return 'github' in post.link.lower()
def main():
    # crawler = HNTopPostsSpider()
    # 使用新的子类
    crawler = GithubOnlyHNTopPostsSpider()
    <... ...>

假如我们的兴趣发生了变化?没关系,增加新的子类就行:

class GithubNBloomBergHNTopPostsSpider(HNTopPostsSpider):
    """只关系来自 Github/BloomBerg 的内容
    """
    def interested_in_post(self, post: Post) -> bool:
        if 'github' in post.link.lower() \
                or 'bloomberg' in post.link.lower():
            return True
        return False

所有的这一切,都不需要修改原本的 HNTopPostsSpider 类的代码,只需要不断在它的基础上创建新的子类就能完成新需求。最终实现了对扩展开放、对改变关闭。

使用组合与依赖注入来改造代码

虽然类的继承特性很强大,但它并非唯一办法, 依赖注入(Dependency injection) 是解决这个问题的另一种思路。与继承不同,依赖注入允许我们在类实例化时,通过参数将业务逻辑的变化点: 帖子过滤算法 注入到类实例中。最终同样实现“开放-关闭原则”。

首先,我们定义一个名为 PostFilter 的抽象类:

from abc import ABC, abstractmethod
class PostFilter(metaclass=ABCMeta):
    """抽象类:定义如何过滤帖子结果
    """
    @abstractmethod
    def validate(self, post: Post) -> bool:
        """判断帖子是否应该被保留"""

Hint:定义抽象类在 Python 的 OOP 中并不是必须的,你也可以不定义它,直接从下面的 DefaultPostFilter 开始。

然后定义一个继承于该抽象类的默认 DefaultPostFilter 类,过滤逻辑为保留所有结果。之后再调整一下 HNTopPostsSpider 类的构造方法,让它接收一个名为 post_filter 的结果过滤器:

class DefaultPostFilter(PostFilter):
    """保留所有帖子
    """
    def validate(self, post: Post) -> bool:
        return True
class HNTopPostsSpider:
    """抓取 HackerNews Top 内容条目
    :param limit: 限制条目数,默认为 5
    :param post_filter: 过滤结果条目的算法,默认为保留所有
    """
    ITEMS_URL = 'https://news.ycombinator.com/'
    def __init__(self, limit: int = 5, post_filter: Optional[PostFilter] = None):
        self.limit = limit
        self.post_filter = post_filter or DefaultPostFilter()
    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            post = Post( ... ... )
            # 使用测试方法来判断是否返回该帖子
            if self.post_filter.validate(post):
                counter += 1
                yield post

默认情况下, HNTopPostsSpider.fetch 会保留所有的结果。假如我们想要定义自己的过滤算法,只要新建自己的 PostFilter 类即可,下面是两个分别过滤 GitHub 与 BloomBerg 的 PostFilter 类:

class GithubPostFilter(PostFilter):
    def validate(self, post: Post) -> bool:
        return 'github' in post.link.lower()
class GithubNBloomPostFilter(PostFilter):
    def validate(self, post: Post) -> bool:
        if 'github' in post.link.lower() or 'bloomberg' in post.link.lower():
            return True
        return False

在 main() 函数中,我可以用不同的 post_filter 参数来实例化 HNTopPostsSpider 类,最终满足不同的过滤需求:

def main():
    # crawler = HNTopPostsSpider()
    # crawler = HNTopPostsSpider(post_filter=GithubPostFilter())
    crawler = HNTopPostsSpider(post_filter=GithubNBloomPostFilter())
    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)

与基于继承的方式一样,利用将“过滤算法”抽象为 PostFilter 类并以实例化参数的方式注入到 HNTopPostsSpider 中,我们同样实现了“开放-关闭原则”。

使用数据驱动思想来改造代码

在实现“开放-关闭”原则的众多手法中,除了继承与依赖注入外,还有一种经常被用到的方式: “数据驱动” 。这个方式的核心思想在于: 将经常变动的东西,完全以数据的方式抽离出来。当需求变动时,只改动数据,代码逻辑保持不动。

它的原理与“依赖注入”有一些相似,同样是把变化的东西抽离到类外部。不同的是,后者抽离的通常是类,而前者抽离的是数据。

为了让 HNTopPostsSpider 类的行为可以被数据驱动,我们需要使其接收 filter_by_link_keywords 参数:

class HNTopPostsSpider:
    """抓取 HackerNews Top 内容条目
    :param limit: 限制条目数,默认为 5
    :param filter_by_link_keywords: 过滤结果的关键词列表,默认为 None 不过滤
    """
    ITEMS_URL = 'https://news.ycombinator.com/'
    def __init__(self,
                 limit: int = 5,
                 filter_by_link_keywords: Optional[List[str]] = None):
        self.limit = limit
        self.filter_by_link_keywords = filter_by_link_keywords
    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            post = Post( ... ... )
            if self.filter_by_link_keywords is None:
                counter += 1
                yield post
            # 当 link 中出现任意一个关键词时,返回结果
            elif any(keyword in post.link for keyword in self.filter_by_link_keywords):
                counter += 1
                yield post

调整了初始化参数后,还需要在 main 函数中定义 link_keywords 变量并将其传入到 HNTopPostsSpider 类的构造方法中,之后所有针对过滤关键词的调整都只需要修改这个列表即可,无需改动 HNTopPostsSpider 类的代码,同样满足了“开放-关闭原则”。

def main():
    # link_keywords = None
    link_keywords = [
        'github.com',
        'bloomberg.com'
    ]
    crawler = HNTopPostsSpider(filter_by_link_keywords=link_keywords)
    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)

与前面的继承和依赖注入方式相比,“数据驱动”的代码更简洁,不需要定义额外的类。但它同样也存在缺点: 它的可定制性不如前面的两种方式 。假如,我想要以“链接是否以某个字符串结尾”作为新的过滤条件,那么现在的数据驱动代码就有心无力了。

如何选择合适的方式来让代码符合“开放-关闭原则”,需要根据具体的需求和场景来判断。这也是一个无法一蹴而就、需要大量练习和经验积累的过程。

总结

在这篇文章中,我通过一个具体的 Python 代码案例,向你描述了 “SOLID” 设计原则中的前两位成员: “单一职责原则” 与 “开放-关闭原则” 。

这两个原则虽然看上去很简单,但是它们背后蕴藏了许多从好代码中提炼而来的智慧。它们的适用范围也不仅仅局限在 OOP 中。一旦你深入理解它们后,你可能会惊奇的在许多设计模式和框架中发现它们的影子 (比如这篇文章就出现了至少 3 种设计模式,你知道是哪些吗?) 。

让我们最后再总结一下吧:

  • “S: 单一职责原则” 认为一个类只应该有一种被修改的原因
  • 编写更小的类通常更不容易违反 S 原则
  • S 原则同样适用于函数,你可以让函数和类协同工作
  • “O: 开放-关闭原则” 认为类应该对改动关闭,对扩展开放
  • 找到需求中频繁变化的那个点,是让类遵循 O 原则的重点所在
  • 使用子类继承的方式可以让类遵守 O 原则
  • 通过定义算法类,并进行依赖注入,也可以让类遵循 O 原则
  • 将数据与逻辑分离,使用数据驱动的方式也是改造代码的好办法

看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。 

为ESXI Host添加v3700 FC存储 物理环境: ESXI Host version 5.1.0 Brocade SAN Switch v3700 Storage 存储和ESXI Host分别用光纤线连接在SAN交换机上 1.查询ESXI Host 的WWN号: 2. SAN交换机上查询存储的WWPN号 SAN交换机: 3.建立zone,添加到cfg文件中 brocade8Gb 阅读详情

相关推荐

大疆OcuSync系统背后的黑科技:跳频扩频技术如何让你的无人机飞得更稳

本文深入解析了大疆OcuSync系统如何运用跳频扩频技术等先进无线通信方案,显著提升无人机在复杂环境下的飞行稳定性。系统通过跳频扩频与直接序列扩频技术协同工作,结合双频智能切换与多天线MIMO,有效对抗信号干扰与多径效应,确保控制指令与高清图传的可靠、低延迟传输,为用户带来更安心、更流畅的飞行体验。

ol7890123的博客 630

python之file文件写入操作

视频地址: python之file文件写入操作 更多精彩内容,敬请扫码关注(HelloWorld小码农):  

HelloWorld小码农的博客 972

2.ROS实用工具:坐标变换

本文围绕ROS坐标变换展开,介绍其在机器人系统中的关键作用,即统一不同传感器和执行器坐标系,服务数据融合等操作。基于ROS的tf库,从广播和监听TF变换两方面阐述实现过程,涵盖新建工作包、编写广播和监听代码、配置构建文件及编译等步骤。此外,介绍启动节点管理器后运行广播和监听节点的方法,并通过rqt_graph、rqt_tf_tree、tf_echo等工具可视化和获取坐标系变换信息。借助这些内容,读者不仅能掌握ROS坐标变换的实现,还能对机器人坐标变换系统的调试和理解有更深入认识 。

qq_74168740的博客 1174

【python】关于文件的读写

Python写入

weixin_47542662的博客 640

分享通达信破解交易接口的使用!

那其实通达信破解交易接口的使用方法比较复杂,这里以MA均线指标为例子,只要理解了这些操作步骤,就可以很容易地使用其他指标包括自己编写的指标来进行交易。委托结果(包含委托编号), 需要分配 1024*1024 字节的空间。委托结果数组, 每项结果需要分配 1024*1024 字节的空间。0: 买入, 1: 卖出, 2: 融资买入, 3: 融券卖出,4: (市价委托)上海五档即成剩撤, 深圳五档即成剩撤。4: 买券还券, 5: 卖券还款, 6: 现券还券。0: 上海限价委托, 深圳限价委托。

1841085904的博客 2361

告别Mac自带终端:iTerm2 + Oh My Zsh 保姆级配置指南(含国内镜像源)

本文提供了一份详细的Mac终端优化指南,涵盖iTerm2、Oh My Zsh和Homebrew的配置流程,特别针对国内用户提供镜像源解决方案。通过安装现代化工具链和优化配置,帮助开发者打造高效、美观的终端工作环境,提升日常开发效率。

weixin_42638178的博客 400

Python之禅:编写优雅Python代码的16个原则

假如你刚参与了一个算法项目,当你第一次打开这个项目时,发现里面已经有上万行与算法相关的代码,仔细查看过后,发现如下一些让你抓狂的问题:1、代码写的非常冗余,维护已经变得越来越困难。2、几乎...

华章IT官方博客 507

ubuntu 20.04系统离线安装nfs

ubuntu环境下,离线安装nfs

m0_62464865的博客 2244

写好 Python 代码的几条原则

程序设计的好与坏,早在我们青葱岁月时就接触过了,只是那是并不知道这竟如此重要。能够立即改善程序设计、写出“好”代码的知识有以下几点: •面向对象五个基本原则; •常见的三种架构; •绘图; •起一个好名字; •优化嵌套的 if else 代码; 当然,其他技术知识的丰富程度也决定了程序设计的好坏。例如通过引入消息队列解决双端性能差异问题、通过增加缓存层提高查询效率等。下面我们一起来看看,上面列出的知识点包含哪些内容,这些内容对代码和程序设计的改善有何帮助。 面向对象五个基本原则 本书作者是 2010 级学生

kyle1314608的博客 574

Innovus Function ECO实战:5步搞定数字后端设计中的网表修正(含Calibre LVS验证技巧)

本文提供了一份完整的Innovus Function ECO实战指南,详细解析了数字后端设计中网表修正的五步流程。从理解Function ECO概念、准备关键文件,到在Innovus中执行逻辑比对、自动布线及GUI检查,并重点攻克了ECO后Calibre LVS验证的难点与调试技巧,帮助工程师高效、精准地完成流片前的设计微调。

day7的博客 950

在 SAP BTP 上为应用开启多租户能力:基于 SaaS Provisioning 和 XSUAA 的完整实践指南

本文介绍了在SAP BTP上配置多租户应用的实战指南。主要内容包括: 多租户架构概述:解释provider子账户、consumer子账户和tenant的概念,以及数据隔离机制。 安全配置:在xs-security.json中设置tenant-mode为shared,并为SaaS Provisioning服务添加回调scope授权。 路由配置:通过manifest.yml设置TENANT_HOST_PATTERN来定义多租户访问模式,确保不同租户通过不同子域名访问同一应用。 服务绑定:将XSUAA和SaaS

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 147

Python 工匠:写好面向对象代码几个原则!

Python学习交流群:1004391443 Python 是一门支持多种编程风格的语言,面对相同的需求,拥有不同背景的程序员可能会写出风格迥异的 Python 代码。比如一位习惯编写 C 语言的程序员,通常会定义一大堆函数来搞定所有事情,这是“过程式编程”的思想。而一位有 Java 背景的程序员则更倾向于设计许多个相互关联的类 (class) ,这是 “面向对...

Pythoncxy的博客 410

《Autosar_MCAL高阶配置》总目录_培训教程持续更新中...

《Autosar_MCAL高阶配置》总目录_培训教程持续更新中...

「汽车电子助手」的博客 1万+

14-Python 工匠:写好面向对象代码的原则(下)

依赖倒置原则”与“接口隔离原则”。这两条原则之间有一个共同点,那就是它们都和“抽象”有着紧密的联系。前者告诉我们要面向抽象而非实现编程,后者则教导我们在设计抽象时应该做到精准。“D:依赖倒置原则”认为高层模块和低层模块都应该依赖于抽象依赖抽象,意味着我们可以完全修改低层实现,而不影响高层代码在 Python 中你可以使用 abc 模块来定义抽象类除 abc 外,你也可以使用其他技术来完成依赖倒置“I:接口隔离原则”认为客户不应该依赖任何它不使用的方法设计接口就是设计抽象。

wujianyouhun的专栏 949

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

12- Python 工匠:写好面向对象代码的原则(上)

单一职责原则”与“开放-关闭原则”。这两个原则虽然看上去很简单,但是它们背后蕴藏了许多从好代码中提炼而来的智慧。它们的适用范围也不仅仅局限在 OOP 中。一旦你深入理解它们后,你可能会惊奇的在许多设计模式和框架中发现它们的影子(比如这篇文章就出现了至少 3 种设计模式,你知道是哪些吗?。“S: 单一职责原则”认为一个类只应该有一种被修改的原因编写更小的类通常更不容易违反 S 原则S 原则同样适用于函数,你可以让函数和类协同工作“O: 开放-关闭原则”认为类应该对改动关闭,对扩展开放。

wujianyouhun的专栏 1236

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

13-Python 工匠:写好面向对象代码的原则(中)

里氏替换原则。“里氏替换原则”是一个非常具体的原则,它专门为 OOP 里的继承场景服务。当你设计类继承关系,尤其是编写子类代码时,请经常性的问自己这个问题:“如果我把项目里所有使用父类的地方换成这个子类,程序是否还能正常运行?如果答案是否定的,那么你就应该考虑调整一下现在的类设计了。调整方式有很多种,有时候你得把大类拆分为更小的类,有时候你得调换类之间的继承关系,有时候你得为父类添加新的方法和属性,就像文章里的第一个场景一样。只要开动脑筋,总会找到合适的办法。“L:里氏替换原则”

wujianyouhun的专栏 1100

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Python 工匠:写好面向对象代码几个原则

点击上方“涛哥聊Python”,选择“星标”公众号重磅干货,第一时间送达本文转载自piglei,禁二次转载Python 是一门支持多种编程风格的语言,面对相同的需求,拥有...

涛哥聊Python 560

python中面向对象的ui_Python 工匠:写好面向对象代码的原则(上)

????点击上方蓝字“Python猫”,免费获得一个公众号花下猫语:今天继续给大家分享一篇好文章,有助于 Python 进阶的。Python 是一门支持面向对象的语言,但它跟典型的面向对象语言不完全相同。如何在 Python 中写出良好的面向对象代码呢?全文较长,建议收藏后慢慢阅读。图 | 宫崎骏电影《侧耳倾听》作者:piglei |公众号:piglei(本文经原作者授权转载,不得二次转载)...

weixin_39767645的博客 124

python编写ATM类_Python 工匠:写好面向对象代码几个原则

点击上方“涛哥聊Python”,选择“星标”公众号重磅干货,第一时间送达本文转载自piglei,禁二次转载Python 是一门支持多种编程风格的语言,面对相同的需求,拥有不同背景的程序员可能会写出风格迥异的 Python 代码。比如一位习惯编写 C 语言的程序员,通常会定义一大堆函数来搞定所有事情,这是“过程式编程”的思想。而一位有 Java 背景的程序员则更倾向于设计许多个相互关联的类...

weixin_39570838的博客 376

Python 工匠精神

我一直觉得编程某种意义上是一门『手艺』,因为优雅而高效的代码,就如同完美的手工艺品一样让人赏心悦目。 在雕琢代码的过程中,有大工程:比如应该用什么架构、哪种设计模式。也有更多的小细节,比如何时使用异常(Exceptions)、或怎么给变量起名。那些真正优秀的代码,正是由无数优秀的细节造就的。 『Python 工匠』这个系列文章,是我的一次小小尝试。它专注于分享 Python 编程中的一些偏『小』的东西。希望能够帮到每一位编程路上的匠人。 ...

qq_17122127的博客 857

eclipse和idea代码通用吗_Python 工匠:写好面向对象代码的原则(中)

文章来源于piglei ,作者piglei在这篇文章中,我将继续介绍 SOLID 原则的第三位成员:L(里氏替换原则)。里氏替换原则与继承在开始前,我觉得有必要先提一下 继承(Inheritance)。因为和前面两条非常抽象的原则不同,“里氏替换原则”是一条非常具体的,和类继承有关的原则。在 OOP 世界里,继承算是一个非常特殊的存在,它有点像一把无坚不摧的双刃剑,强大且危险。合理使用继承,可以大...

weixin_39728544的博客 1259

里氏替换原则_Python 工匠:写好面向对象代码的原则(中)

前言 这是 “Python 工匠”系列的第 13 篇文章。[查看系列所有文章]在 上一篇文章 里,我用一个虚拟小项目作为例子,讲解了“SOLID”设计原则中的前两位成员:S(单一职责原则)与 O(开放-关闭原则)。在这篇文章中,我将继续介绍 SOLID 原则的第三位成员:L(里氏替换原则)。里氏替换原则与继承在开始前,我觉得有必要先提一下 继承(Inheritance)。因为和前面两条非常抽象的原...

weixin_39966465的博客 170

Python开发必看书籍:《Python工匠》

python相关的就业方向很多,学校开设的python课程大多集中在数据分析和数据挖掘领域。然而实际工作中,算法工程师完成模型算法之后,往往还需要负责接口的开发上线。偏工程的数据治理方向偏业务的数据分析方向偏算法的数据挖掘方向偏开发的数据开发方向偏产品的数据产品经理我在入职两年的时候负责了一个大项目的开发工作,设计了5个功能模块,接入了1个查询接口2个数据库7个预测模型,一整套弄下来非常的struggle。Python开发远比我预期的难度要大。

2401_84205765的博客 1835

1-Python 工匠:善用变量来改善代码质量

这个原则属于老生常谈了。很多人(包括我)在刚开始学习编程时,会有一个习惯。就是把所有的变量定义写在一起,放在函数或方法的最前面。path = []... ...这样做只会让你的代码『看上去很整洁』,但是对提高代码可读性没有任何帮助。更好的做法是,让变量定义尽量靠近使用。那样当你阅读代码时,可以更好的理解代码的逻辑,而不是费劲的去想这个变量到底是什么、哪里定义的?# 为所有性别为女性,或者级别大于 3 的活跃用户发放 10000 个金币return看见if后面那一长串了吗?有点难读对不对?

wujianyouhun的专栏 1068

依赖倒置原则_Python 工匠:写好面向对象代码的原则(下)

(给Python开发者加星标,提升Python技能)来源:piglei在这篇文章中,我将继续介绍 SOLID 原则剩下的两位成员:I(接口隔离原则) 和 D(依赖倒置原则)。为了方便,这篇文章将会使用先 D 后 I 的顺序。D:依赖倒置原则软件是由一个个模块组合而成的。当你跟别人说:“我在写一个很复杂的软件”,其实你并不是直接在写那个软件,你只是在编写它的一个个模块,最后把它们放在一起组...

weixin_39668496的博客 182

Python工匠 | 全书要点汇总

对各章的总结汇总在了一起,全文6000余字。

成长之路 1089
上一篇: Github 标星 4w+,如何用 Python 实现所有算法!
下一篇: Django Web开发技术栈清单-Python基础篇!
普通网友
博客等级 码龄7年 438粉丝 263原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值