jjzjj

想学会SOLID原则,看这一篇文章就够了!

Aaron-948 2023-03-28 原文

背景

在我们日常工作中,代码写着写着就出现下列的一些臭味。但是还好我们有SOLID这把‘尺子’, 可以拿着它不断去衡量我们写的代码,除去代码臭味。这就是我们要学习SOLID原则的原因所在。

设计的臭味

  • 僵化性
    • 具有联动性,动一处,会牵连到其他地方
  • 脆弱性
    • 不敢改动,动一处,全局瘫痪
  • 顽固性
    • 不易改动
  • 粘滞性
    • 耦合性太高
  • 不必要的复杂性
    • 代码设计过于复杂
  • 不必要的重复
    • 提高复用性,减少重复
  • 晦涩性
    • 代码设计不易理解

SRP-单一职责原则

  • 一个类只做一件事情。当然一件事情,不是说类中只有一个方法。而是类中的方法都是属于同一种职责。
  • 不能因为第二职责的原因去改动这个类。

一个很好的例子:在我们封装request库时,我们需要实现以下4个方法.

class MyRequestClient:
    
    def post(self):
        pass
    def get(self):
        pass  
    def update(self):
        pass
    def delete(self):
        pass
        
    #上面的方法就是属于同一职责。 如何还有其他的方法,那么这个类就不符合单一职责原则。
    #例增加以下方法:
    def get_db_data(self):
        pass
    def to_object(self):
        pass
       

OCP-开放封闭原则

  • 对扩展开放,对修改封闭。
  • 无需改动自身代码,就可以扩展它的行为。
  • 对类的改动往往是新增代码就可以了,而不是去修改原有的代码。
  • 使用子类继承、依赖注入、数据驱动的方法可以实现OCP原则。

首先我们来看一个违反OCP原则的例子。

#bad code
def circle_draw():
    print(f"this is circle draw")

def square_draw():
    print(f"this is square draw")

def draw_all_shape(shapes):
    for shape in shapes:
        if shape == "circle":
            circle_draw()
        if shape == "square":
            square_draw()

这段代码的问题是如果再有新的类型需要draw, 我们需要修改draw_all_shape函数来适配新的类型。

依赖注入实现OCP原则

我们定义了一个抽象类Shape, 子类Square和Circle继承Shape. 并且在子类中重写了父类的方法。函数draw_all_shape是绘制所有图形。

from typing import List
from abc import ABCMeta, abstractmethod


class Shape(metaclass=ABCMeta):

    @abstractmethod
    def draw(self):
        pass


class Square(Shape):

    def draw(self):
        print(f"this is square draw")


class Circle(Shape):

    def draw(self):
        print(f"this is circle draw")


def draw_all_shape(shapes: List[Shape]):
    for shape in shapes:
        shape.draw()

我们定义了一个抽象类Shape, 子类SquareCircle继承Shape. 并且在子类中重写了父类的方法。函数draw_all_shape是绘制所有图形。

参数注入实现OCP原则

def circle_draw():
    print(f"this is circle draw")


def square_draw():
    print(f"this is square draw")


def draw_all_shape_by_function(data: Dict[str,Callable]):
    for key,value in data.items():
        value()


data = {
    "circle": circle_draw,
    "square": square_draw
}

draw_all_shape_by_function(data=data)

Conclusion

  • 这样的设计的好处是,如果需要再绘制一个三角形,那么我们只需要增加一个新类并继承Shape.无需修改shape类和draw_all_shape就可以实现三角形类的绘制。
  • 当我们在类中或函数中需要使用大量的if-else逻辑判断时,很有可能代码就违反了OCP原则。

LSP:Liskov 替换原则

  • 派生类应该可以替换父类中的方法使用,而不会改变程序原本的功能。
  • 派生类重写方法的参数应该和父类的保持一致或多于父类,不能少于父类。
  • 派生类重写方法的返回值必须和父类返回值类型一致。
  • 违反LSP原则,通常也会违反OCP原则。

首先我们来看一段违法LSP的例子

