精通代码复用:设计原则与最佳实践

这篇具有很好参考价值的文章主要介绍了精通代码复用:设计原则与最佳实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

精通代码复用:设计原则与最佳实践

在你开始设计的所有层次上,从单一函数、类,到整个库和框架,都需要从一开始就考虑到代码复用。在接下来的文本中,所有这些不同的层次都被称为组件。以下策略将帮助你合理地组织你的代码。注意,所有这些策略都专注于使你的代码具有通用性。设计可复用代码的第二个方面,即提供易用性,更多地与你的接口设计相关,将在后面进行讨论。

避免合并无关或逻辑独立的概念

当你设计一个组件时,应该让它专注于一个单一任务或一组任务,即,你应该追求高内聚。这也被称为单一职责原则(SRP)。不要合并无关的概念,例如随机数生成器和XML解析器。即使你没有专门为了复用而设计代码,也要牢记这一策略。整个程序很少会被单独复用。相反,程序的部分或子系统会直接被纳入其他应用,或者被改编以用于稍微不同的用途。因此,你应该设计你的程序,以便你将逻辑上独立的功能划分为可以在不同程序中复用的独立组件。每个这样的组件都应具有明确定义的职责。这种程序策略模仿了现实世界中的离散、可互换部件的设计原则。

例如,你可以编写一个Car类,并将发动机的所有属性和行为都放入其中。然而,发动机是可分离的组件,不与汽车的其他方面绑定在一起。发动机可以从一辆汽车中取出,放入另一辆汽车中。一个合适的设计应该包括一个Engine类,其中包含所有与发动机相关的功能。一个Car实例然后只包含一个Engine实例。

将程序划分为逻辑子系统

你应该将你的子系统设计为可以独立复用的离散组件,即,追求低耦合。例如,如果你正在设计一个网络游戏,应该将网络和图形用户界面方面分开。这样,你就可以在不拖入另一个组件的情况下复用其中一个组件。例如,你可能想写一个非网络游戏,在这种情况下,你可以复用图形界面子系统,但不需要网络方面。同样地,你可以设计一个P2P文件共享程序,在这种情况下,你可以复用网络子系统,但不需要图形用户界面功能。确保遵循每个子系统的抽象原则。把每个子系统看作一个微型库,并为其提供一个连贯且易于使用的接口。即使你是唯一使用这些微型库的程序员,你也将从设计良好的接口和实现中受益,这些接口和实现将逻辑上不同的功能进行了分离。

使用类层次结构以分离逻辑概念

除了将程序划分为逻辑子系统外,你还应避免在类级别合并无关的概念。例如,假设你想为自动驾驶汽车编写一个类。你决定从一个基础的汽车类开始,并直接将所有自动驾驶逻辑加入其中。然而,如果你的程序中只需要一个非自动驾驶汽车呢?在这种情况下,与自动驾驶有关的所有逻辑都是无用的,可能会要求你的程序链接到它本来可以避免的库,如视觉库、LIDAR库等。一个解决方案是创建一个类层次结构,在其中自动驾驶汽车是通用汽车的派生类。这样,你就可以在不需要自动驾驶功能的程序中使用汽车基类,而不会招致这种算法的成本。

当有两个逻辑概念时,如自动驾驶和汽车,这种策略效果很好。当有三个或更多概念时,情况就变得更复杂了。例如,假设你想提供一辆卡车和一辆汽车,每辆都可能是自动驾驶或非自动驾驶的。从逻辑上讲,卡车和汽车都是车辆的特殊情况,因此它们应该是车辆类的派生类。同样,自动驾驶类可以是非自动驾驶类的派生类。你不能用一个线性层次结构提供这些分离。一个可能性是将自动驾驶方面作为一个混合类。通过使用多重继承在C++中实现了混合类的一种方式。例如,一个PictureButton可以从Image类和Clickable混合类继承。然而,对于自动驾驶设计,最好使用一种不同类型的混合实现,即使用类模板。基本上,SelfDrivable混合类可以定义如下:

