资讯详情

设计模式之基本原则

基本原则

  • 一言概之
  • 食果去皮
    • 单一职责原则
      • 定义
      • 示例
        • 例子-违反原则的例子
        • 解决方法
      • 好处
    • 开闭原则
      • 定义
      • 如何设计
        • 例外情况
        • 如何设计
      • 示例
        • 计算面积
      • 水果示例
        • 更好的示例-spring示例
      • 好处
    • 替氏替换原则
      • 定义
      • 如何判断是否违反LSP
      • 示例
        • 正方形和长方形的示例
      • 好处
    • 依靠倒置原则
      • 定义
      • 实现
      • 示例
      • 好处
    • 接口隔离原则
      • 定义
      • 实现
      • 示例
      • 好处
    • 迪米特法则
      • 定义
      • 示例.
      • 好处
    • 组合/聚合复用原则
      • 定义
      • 示例
      • 好处
      • 缺点
  • 参考

一言概之

  1. 单一职责原则 (Single Responsibility Principle) 就一个类而言,它变化的原因应该只有一个。
  1. 开关原则 (Open-Closed Principle) 软件实体(类、模块、函数等)应对扩展开放,修改关闭
  1. 替氏替换原则 (Liskov Substitution Principle) 子类可以扩展父类的功能,但不能改变父类的原始功能
  1. 依靠倒转原则 (Dependence Inversion Principle) 面向接口编程
  1. 接口隔离原则 (Interface Segregation Principle) 一类对另一类的依赖应基于最小接口, 不应强迫客户依赖他们不使用的界面
  1. 迪米特法则(Law Of Demeter) 对其他类别了解的越少越好
  1. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle) 组合或聚合过于继承。

SOLID原则 RP – 单一职责原则 CP – 开/关原则 SP – Liskov 替换原则 SP – 接口隔离原则 IP – 依靠倒转原则

食果去皮

单一职责原则

定义

一类变化的原因不应超过一个。一类应注重单一功能,解决特定问题。

示例

例如,考虑编译和打印报告的模块。假设此类模块可以根据两个原因进行更改。首先,报告的内容可能会改变。第二,报告格式可能会改变。由于不同的原因,这两件事发生了变化。单责任原则认为,这两个问题实际上是两个独立的责任,所以应该放在独立的类别或模块中。将两个因不同原因而在不同时间改变的东西结合起来是一个糟糕的设计。 1

例子-违反原则的例子

class Journal { 
          string          m_title;  vector<string>  m_entries;  public:  explicit Journal(const string &title) : m_title{ 
        title} { 
        }  void add_entries(const string &entry) { 
           static uint32_t count = 1;   m_entries.pushback(to_string(count++) + ": " + entry);
	}
	auto get_entries() const { 
         return m_entries; }
	void save(const string &filename) { 
        
		ofstream ofs(filename); 
		for (auto &s : m_entries) ofs << s << endl;
	}
};

int  main() { 
        
    Journal journal{ 
        "Dear XYZ"};
    journal.add_entries("I ate a bug");
    journal.add_entries("I cried today");
    journal.save("diary.txt");
    return EXIT_SUCCESS;
}
  • 上面的c++例子看起来很好,只要你有一个单一的领域对象,例如Journal。但在实际应用程序中通常不是这样。
  • 当我们开始添加像Book, File等域对象时,你必须为每个人分别实现保存方法,这不是实际的问题。
  • 当您必须更改或维护保存功能时,真正的问题就出现了。例如,有一天你将不再保存数据文件和采用的数据库。在这种情况下,你必须遍历每个域对象实现&需要修改所有代码,这是不好的。
  • 在这里,我们违反了单一责任原则,为Journal类提供了更改的两个原因,即:
    • 与期刊相关的事情
    • 保存杂志
  • 此外,代码也会变得重复、臃肿和难以维护。

解决方法

 class Journal { 
        
	string          m_title;
	vector<string>  m_entries;

public:
	explicit Journal(const string &title) : m_title{ 
        title} { 
        } 
	void add_entries(const string &entry) { 
        
		static uint32_t count = 1;
		m_entries.push_back(to_string(count++) + ": " + entry);
	} 
	auto get_entries() const { 
         return m_entries; }

	//void save(const string &filename)
	//{ 
        
	// ofstream ofs(filename); 
	// for (auto &s : m_entries) ofs << s << endl;
	//}
};

struct SavingManager { 
        
	static void save(const Journal &j, const string &filename) { 
        
		ofstream ofs(filename);
		for (auto &s : j.get_entries())
			ofs << s << endl;
	}
};
SavingManager::save(journal, "diary.txt");
  • 日志应该只处理条目和与日志相关的事情。
  • 应该有一个独立的中心位置或实体来做拯救的工作。在我们的例子中,它是SavingManager。
  • 随着SavingManager的增长,所有与存储相关的代码都将放在一个地方。您还可以对它进行模板化以接受更多的域对象。