from typing import Iterable
class User():
    def __init__(self, user: str) -> None:
        self.user = user
    def disable(self) -> None:
        print(f"{self.user} disable!")        
  
  
class Admin(User):
    def __init__(self, user: str = "Admin") -> None:
        self.user = user
    def disable(self):
        raise "Admin do not disable!"
   
   
def delete_user(users: Iterable[User]):
    for user in users:
        user.disable()

当执行delete_user时,就会抛出TypeError 错误,Admin类中disable方法违法了LSP替换原则。

Optimize

#Good
from typing import Iterable

class User():
    def __init__(self, user: str) -> None:
        self.user = user

    def allow_disable(self):
        return True

    def disable(self) -> None:
        print(f"{self.user} disable!")        
    

class Admin(User):
    def __init__(self, user: str = "Admin") -> None:
        self.user = user

    def allow_disable(self):
        return False
    

def delete_user(users: Iterable[User]) -> None:
    for user in users:
        if user.allow_disable:
            user.disable()

Conclusion

  • 上例中通过添加allow_disable 的方法,解决了Admin类不能disable的问题。
  • 当派生类不正确的重写父类方法的时候,就会违反LSP原则,我们在继承类的时候重写方法的时候,尤其- 要注意是否违反了LSP原则。

ISP 接口隔离原则

  • 客户应该不依赖它不使用的方法。
  • 一个类只做一件事。

首先来看一个违反ISP原则的例子:

class Animal(metaclass=ABCMeta):

    @abstractclassmethod
    def run(self):
        pass

    @abstractclassmethod
    def speak(self):
        pass

    @abstractclassmethod
    def fly(self):
        pass


class Dog(Animal):

    def run(self):
        return "Dog Running"

    def speak(self):
        return "Dog Speaking"

    def fly(self):
        raise TypeError("Dog can not fly")


class Bird(Animal):

    def run(self):
        raise TypeError("Bird can not run")

    def speak(self):
        return "Bird Speaking"

    def fly(self):
        return "Bird fly"


def fly_animal(animals: Iterable[Animal]):
    for animal in animals:
        animal.fly()

当我们执行fly_animal时,就会抛出TypeError的错误。此时Animal抽象类是一个胖类,违法了ISP原则。

Optimize

  • 将Animal抽象类分解为三个新抽象类,FlyingAnimal, TalkingAnimal, RunningAnimal, 底层代码按需继承。
#good
class FlyingAnimal(metaclass=ABCMeta):

    @abstractclassmethod
    def fly(self):
        pass


class RunningAnimal(metaclass=ABCMeta):

    @abstractclassmethod
    def run(self):
        pass


class TalkingAnimal(metaclass=ABCMeta):

    @abstractclassmethod
    def talk(self):
        pass


class Dog(RunningAnimal,TalkingAnimal):

    def run(self):
        return "Dog Running"

    def talk(self):
        return "Dog Speaking"


class Bird(FlyingAnimal, TalkingAnimal):

    def talk(self):
        return "Bird Speaking"

    def fly(self):
        return "Bird fly"


def fly_animal(animals: Iterable[FlyingAnimal]):
    for animal in animals:
        print(animal.fly())

Conclusion

  • 接口隔离原则看似和单一职责原则相似,单一职责原则是针对模块,类,方法的设计。接口隔离原则更注重在调用者的角度,按需提供接口。
  • 写更小的类,大多数情况下是个好主意。
  • 违反ISP原则也可能会违反LSP原则和SRP原则。
  • 当子类重写了一个不需要的方法时,很可能违反了ISP原则。

DIP 依赖倒置原则

  • 程序中所有的依赖都应该终止于抽象类或接口。
  • 任何类都不应该从具体类派生。
  • 任何方法都不易应该重写它的任何基类已经实现了的方法。
  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。

首先看一个违反DIP原则的例子:

class Lamp:
    def turn_on(self):
        print("turn on the lamp")
    
    def turn_off(self):
        print("turn off the lamp")


class Button():

    def __init__(self) -> None:
        self.lamp = Lamp()
    
    def turn_on(self):
        return self.lamp.turn_on()

    def turn_off(self):
        return self.lamp.turn_off()