template <typename T>
class SelfDrivable : public T {
};

这个SelfDrivable混合类提供了实现自动驾驶功能所需的所有算法。一旦你有了这个SelfDrivable混合类模板

,你就可以为汽车和卡车分别实例化一个:

SelfDrivable<Car> selfDrivingCar;
SelfDrivable<Truck> selfDrivingTruck;

这两行代码的结果是,编译器将使用SelfDrivable混合类模板创建一个实例,其中所有的T都被替换为Car,因此是从Car派生的,另一个实例的T被替换为Truck,因此是从Truck派生的。

使用聚合以分离逻辑概念

聚合在接下来的内容中讨论,它模拟了“有一个”关系:对象包含其他对象以执行其某些方面的功能。当继承不适当时,你可以使用聚合来分离无关或相关但独立的功能。

无论你的设计在哪个层次,都应避免合并无关的概念,即,追求高内聚。例如,在方法级别,单一方法不应执行逻辑上无关的事情,混合变异(set)和检查(get)等。

例如,假设你想写一个Family类来存储一个家庭的成员。显然,树状数据结构将是理想的存储这些信息的方式。你应该写一个单独的Tree类,而不是在你的Family类中集成树结构的代码。然后,你的Family类可以包含和使用一个Tree实例。用面向对象的术语来说,Family has-a Tree。采用这种技术,树状数据结构在另一个程序中更容易被复用。

消除用户界面依赖性

如果你的库是一个数据操作库,你会希望将数据操作与用户界面分开。这意味着对于这种类型的库,你绝对不应该假设库将在哪种类型的用户界面中使用。库不应使用任何标准输入和输出流,如coutcerrcin,因为如果库是在图形用户界面的环境中使用,这些流可能没有意义。例如,一个基于Windows GUI的应用程序通常不会有任何形式的控制台I/O。即使你认为你的库只会在基于GUI的应用程序中使用,你也绝不应弹出任何类型的消息框或其他类型的通知给最终用户,因为这是客户端代码的责任。客户端代码决定如何向用户显示消息。这种类型的依赖性不仅导致可复用性差,而且还阻止了客户端代码适当地响应错误,例如,静默处理它。

模型-视图-控制器(MVC)范式是一个用于分离数据存储和数据可视化的著名设计模式。使用这个范式,模型可以在库中,而客户端代码可以提供视图和控制器。

使用模板进行通用数据结构和算法设计

C++有一个叫做模板(Templates)的概念,它允许你创建对类型或类具有通用性的结构。例如,你可能已经为整数数组编写了代码。如果你随后想要一个双精度浮点数数组,你需要重写和复制所有代码以适应双精度浮点数。模板的概念是,类型变成了规范的一个参数,你可以创建一个可以在任何类型上工作的单一代码体。模板允许你编写在任何类型上工作的数据结构和算法。

最简单的例子是std::vector类,它是C++标准库的一部分。要创建一个整数向量,你写std::vector<int>;要创建一个双精度浮点数向量,你写std::vector<double>。模板编程通常非常强大,但也可能非常复杂。幸运的是,可以创建相对简单的模板用法,根据类型进行参数化。

无论何时有可能,你都应该使用通用设计来编写数据结构和算法,而不是编码某个特定程序的细节。不要编写只存储书籍对象的平衡二叉树结构。使其通用,以便它可以存储任何类型的对象。这样,你可以在书店、音乐商店、操作系统或任何需要平衡二叉树的地方使用它。

为什么模板比其他通用编程技术更好

模板并不是编写通用数据结构的唯一机制。另一种、尽管更老的方法是在C和C++中存储void*指针,而不是特定类型的指针。客户端可以通过将其转换为void*来存储他们想要的任何东西。然而,这种方法的主要问题是它不是类型安全的:容器不能检查或强制存储元素的类型。

与直接在你的通用非模板数据结构中使用void*指针相比,你可以使用自C++17以来可用的std::any类。std::any类的底层实现在某些情况下确实使用了void*指针,但它还跟踪了存储的类型,所以一切都保持了类型安全。