好处

  • 表现力
    1. 当类只做一件事时,它的接口通常有少量的方法,更具表现力。 因此,它也有少量的数据成员。
    2. 这提高了您的开发速度,并使您作为软件开发人员的生活更加轻松。
  • 可维护性
    1. 我们都知道需求会随着时间而变化,设计/架构也是如此。 你的class的责任越多,你就越需要改变它。 如果你的类实现了多个职责,它们就不再相互独立。
    2. 独立的更改减少了软件其他不相关区域的中断。
    3. 由于编程错误与复杂性成反比,因此更容易理解使代码不易出现错误并更易于维护。
  • 可重用性
    1. 如果一个类有多个职责,并且在软件的另一个领域只需要其中一个,那么其他不必要的职责会阻碍可重用性。
    2. 具有单一职责意味着该类应该是可重用的,无需或更少修改。

开闭原则

定义

– This means that the behavior of the module can be extended. As the requirements of the application change, we are able to extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does. – Extending the behavior of a module does not result in changes to the source or binary code of the module. The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched.

  • 对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
  • 对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。

两者结合起来表述为:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。2

如何设计

例外情况

在极少数情况下,代码的修改是绝对必要的,而且无法避免。

  1. 其中一个例子就是模块中存在的缺陷。在修复缺陷的情况下,允许更改模块代码及其各自的测试用例。
  2. 允许对现有代码进行任何更改,只要它不需要对该代码的任何客户端进行更改。这使得模块版本可以通过新的语言特性进行升级。例如,Spring 5支持并使用Java 8 lambda语法,但要使用它,我们不需要更改我们的客户端应用程序代码。

如何设计

  1. 继承实现: 类是封闭的,因为它可以被编译、存储在库中、基线化,并被客户端类使用。但它也是开放的,因为任何新类都可以使用它作为父类,从而增加了新特性。定义子类时,不需要更改原始类或干扰其客户端。3

Design for inheritance or prohibit it. – Effective Java (Addison-Wesley, 2008), Joshua Bloch

  1. 抽象与组合实现(接口继承): 将开/闭原则重新定义为多态开/闭原则。它使用接口而不是超类来允许不同的实现,您可以轻松地替换这些实现,而无需更改使用它们的代码。这些接口对修改是关闭的,您可以提供新的实现来扩展软件的功能。3

示例

计算面积

  1. 如果只有一个矩形的面积计算,那么我们可以这么写(违反OCP)
public class Rectangle
{ 
        
    public double Width { 
         get; set; }
    public double Height { 
         get; set; }
}
public class AreaCalculator
{ 
        
    public double Area(Rectangle[] shapes)
    { 
        
        double area = 0;
        foreach (var shape in shapes)
        { 
        
            area += shape.Width*shape.Height;
        }
        return area;
    }
}
  1. 如果再加一个圆形面积,我们还可以这么写(违反OCP)
public double Area(object[] shapes)
{ 
        
    double area = 0;
    foreach (var shape in shapes)
    { 
        
        if (shape is Rectangle)
        { 
        
            Rectangle rectangle = (Rectangle) shape;
            area += rectangle.Width*rectangle.Height;
        }
        else
        { 
        
            Circle circle = (Circle)shape;
            area += circle.Radius * circle.Radius * Math.PI;
        }
    }

    return area;
}
  1. 但如果我们要再加一个三角形,我们就不得不好好思索下自己的架构是否合适了,写下如下代码,这样我们只需要添加一个三角形的类就行了,而不用修改原有的逻辑。(符合OCP)
//基类
public abstract class Shape
{ 
        
    public abstract double Area();
}
//矩形面积计算
public class Rectangle : Shape
{ 
        
    public double Width { 
         get; set; }
    public double Height { 
         get; set; }
    public override double Area()
    { 
        
        return Width*Height;
    }
}
// 圆形面积计算
public class Circle : Shape
{ 
        
    public double Radius { 
         get; set; }
    public override double Area()
    { 
        
        return Radius*Radius*Math.PI;
    }
}
//通用计算
public double Area(Shape[] shapes)
{ 
        
    double area = 0;
    foreach (var shape in shapes)
    { 
        
        area += shape.Area();
    }

    return area;
}

水果示例

  1. 违反ocp原则
enum class COLOR { 
         RED, GREEN, BLUE };//颜色红、绿、蓝
enum class SIZE { 
         SMALL, MEDIUM, LARGE };//大小:小、中、大
struct Product { 
        
    string  m_name;//名称
    COLOR   m_color;//颜色
    SIZE    m_size;//大小
};
using Items = vector<Product*>;//产品容器
#define ALL(C) begin(C), end(C)
struct ProductFilter { 
        