当有一天,button需要控制televsion时,就需要修改Button类。ButtonLamp 具有强耦合关系。所以,当Lamp变动时,会影响到Button类。违法了DIP原则的高层模块依赖于底层模块。

Optimize

定义一个抽象类ElectricAppliance Button 和 Lamp 都依赖这个抽象类。 解决了ButtonLamp 具有强耦合的问题。

class ElectricAppliance(metaclass=ABCMeta):

    @abstractmethod
    def turn_on(self):
        pass

    @abstractmethod
    def turn_off(self):
        pass


class Lamp(ElectricAppliance):
    def turn_on(self):
        print("turn on the lamp")

    def turn_off(self):
        print("turn off the lamp")


class Television(ElectricAppliance):
    def turn_on(self):
        print("turn on the televison")

    def turn_off(self):
        print("turn off the televison")


class Button:

    def __init__(self, electric_appliance: ElectricAppliance) -> None:
        self.electric_appliance = electric_appliance

    def turn_on(self):
        self.electric_appliance.turn_on()

    def turn_off(self):
        self.electric_appliance.turn_off()

Conclusion

  • 要确定代码是否违反了DIP原则,需要观察一个类中是否嵌入了调用其他类或函数。如果是,那么很可能是违反了DIP原则。

有关想学会SOLID原则,看这一篇文章就够了!的更多相关文章

  1. ruby - 最佳原则中的原则 - 2

    我似乎经常遇到一些设计问题,但我不知道是什么是真的很合适。一方面我经常听到我应该限制耦合和坚持单一职责,但当我这样做时,我常常发现它很困难到在需要时将信息获取到程序的一部分。为了例如,classSingerdefinitialize(name)@name=nameendattr:nameend那么Song应该是:classSongdefnew(singer)@singer=singerendend或classSongdefnew(singer_name)@singer_name=singer_nameendend后者耦合性小,按道理应该用。但如果我以后发现宋有什么需要了解更多歌手,我的

  2. ruby-on-rails - rails delete_if 使用哈希忽略当前文章(中间人) - 2

    我为你们准备了一个简单的。我想要一个特色内容部分,其中排除了当前文章所以这可以通过delete_if使用MiddlemanBlog:但是我使用的是中间人代理,所以我无法访问current_article方法...我有一个YAML结构,其中包含以下模拟数据(以及其他数据),文件夹设置如下:data>site>caseStudy>RANDOM-ID423536.yaml(由CMS生成)在每个yaml文件中,您会发现如下内容::id:2k1YccJrQsKE2siSO6o6ac:title:Heyplace我的config.rb看起来像这样data.site.caseStudy.eachdo

  3. ruby - 为什么方法调用在原则上可以是常量时需要消除歧义? - 2

    方法调用通常可以省略接收者和参数的括号:deffoo;"foo"endfoo#=>"foo"在上面的例子中,foo在方法调用和对潜在局部变量的引用之间是不明确的。在没有后者的情况下,它被解释为方法调用。但是,当方法名原则上可以是常量名时(即,当它以大写字母开头,并且仅由字母组成时),似乎需要消歧。defFoo;"Foo"endFoo#=>NameError:uninitializedconstantFooFoo()#=>"Foo"self.Foo#=>"Foo"为什么会这样?为什么即使在没有同名常量的情况下,也需要明确区分方法调用和对常量的引用? 最佳答案

  4. ruby - 在 Middleman 中移动博客文章位置 - 2

    我正在为我的网站使用MiddlemanBloggem,但默认情况下,博客文章似乎需要位于/source中,这在查看vim中的树时并不是特别好并尝试在其中找到其他文件之一(例如模板)。通过查看文档,我看不出是否有任何方法可以移动博客文章,以便将它们存储在其他地方,例如blog_articles文件夹或类似文件夹。这可能吗? 最佳答案 将以下内容放入您的config.rb文件中。activate:blogdo|blog|blog.permalink=":year-:month-:day-:title.html"blog.sources=

  5. ruby-on-rails - 文章#index 中的 Ruby on Rails 教程 NoMethodError - 2

    所以我正在关注http://guides.rubyonrails.org/getting_started.html上的官方ROR教程我被困在第5.8节,它教我如何列出所有文章下面是我的controller和index.html.erbControllerclassArticlesControllerindex.html.erbListingarticlesTitleText我收到带有错误消息的NoMethodErrorinArticles#indexundefinedmethod`each'fornil:NilClass"怎么了?我从网站上复制并粘贴了代码以查看我做错了什么,但仍然无法

  6. ruby-on-rails - 文章中的 ActionController::UrlGenerationError#edit - 2

    我收到以下错误:没有路由匹配{:action=>"show",:controller=>"articles",:id=>nil}缺少必需的键:[:id]以下是显示错误的代码。这是什么错误,每当我从上一个屏幕点击编辑时,我想我正在发送文章ID。这是我的rake路由输出PrefixVerbURIPatternController#Actionwelcome_indexGET/welcome/index(.:format)welcome#indexarticlesGET/articles(.:format)articles#indexPOST/articles(.:format)articl

  7. ruby-on-rails - Rails 5 上一篇或下一篇文章仅来自特定标签 - 2

    我有一个名为posts的资源,其中有很多。但是,每个帖子可以有多个标签。我希望用户只能从所选标签转到上一篇和下一篇文章。我让它适用于上一个下一个数据库中的所有帖子,但是当我单击一个标签并显示所有标签时,上一个/下一个不符合标签是什么。如果我访问与routes.rb中定义的代码关联的url,get'tags/:tag',to:'posts#index',as::tag,它会列出索引中的所有标签。我不想要这个,我希望用户能够单击上一个或下一个,并且只能在与标签关联的帖子上执行此操作。注意:我使用的是friendly_idgemcontrollers/posts_controller.rbd

  8. ruby-on-rails - 设计模式和设计原则有什么区别? - 2

    我是RubyonRails的新手,我阅读了这些文章。DesignPatternsinRuby:Observer,SingletonDesignPatternsinRuby但我无法理解设计模式和设计原则之间的实际区别。有人可以解释一下区别吗? 最佳答案 设计原则:设计原则是我们在设计软件时应该遵循的核心抽象原则。记住它们不是具体的——而是抽象的。只要我们在允许的条件内,它们就可以以任何语言、任何平台应用,无论处于何种状态。例子:封装变化的内容。针对接口(interface)而非实现编程。依赖抽象。不要依赖于具体的类。设计模式:它们是针

  9. 【保姆级】python最新版3.11.1开发环境搭建,看这一篇就够了 - 2

    【保姆级】Python最新版开发环境搭建,看这一篇就够了(适用于Python3.11.2安装)文章目录【保姆级】Python最新版开发环境搭建,看这一篇就够了(适用于Python3.11.2安装)一、Python解释器安装Windows安装步骤环境变量配置(非必要)MacOS安装步骤Linux安装步骤二、PyCharm安装三、创建Python工程工欲善其事必先利其器,在使用Python开发程序之前,在计算机上搭建Python开发环境是必不可少的环节,目前Python最新稳定版本是3.11.1,且支持到2027年,如下图所示本文手把手带你从0到1搭建Python最新版3.11.1开发环境,堪称保

  10. 接口测试重点内容看这一篇就够了 - 2

    1、接口的概念系统与系统之间,组件与组件之间,数据传递交互的通道2、接口的类型按协议划分:http、tcp、IP按语言划分:C++、java、PHP……按范围划分:系统之间多个内部系统之间内部系统与外部系统之间程序之间方法与方法之间、函数与函数之间、模块与模块之间3、接口测试的概念对系统或组件之间的接口进行测试,校验传递的数据正确性和逻辑依赖关系的正确行。4、接口测试的原理主要针对服务器,模拟客户端向服务器发送请求,通过工具或者代码来测试服务器针对客户端请求回发的响应数据是否与预期结果一致。5、接口测试的特点符合质量控制前移的理念可以发现一些页面操作发现不了的问题接口测试低成本高效益接口测试是

随机推荐