一.设计原则:代表一组指南,帮助我们避免设计不良,它更多的是一种高度的抽象,思想。而设计模式可以看做设计原则的一种实现。
常见面向对象原则:
单一职责原则
1.1 single responsibility principle:单一职责原则,一个类应该仅有一个引起它变化的原因,如果我们有2个以上要修改这个类的原因,那我们就要拆分它。
1.2 为什么要这么做?
想象一下,如果设计具有多个责任/实现多个功能的类。在开发时间内,你的类可以在自身中创建的依赖的数量是恐怖的。所以当你被要求改变某个功能时,你不能真正地确定它将如何影响类中实现的其他功能。你的更改可能会影响或可能不会影响其他功能,但您并不能承担风险,尤其是在生产应用程序中。所以你最终测试所有的依赖特性,那将耗费大量时间同时也不好维护。
假如:要求您实现UserSettingService,其中用户可以更改设置,但在此之前,用户必须通过身份验证。
//bad example
public class UserSettingService
{
public void changeEmail(User user)
{
if(checkAccess(user))
{
//Grant option to change
}
}
public boolean checkAccess(User user)
{
//Verify if the user is valid.
}
}
看起来似乎不错,直到你想在其他地方重复使用checkAccess代码,或者你想改变checkAccess的方式,或者你想改变电子邮件的更方式。在后面的2个情况下,你最终都会改变这个类,所以有2个地方会引起这个类的变化。在第一种情况下,你必须使用UserSettingService来检查访问,这是不必要的。
一种方法是将UserSettingService分解为UserSettingService和SecurityService。并将checkAccess代码移动到SecurityService。
//good example
public class UserSettingService
{
public void changeEmail(User user)
{
if(SecurityService.checkAccess(user))
{
//Grant option to change
}
}
}
public class SecurityService
{
public static boolean checkAccess(User user)
{
//check the access.
}
}
1.3 结论:单一职责原则代表着在应用程序的设计阶段标志类的好方法,它提醒你覆盖一个类可以发展的所有方式。 只有当应用程序应该如何工作的全部情况都被理解时,才能完成良好的责任分离。
开-闭原则
2.1 open-closed principle:对扩展开放,对修改关闭。合理抽象,分离出变化和不变化的部分,为变化的部分留下可扩展的功能。
以下是违反开放关闭原则的示例。它实现一个图形编辑器处理不同形状的绘图。很明显,它不遵循开放关闭原则,因为GraphicEditor类必须为每个添加的新形状类进行修改。它有几个缺点:
a. 对于每个新的形状添加的GraphicEditor的单元测试应该重做。
b. 当添加新类型的形状时,添加它的时间将会很长,因为添加它的开发人员应该理解GraphicEditor的逻辑。
c. 添加新形状可能以不期望的方式影响现有功能,即使新形状完美地工作。
// Open-Close Principle - Bad example
class GraphicEditor {
public void drawShape(Shape s) {
if (s.m_type==1)
drawRectangle(s);
else if (s.m_type==2)
drawCircle(s);
}
public void drawCircle(Circle r) {....}
public void drawRectangle(Rectangle r) {....}
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
}
以下是支持开放关闭原则的示例。 在新设计中,我们在GraphicEditor中使用抽象draw()方法绘制对象,同时在具体形状对象中实现。 使用开放关闭原则避免了先前设计中的问题,因为添加新的形状类时,GraphicEditor不会更改,有以下优点:
a. 无需单元测试。
b. 无需了解GraphicEditor的源代码。
c. 因为绘图代码被移动到具体的形状类,当添加新的功能时,降低了影响旧功能的风险。
// Open-Close Principle - Good example
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
class Shape {
abstract void draw();
}
class Rectangle extends Shape {
public void draw() {
// draw the rectangle
}
}
2.2 结论:做出灵活的设计需要花费额外的时间和精力,并且它引入了新的级别的抽象,增加了代码的复杂性。 因此,这个原则应该应用于最有可能改变的领域。
里氏替换原则
3.1 里氏替换原则LSP:如果程序模块使用的基类,则对基类的引用可以替换为派生类,而不会影响程序模块的功能,换句话说:子类必须能够替换它的父类。并且我们必须确保子类只是扩展而不替换父类的功能。 否则,新类在现有程序模块中使用时会产生不良影响。
注意:这个原则与开放封闭原则(OCP)非常密切相关,如果违反LSP那么也将违反OCP。如果子类不能替换为父类引用,那么为了支持子类,我们继续对现有代码进行更改并添加支持,这将严重违反OCP。
以下是违反里氏替换原则的典型示例。 在示例中使用2个类:Rectangle和Square。 让我们假设Rectangle对象在应用程序中的某处使用。 我们扩展应用程序并添加Square类。 正方形类由工厂模式返回,我们不知道确切的返回对象的类型。 但我们知道这是一个矩形。 我们得到矩形对象,将宽度设置为5,高度设置为10,并获取区域。 对于宽度为5和高度为10的矩形,区域应为50.然而,结果确是100。
// Violation of Likov's Substitution Principle
class Rectangle {
protected int m_width;
protected int m_height;
public void setWidth(int width){
m_width = width;
}
public void setHeight(int height){
m_height = height;
}
public int getWidth(){
return m_width;
}
public int getHeight(){
return m_height;
}
public int getArea(){
return m_width * m_height;
}
}
class Square extends Rectangle
{
public void setWidth(int width){
m_width = width;
m_height = width;
}
public void setHeight(int height){
m_width = height;
m_height = height;
}
}
class LspTest {
private static Rectangle getNewRectangle() {
// it can be an object returned by some factory ...
return new Square();
}
public static void main (String args[]) {
Rectangle r = LspTest.getNewRectangle();
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle.
// It assumes that he's able to set the width and height as for the base class
System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}
依赖倒置原则
4.1 依赖倒置原则DIP:依赖抽象,不要依赖具体类。要求:高层模块不应该依赖于底层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,而是具体实现依赖于抽象。
下面是违反依赖反转原则的示例。我们有Manager,这是一个高级类,而低级类称为Worker。我们需要在我们的应用程序中添加一个新模块,以模拟由新的专业工作者雇用决定的公司结构的变化。我们为此创建了一个新类SuperWorker。
让我们假设Manager类相当复杂,包含非常复杂的逻辑。现在我们必须改变它,以引入新的SuperWorker。让我们看看缺点:
a. 我们必须更改Manager类(记住它是一个复杂的,这将涉及时间和精力进行更改)。
b. Manager类中的某些当前功能可能会受到影响。
c. 单元测试应重做。
所有这些问题可能需要很多时间来解决,它们可能在旧的功能中引入新的错误。如果应用程序是按照依赖反转原则设计的,情况会有所不同。这意味着我们设计了manager类,一个IWorker接口和实现IWorker接口的Worker类。当我们需要添加SuperWorker类时,我们所要做的就是为它实现IWorker接口。在现有类中没有其他更改。
// Dependency Inversion Principle - Bad example
class Worker {
public void work() {
// ....working
}
}
class Manager {
Worker worker;
public void setWorker(Worker w) {
worker = w;
}
public void manage() {
worker.work();
}
}
class SuperWorker {
public void work() {
//.... working much more
}
}
下面是支持依赖反转原理的代码。 在这个新设计中,通过IWorker接口添加了一个新的抽象层。 现在上面的代码中的问题被解决了(考虑到高级逻辑没有变化):
a. 在添加SuperWorkers时,Manager类不需要更改。
b. 最小化风险以影响Manager类中的旧功能,因为我们不更改它。
c. 无需为Manager类重做单元测试。
// Dependency Inversion Principle - Good example
interface IWorker {
public void work();
}
class Worker implements IWorker{
public void work() {
// ....working
}
}
class SuperWorker implements IWorker{
public void work() {
//.... working much more
}
}
class Manager {
IWorker worker;
public void setWorker(IWorker w) {
worker = w;
}
public void manage() {
worker.work();
}
}
接口隔离原则
5.1 接口隔离原则ISP:不应该强迫客户依赖于他们不用的方法。分离的方式:代理或者委托。
下面是违反接口隔离原则的示例。我们有一个Manager类,代表管理工人的人。我们有两种类型的工人一些平常和一些非常有效的工人。这两种类型的工人工作,他们需要每天休息。但现在有些机器人来到他们工作的公司,他们不需要吃东西,所以他们不需要休息。一个新的机器人类需要实现IWorker界面,因为机器人工作。在另一方面,不必完全继承它,因为他们不吃东西。
这就是为什么在这种情况下,IWorker被认为是一个污染的接口。
如果我们保持当前的设计,新的Robot类被强制实现eat方法。我们可以编写一个不做任何事情的虚拟类(假设每天打断1秒),并且在应用中可能有不良影响(例如,管理者看到的报告会报告比所需人数多的午餐)。
根据接口隔离原则,灵活的设计不会有接口污染。在我们的例子中,IWorker接口应该分为两个不同的接口。
// interface segregation principle - bad example
interface IWorker {
public void work();
public void eat();
}
class Worker implements IWorker{
public void work() {
// ....working
}
public void eat() {
// ...... eating in launch break
}
}
class SuperWorker implements IWorker{
public void work() {
//.... working much more
}
public void eat() {
//.... eating in launch break
}
}
class Manager {
IWorker worker;
public void setWorker(IWorker w) {
worker=w;
}
public void manage() {
worker.work();
}
}
下面是支持接口隔离原则的代码。 通过在2个不同的接口中拆分IWorker接口,新的Robot类不再强制实现eat方法。 此外,如果我们需要另一个功能来给机器人充电,我们创建另一个接口IRechargeble的方法充电。
// interface segregation principle - good example
interface IWorker extends Feedable, Workable {
}
interface IWorkable {
public void work();
}
interface IFeedable{
public void eat();
}
class Worker implements IWorkable, IFeedable{
public void work() {
// ....working
}
public void eat() {
//.... eating in launch break
}
}
class Robot implements IWorkable{
public void work() {
// ....working
}
}
class SuperWorker implements IWorkable, IFeedable{
public void work() {
//.... working much more
}
public void eat() {
//.... eating in launch break
}
}
class Manager {
Workable worker;
public void setWorker(Workable w) {
worker=w;
}
public void manage() {
worker.work();
}
}
最少知识原则(迪米特法则)
6.1 最少知识原则LKP(迪米特法则):只与你的朋友谈话,减少对象之间的交互。
哪些是朋友:当前对象本身;通过方法的参数传递进来的对象;当前对象创建的对象;当前对象的实例变量引用的对象;方法内所创建或引用的对象。
public class LawOfDemeterInJava
{
private Topping cheeseTopping;
/**
* Good examples of following the Law of Demeter.
*/
public void goodExamples(Pizza pizza)
{
Foo foo = new Foo();
// (1) 可以调用我们自己的方法
doSomething();
// (2) 可以调传递进来参数的方法
int price = pizza.getPrice();
// (3) 可以调我们自己创建对象的方法
cheeseTopping = new CheeseTopping();
float weight = cheeseTopping.getWeightUsed();
// (4) 任何直接持有的组件对象
foo.doBar();
}
private void doSomething()
{
// do something here ...
}
}
//In short, the Law of Demeter aims to keep you from doing things like this:
objectA.getObjectB().doSomething();
//or even worse, this:
objectA.getObjectB().getObjectC().doSomething();
6.2 结论:使用迪米特法则:你的类将“松耦合”; 你的依赖性降低;重用你的类将更容易;你的类在其他类中不太需要修改;你的代码将更容易测试。
7.其他原则:面向接口编程;优先使用组合而非继承。
二.设计模式:是指在软件开发过程中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。
组成:模式名称,环境和问题,解决方案,效果。
分类:
-
a. 创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
-
b. 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
-
c. 行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)、访问者模式。
接口实现“封装隔离”。接口和抽象类使用原则:优先使用接口;在既要定义子类的行为,又要为子类提供公共的功能的时候用抽象类。
参考文献:研磨设计模式
参考案例:点击打开链接