		//过滤颜色
    static Items by_color(Items items, const COLOR e_color) { 
        
        Items result;
        for (auto &i : items)
            if (i->m_color == e_color)
                result.push_back(i);
        return result;
    }
    //过滤大小
    static Items by_size(Items items, const SIZE e_size) { 
        
        Items result;
        for (auto &i : items)
            if (i->m_size == e_size)
                result.push_back(i);
        return result;
    }
    //过滤二者的组合
    static Items by_size_and_color(Items items, const SIZE e_size, const COLOR e_color) { 
        
        Items result;
        for (auto &i : items)
            if (i->m_size == e_size && i->m_color == e_color)
                result.push_back(i);
        return result;
    }
};
int main() { 
        
    const Items all{ 
        
        new Product{ 
        "Apple", COLOR::GREEN, SIZE::SMALL},
        new Product{ 
        "Tree", COLOR::GREEN, SIZE::LARGE},
        new Product{ 
        "House", COLOR::BLUE, SIZE::LARGE},
    };
    for (auto &p : ProductFilter::by_color(all, COLOR::GREEN))
        cout << p->m_name << " is green\n";
    for (auto &p : ProductFilter::by_size_and_color(all, SIZE::LARGE, COLOR::GREEN))
        cout << p->m_name << " is green & large\n";
    return EXIT_SUCCESS;
}
/* Apple is green Tree is green Tree is green & large */
  • 我们有很多产品并且我们通过它的一些属性来过滤它。只要需求是固定的,上面的代码就没有什么问题(这在软件工程中永远不会是这样)。
  • 但是请想象一下这种情况:您已经将代码发送给客户端。随后,需求变化和一些新的过滤器是必需的。在这种情况下,您再次需要修改类并添加新的筛选器方法。
  • 这是一个有问题的方法,因为我们有两个属性(即颜色,大小),需要实现3个函数(即颜色,尺寸,其组合),多一个属性就需要实现8个函数
  • 你需要一遍又一遍地在现有的实现代码并且必须修改它,这可能会破坏其他部分的代码。这不是一个可扩展的解决方案。
  • 开放-关闭原则指出,你的系统应该对扩展开放,但对修改关闭。不幸的是,我们在这里做的是修改现有的代码,这是一个违反OCP的示例。
  1. 解决方法
  • 添加可扩展的抽象级别
template <typename T>
struct Specification { 
        
    virtual ~Specification() = default;
    virtual bool is_satisfied(T *item) const = 0;
};
struct ColorSpecification : Specification<Product> { 
        
    COLOR e_color;
    ColorSpecification(COLOR e_color) : e_color(e_color) { 
        }
    bool is_satisfied(Product *item) const { 
         return item->m_color == e_color; }
};
struct SizeSpecification : Specification<Product> { 
        
    SIZE e_size;
    SizeSpecification(SIZE e_size) : e_size(e_size) { 
        }
    bool is_satisfied(Product *item) const { 
         return item->m_size == e_size; }
};
template <typename T>
struct Filter { 
        
    virtual vector<T *> filter(vector<T *> items, const Specification<T> &spec) = 0;
};
struct BetterFilter : Filter<Product> { 
        
    vector<Product *> filter(vector<Product *> items, const Specification<Product> &spec) { 
        
        vector<Product *> result;
        for (auto &p : items)
            if (spec.is_satisfied(p))
                result.push_back(p);
        return result;
    }
};
// ------------------------------------------------------------------------------------------------
BetterFilter bf;
for (auto &x : bf.filter(all, ColorSpecification(COLOR::GREEN)))
    cout << x->m_name << " is green\n";

正如你所看到的,我们不需要修改BetterFilter的过滤方法。它可以满足所有的specification。 对于组合可以使用如下代码

template <typename T>
struct AndSpecification : Specification<T> { 
        
    const Specification<T> &first;
    const Specification<T> &second;
    AndSpecification(const Specification<T> &first, const Specification<T> &second)
    : first(first), second(second) { 
        }
    bool is_satisfied(T *item) const { 
         
        return first.is_satisfied(item) && second.is_satisfied(item); 
    }
};
template <typename T>
AndSpecification<T> operator&&(const Specification<T> &first, const Specification<T> &second) { 
        
    return { 
        first, second};
}
// -----------------------------------------------------------------------------------------------------
auto green_things = ColorSpecification{ 
        COLOR::GREEN};
auto large_things = SizeSpecification{ 
        SIZE::LARGE};
BetterFilter bf;
for (auto &x : bf.filter(all, green_things && large_things))
    cout << x->m_name << " is green and large\n";

// warning: the following will compile but will NOT work
// auto spec2 = SizeSpecification{SIZE::LARGE} &&
// ColorSpecification{COLOR::BLUE}