另一种方法是为特定类编写数据结构。通过多态性,该类的任何派生类都可以存储在结构中。模板,另一方面,在正确使用时是类型安全的。每个模板实例只存储一种类型。如果你尝试在同一个模板实例中存储不同的类型,你的程序将无法编译。此外,模板允许编译器为每个模板实例生成高度优化的代码。

模板的问题

模板并不完美。首先,它们的语法可能令人困惑,尤其是对于那些以前没有使用过它们的人。其次,模板需要同质的数据结构,在单一结构中只能存储相同类型的对象。这就是模板的类型安全性直接导致的限制。

从C++17开始,有一种标准化的方法来绕过这种同质性限制。你可以编写你的数据结构以存储std::variantstd::any对象。一个std::any对象可以存储任何

类型的值,而一个std::variant对象可以存储一系列类型中的一个值。std::anystd::variant在后文讨论。

模板的缺点:代码膨胀

模板的另一个可能的缺点是所谓的代码膨胀:最终二进制代码的大小增加。每个模板实例的高度专门化代码比稍慢的通用代码需要更多的代码。然而,通常来说,如今代码膨胀并不是一个很大的问题。

模板与继承

程序员有时发现决定是否使用模板或继承有点棘手。以下是一些帮助你做出决策的提示。

  • 当你想为不同类型提供相同的功能时,使用模板。例如,如果你想编写一个适用于任何类型的通用排序算法,使用函数模板。如果你想创建一个可以存储任何类型的容器,使用类模板。

  • 当你想为相关类型提供不同的行为时,使用继承。例如,在一个绘图应用程序中,使用继承来支持不同的形状,如圆形、正方形、线条等。特定的形状然后从一个基类(例如,Shape)派生。

值得注意的是,你可以组合继承和模板。你可以编写一个从基类模板派生的类模板。

提供适当的检查和保护措施

有两种相反的编写安全代码的风格。最佳的编程风格可能是两者之间的健康组合。

  1. 契约式设计(Design-by-Contract):这意味着函数或类的文档代表了一份合同,详细描述了客户端代码的责任和你的函数或类的责任。契约式设计有三个重要方面:前置条件、后置条件和不变量。

  2. 安全最大化设计:这一准则的最重要方面是在你的代码中进行错误检查。例如,如果你的随机数生成器需要种子在特定范围内,不要只是信任用户传递一个有效的种子。检查传入的值,并在无效时拒绝调用。

为可扩展性设计

你应该努力以这样一种方式设计你的类,使它们可以通过从它们派生另一个类来进行扩展,但它们应该是封闭的,即行为应该是可扩展的,而无需你修改其实现。这被称为开闭原则(OCP)。

作为一个例子,假设你开始实施一个绘图应用程序。第一个版本应该只支持正方形。你的设计包含两个类:SquareRenderer

class Square { /* Details not important for this example. */ };
class Renderer {
public:
    void render(const vector<Square>& squares) {
        for (auto& square : squares) {
            /* Render this square object... */
        }
    }
};

接下来,你添加对圆形的支持,所以你创建了一个Circle类。

class Circle { /* Details not important for this example. */ }

为了能够渲染圆形,你必须修改Renderer类的render()方法。

在这个设计中,如果你想添加对新类型形状的支持,你只需要编写一个从Shape派生并实现render()方法的新类。你不需要在Renderer类中修改任何内容。因此,这个设计可以在不修改现有代码的情况下进行扩展;也就是说,它是开放的,用于扩展和封闭的,用于修改。


参考:Professional C++ (English Edition) 5th Edition by Marc Gregoire

公众号:coding日记文章来源地址https://www.toymoban.com/news/detail-720250.html

到了这里,关于精通代码复用:设计原则与最佳实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java 设计模式最佳实践:6~9

    原文:Design Patterns and Best Practices in Java 协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【ApacheCN Java 译文集】,采用译后编辑(MTPE)流程来尽可能提升效率。 这一章将描述反应式编程范式,以及为什么它能很好地适用于带有函数元素的语言。读者将熟悉反应式编程背后的概念。我们

    2023年04月14日
    浏览(56)
  • Python中的设计模式与最佳实践

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在软件开发中,设计模式是一种解决常见问题的经过验证的解决方案。Python作为一种流行的编程语言,具有丰富的库和灵活的语法,使其成为

    2024年04月23日
    浏览(47)
  • React.js 中用于高质量应用程序的最佳实践和设计模式

    原文:Best Practices and Design Patterns in React.js for High-Quality Applications,适当增删 原作者:Ori Baram 文章已获原文作者授权,禁止转载和商用 不按文件类型对组件进行分组,而是按特征。示例: 小而集中的组件易于理解,维护和测试。 假设您有一个UserProfile组件代码体积逐渐变大

    2024年02月15日
    浏览(52)
  • 六大程序设计原则 + 合成复用原则

    目录 Global Diagram 依赖倒置原则(依赖抽象接口,而不是具体对象) 单一职责原则(类、接口、方法) 开闭原则 (扩展开放,修改关闭) 里氏替换原则(基类和子类之间的关系) 接口隔离原则(接口按照功能细分) 最少知道原则 (类与类之间的亲疏关系) 合成复用原则(

    2024年02月10日
    浏览(31)
  • 【ASP.NET Core 基础知识】--最佳实践和进阶主题--设计模式在ASP.NET Core中的应用

    一、设计模式概述 1.1 什么是设计模式 设计模式是在软件设计过程中反复出现的、经过验证的、可重用的解决问题的方法。它们是针对特定问题的通用解决方案,提供了一种在软件开发中可靠的指导和标准化方法。设计模式通常描述了一种在特定情景下的解决方案,包括了问

    2024年02月21日
    浏览(160)
  • 前端设计模式和设计原则之设计原则

    1 开闭原则 该原则指出软件实体(类、模块、函数等)应该 对扩展开放,对修改关闭 。也就是说,在添加新功能时,应该通过扩展现有代码来实现,而不是直接修改已有的代码。这样可以确保现有代码的稳定性,并且减少对其他部分的影响。 在上述例子中,有一个原始功能

    2024年02月07日
    浏览(39)
  • 【设计模式】设计原则-开闭原则

    定义 作用 1、方便测试;测试时只需要对扩展的代码进行测试。 2、提高代码的可复用性;粒度越小,被复用的可能性就越大。 3、提高软件的稳定性和延续性,易于扩展和维护。 实现方式 通过“抽象约束、封装变化”来实现开闭原则。通过接口或者抽象类为软件实体定义一

    2024年02月15日
    浏览(37)
  • 【设计模式】设计原则-里氏替换原则

    定义 任何基类可以出现的地方,子类一定可以出现。 通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。 换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。 针对的问题 主要作用就是规范继承时子类的一些书写规则。

    2024年02月14日
    浏览(49)
  • 【Java 设计模式】设计原则之开放封闭原则

    在软件开发中,设计原则是创建灵活、可维护和可扩展软件的基础。 这些原则为我们提供了指导方针,帮助我们构建高质量、易理解的代码。 ✨单一职责原则(SRP) ✨开放/封闭原则(OCP) ✨里氏替换原则(LSP) ✨依赖倒置原则(DIP) ✨接口隔离原则(ISP) ✨合成/聚合复

    2024年02月02日
    浏览(49)
  • 【Java 设计模式】设计原则之里氏替换原则

    在软件开发中,设计原则是创建灵活、可维护和可扩展软件的基础。 这些原则为我们提供了指导方针,帮助我们构建高质量、易理解的代码。 ✨单一职责原则(SRP) ✨开放/封闭原则(OCP) ✨里氏替换原则(LSP) ✨依赖倒置原则(DIP) ✨接口隔离原则(ISP) ✨合成/聚合复

    2024年01月20日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包