对于两个以上的规范,可以使用可变参数模板。

更好的示例-spring示例

Spring的设计和实现非常完美,您可以扩展它的任何部分特性,并将您的自定义实现开箱即用。

好处

  • 可扩展性 “当对程序的一次更改导致对相关模块的一连串更改时,该程序就会表现出我们认为与‘糟糕’设计相关联的不良属性。 程序变得脆弱、僵化、不可预测和不可重用。 开闭原则以非常直接的方式解决了这一问题。 它说你应该设计永不改变的模块。 当需求发生变化时,您可以通过添加新代码来扩展此类模块的行为,而不是通过更改已经工作的旧代码。”— 罗伯特·马丁
  • 可维护性 这种方法的主要好处是。因此,您可以轻松应对客户不断变化的需求。 在敏捷方法中非常有用。
  • 灵活性
  1. 开闭原则也适用于插件和中间件架构。 在这种情况下,您的基础软件实体就是您的应用程序核心功能。
  2. 在插件的情况下,您有一个基础或核心模块,可以通过通用网关接口插入新特性和功能。 Web 浏览器扩展就是一个很好的例子。
  3. 二进制兼容性也将在后续版本中保持不变。

里氏替换原则

定义

  1. Liskov 替换原则指出: “在计算机程序中,如果 S 是 T 的子类型,则类型 T 的对象可以被类型 S 的对象替换(即,类型 S 的对象可以替换类型 T 的对象)而不改变该程序的任何理想属性(正确性、执行的任务等)” 。
  2. 使用对基类的引用的方法必须能够在不知情的情况下使用派生类的对象。也就是说
    • 如果我在 C++ 的上下文中解决这个问题,这实际上意味着使用指向基类的指针/引用的函数必须能够被其派生类替换。
    • Liskov 替换原则围绕确保正确使用继承展开。
  3. LSP 有时被表示为duck test的反例:“如果它看起来像鸭子,叫起来像鸭子,但需要电池——你可能有错误的抽象”。马克思兄弟(Marx Brothers)对“鸭子测试”的重新表述是:“他可能看起来像个白痴,说话也像个白痴,但不要被这一点骗了。他真的是个白痴。”这句话的幽默之处在于它违背了预期的反面。通俗点将就是

如何判断是否违背LSP

LSP适用于存在超类型-子类型继承关系的情况,可以是类的扩展,也可以是接口的实现。我们可以将超类型中定义的方法视为定义契约。每个子类型都应该遵守这个契约。如果一个子类不遵守父类的契约,它就违反了LSP。

  1. 产生 子类中的方法如何打破父类方法的契约?有几种可能的方法:
    • 返回与超类方法返回的对象不兼容的对象。
    • 抛出超类方法没有抛出的新异常。
    • 更改语义或引入不属于父类契约的副作用。
  2. 一些识别指标
    • 客户端代码中的条件逻辑(使用instanceof操作符或object.getClass().getName()来标识实际的子类)也就是说在多态代码块中存在类型检查代码多数是LSP违规了。 例如,如果你在Foo类型的对象集合上有一个std::for_each循环,在这个循环中,有一个检查Foo是否真的是Bar(Foo的一种子类型),那么几乎可以肯定这是一个LSP违规。相反,你应该确保Bar在所有方面都可以替代Foo,应该没有必要包括这样的检查。-
    • 子类中一个或多个方法是空的、不做任何事情的实现
    • 从子类方法抛出UnsupportedOperationException或其他意外异常,从父类的契约角度来看,异常需要是不可预料的。因此,如果我们的超类方法的签名明确指定子类或实现可以抛出UnsupportedOperationException,那么我们就不会认为它违反了LSP。

示例

正方形和长方形的示例

  1. 违反了LSP 从数学的角度,正方形是长方形的特例,所以两者之间存在一种“是”的关系。这诱使我们创建一个继承自Rectangle类的Square类。
struct Rectangle { 
        
    Rectangle(const uint32_t width, const uint32_t height) : m_width{ 
        width}, m_height{ 
        height} { 
        }

    uint32_t get_width() const { 
         return m_width; }
    uint32_t get_height() const { 
         return m_height; }

    virtual void set_width(const uint32_t width) { 
         this->m_width = width; }
    virtual void set_height(const uint32_t height) { 
         this->m_height = height; }

    uint32_t area() const { 
         return m_width * m_height; }

protected:
    uint32_t m_width, m_height;
};

struct Square : Rectangle { 
        
    Square(uint32_t size) : Rectangle(size, size) { 
        }
    void set_width(const uint32_t width) override { 
         this->m_width = m_height = width; }
    void set_height(const uint32_t height) override { 
         this->m_height = m_width = height; }

        标签: way系列圆形耦合连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台