1 Seven principles of design pattern

Design pattern principles are the principles that programmers should follow as they program, and are the basis of design patterns (i.e., why design patterns are designed the way they are).

The seven common principles of design patterns are:

  1. Single Responsibility Principle
  2. Interface isolation principle
  3. Rely on the inversion principle
  4. The Richter principle of substitution
  5. Open Closed Principle (OCP)
  6. Demeter’s rule
  7. Principle of composite reuse

Single responsibility principle

The Single Responsibility Principle

2.1 Basic Introduction

For a class, that is, a class should be responsible for only one responsibility. For example, category A is responsible for two different responsibilities: duty 1, duty 2. When the requirement of Responsibility 1 changes to A, it may cause the execution error of Responsibility 2, so the granularity of Class A needs to be decomposed into A1 and A2.

2.2 Application Examples

Take the case of transportation to explain.

public class SingleResponsibility { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); Vehicle.run (" motorcycle "); Vehicle. The run (" car "); Vehicle. The run (" aircraft "); }} /** * Transportation class * 1. In the run method of this mode, the Single Responsibility Principle is violated * 2. The solution is very simple, depending on how the vehicle operates, */ class Vehicle {public void run(String Vehicle) {System.out.println(Vehicle + "...."); ); }}

Solution 1:

/** * Analysis of programme 1 * 1. Compliance with the principle of single responsibility * 2. However, this is a big change, that is, the class is decomposed, and the client is modified at the same time. Modify the Vehicle class directly, 2 */ public class singleResponse {public static void main(String[] args) {RoadVehicle RoadVehicle = new RoadVehicle(); Roadvehicle.run (" motorcycle "); RoadVehicle. Run (" car "); AirVehicle airVehicle = new AirVehicle(); AirVehicle. Run (" aircraft "); }} class Roadvehicle {public void run(String vehicle) {System.out.println(vehicle +);} class Roadvehicle {public void run(String vehicle) {System.out.println(vehicle +); }} class AirVehicle {public void run(String Vehicle) {System.out.println(Vehicle + "SkyTrain "); }} class WaterVehicle {public void run(String vehicle) {System.out.println(vehicle + "WaterVehicle "); }}

Solution 2:

/ scheme 2 * * * * 1. The modified method not to modify the original class to do big, just add method * 2. The single responsibility principle is not observed here at the class level, but at the method level, Again, observe a SingleResponsibility (recommended for a SingleResponsibility at the class level) */ public class singleResponse {public static void main(String[] args) {Vehicle vehicle2 = new Vehicle(); Vehicle2. Run (" car "); Vehicle2. RunWater (" ship "); Vehicle2. RunAir (" aircraft "); }} class Vehicle {public void run(String Vehicle) {public void run(String Vehicle) {System.out.println(Vehicle + "...."); ); } public void runAir(String Vehicle) {System.out.println(Vehicle + "...."); ); } public void runWater(String Vehicle) {System.out.println(Vehicle + "...."); ); }}

2.3 Precautions and details of the Single Responsibility Principle

  1. Reduce the complexity of classes so that each class is responsible for only one task.
  2. Improve class readability and maintainability.
  3. Reduce the risk of change.
  4. In general, we should adhere to the Single Responsibility Principle, which can only be violated at the code level if the logic is simple enough. Only the number of methods in a class is small enough to maintain the single-responsibility principle at the method level

The single responsibility principle is relative, keeping the relative responsibilities of a class single. If the complexity of the class is not high, the Single Responsibility Principle may be violated appropriately. In practice developers don’t have to use if else if else if complicated branches as much as they can, and use classes instead.

3. Interface isolation principle

Interface Segregation Principle

3.1 Basic introduction

  1. A client should not rely on interfaces it does not need, that is, a class’s dependency on another class should be based on the smallest interface.
  2. Let’s start with a picture:
  3. Class A depends on class B through the interface Interface1, and class C depends on class D through the interface Interface1. If interface Interface1 is not the minimum interface for classes A and C, then classes B and D must implement methods they do not need.
  4. The isolation principle is to split the interface Interface1 into separate interfaces (here we split it into three), and classes A and C have dependencies on the interfaces they need, respectively. That is, use the interface isolation principle.

3.2 Application Examples

  1. Class A depends on class B through the interface Interface1, and class C depends on class D through the interface Interface1. Write code to complete this application example.
  2. Look at the code – no code using the interface isolation principle.
Public class SeGregation1 {public static void main(String[] args) {}} // Interface Interface1 {void Operation1 (); void operation2(); void operation3(); void operation4(); void operation5(); } class B implements Interface1 {public void operation1() {System.out.println("B implements operation1");} class B implements Interface1 {public void operation1() {System.out.println(); } public void operation2() {System.out.println();} public void operation2() {System.out.println(); } public void operation3() {System.out.println();} public void operation3() {System.out.println(); } public void operation4() {System.out.println();} public void operation4() {System.out.println(); } public void operation5() {System.out.println();} public void operation5() {System.out.println(); }} class D implements Interface1 {public void operation1() {System.out.println("D implements operation1"); } public void operation2() {System.out.println();} public void operation2() {System.out.println(); } public void operation3() {System.out.println("D ");} public void operation3() {System.out.println("D "); } public void operation4() {System.out.println("D ");} public void operation4() {System.out.println("D "); } public void operation5() {System.out.println();} public void operation5() {System.out.println(); Public void depend1(Interface1 I) {i.operation1(); public void depend1(Interface1 I) {i.operation1(); public void depend1(Interface1 I) {i.operation1(); } public void depend2(Interface1 i) { i.operation2(); } public void depend3(Interface1 i) { i.operation3(); Public void depend1(Interface1 I) {i.operation1(); public void depend1(Interface1 I) {i.operation1(); public void depend1(Interface1 I) {i.operation1(); } public void depend4(Interface1 i) { i.operation4(); } public void depend5(Interface1 i) { i.operation5(); }}

Solutions:

Public class SeGregation2 {public static void main(String[] args) {// A = new A(); a.depend1(new B()); // Class A depends on class B through the interface a.depend2(new B()); a.depend3(new B()); C c = new C(); c.depend1(new D()); C (new D()); C (new D()); c.depend5(new D()); }} // void operation1() {void operation1(); } // interface 2 Interface2 {void operation2(); void operation3(); } // void operation4() {void operation4(); void operation5(); } class B implements Interface1, Interface2 {public void operation1() {System.out.println("B implements operation1");} class B implements Interface1, Interface2 {public void operation1() {System.out.println(); } public void operation2() {System.out.println();} public void operation2() {System.out.println(); } public void operation3() {System.out.println();} public void operation3() {System.out.println(); }} class D implements Interface1, Interface3 {public void operation1() {System.out.println("D implements operation1");} class D implements Interface1, Interface3 {public void operation1() {System.out.println(); } public void operation4() {System.out.println("D ");} public void operation4() {System.out.println("D "); } public void operation5() {System.out.println();} public void operation5() {System.out.println(); }} // Class A depends on (uses) Class B through the interface Interface1,Interface2, Class A {public void depend1(Interface1 I) {i.opperation1 (); } public void depend2(Interface2 i) { i.operation2(); } public void depend3(Interface2 i) { i.operation3(); }} // Class C depends on (uses) class D through the interface Interface1,Interface3, Class C {public void depend1(Interface1 I) {i.opperation1 (); } public void depend4(Interface3 i) { i.operation4(); } public void depend5(Interface3 i) { i.operation5(); }}

3.3 Improved use of interface isolation principle

  1. Class A depends on class B through the interface Interface1, and class C depends on class D through the interface Interface1. If interface Interface1 is not the minimum interface for classes A and C, then classes B and D must implement methods they do not need.
  2. The interface Interface1 is split into several separate interfaces, and classes A and C have dependencies on the interfaces they need, respectively. That is to take the method presented in the interface isolation principle interface Interface1 and split it into three interfaces according to the actual situation.

4. Rely on the reverse principle

4.1 Basic Introduction

Dependence Inversion Principle means:

  1. High-level modules should not depend on lower-level modules; both should depend on their abstractions.
  2. Abstractions should not depend on details, details should depend on abstractions. Depend on interfaces rather than implementations.
  3. The central idea of dependency inversion is programming to an interface.
  4. The principle of dependency inversion is based on the design idea that abstract things are much more stable than the variability of details. Architecture based on abstraction is much more stable than architecture based on detail. In Java, abstraction refers to interfaces or abstract classes, and details are concrete implementation classes.
  5. The purpose of using interfaces or abstract classes is to set the specification without involving any concrete operations, leaving the task of presenting the details to their implementation classes. The value of interfaces and abstract classes is in the design.

4.2 Application Examples

Please programmatically complete the function of Person to receive messages. Implementation method:

public class DependencyInversion { public static void main(String[] args) { Person person = new Person(); person.receive(new Email()); }} class Email {public String getInfo() {return "Email message: hello,world"; }} /** * Complete Person function to receive messages * mode 1 analysis * 1. * 2. If the object we get is WeChat, SMS, etc., we will add a new class, and meanwhile, Person will also add the corresponding receiving method * 3. Solution: An abstract interface iReceiver is introduced to represent the receiver, so that the Person class is dependent on the interface iReceiver * since Email, Weixin and so on are within the range of receiving. If they implement the iReceiver interface respectively, */ class Person {public void receive(Email Email) {System.out.println(email.getInfo()); }}

Optimization (rely on inversion) :

Public class dependencyInversion {public static void main(String[] args) {// Client need not change Person Person = new Person(); person.receive(new Email()); person.receive(new WeiXin()); } // Interface iReceiver {String getInfo(); } class Email implements iReceiver {public String getInfo() {return "Email message: hello,world"; Public String getInfo() {return "WeChat info: hello, OK ";} // add class WeiXin () {public String getInfo(); }} // Mode 2 class Person {// We are a dependency on the interface, not a direct dependency on the implementation class. public void receive(IReceiver receiver) { System.out.println(receiver.getInfo()); }}

4.3 Precautions and details of relying on the inversion principle

  1. Low-level modules should try to have abstract classes or interfaces, or both, the program is more stable.
  2. Variables should be declared as abstract classes or interfaces, so that there is a buffer layer between our variable reference and the actual object, which is easy to scale and optimize.
  3. Follow the Richter principle of substitution in succession.

4.4 Three ways of dependency passing

1) Interface transfer; 2) Construction method transfer; 3) Setter method passing;
Public class DependencyPass {public static void main(String[] args) {// Passing Changhong Changhong1 = new Changhong ();  OpenAndClose openAndClose1 = new OpenAndClose(); openAndClose1.open(changHong1); OpenAndClose2 OpenAndClose = new OpenAndClose2(Changhong1); openAndClose.open(); OpenAndClose3 = new OpenAndClose3(); // OpenAndClose3 = new OpenAndClose3(); openAndClose3.setTv(changHong1); openAndClose.open(); Void open(ITV TV) {// void open(ITV TV) {// void open(ITV TV); } interface ITV {// void play(); } class Changhong implements ITV {@Override public void play() {System.out.println();} class Changhong implements ITV {@Override public void play() {System.out.println(); }} class OpenAndClose implements IOpenAndClose {@Override public void Open (ITV TV) {Tv. Play (); }} // void Open (); // void Open (); // void Open (); } class OpenAndClose2 implements IOpenAndClose2 { public ITV tv; // public OpenAndClose2(ITV TV) {this. TV = TV; } @Override public void open() { this.tv.play(); }} // pass Interface IOpenAndClose3 {void Open (); void setTv(ITV tv); } class OpenAndClose3 implements IOpenAndClose3 { private ITV tv; @Override public void setTv(ITV tv) { this.tv = tv; } @Override public void open() { this.tv.play(); }}

The Richter Principle of Substitution

5.1 Considerations and explanations on inheritance in OO

  1. Inheritance implies that any implemented method in a superclass is actually setting conventions and contracts, and while it does not force all subclasses to follow those contracts, it would break the inheritance system if the subclass made arbitrary changes to the implemented methods.
  2. Inheritance not only brings convenience to program design, but also brings disadvantages. For example, the use of inheritance will bring intrusion to the program, reduce the portability of the program, increase the coupling between objects, if a class is inherited by other classes, when the class needs to be modified, all the subclasses must be taken into account, and after the modification of the parent class, all the functions involving the subclasses are likely to fail.
  3. The question is: how to use inheritance correctly in programming? => Richter Replacement Principle

5.2 Basic Introduction

  1. The Liskov Substitution Principle was proposed in 1988 by a woman at MIT with the last name Li.
  2. If for every object o1 of type T1, there is an object o2 of type T2, such that the behavior of program P defined in terms of T1 does not change when all objects o1 are substituted for o2, then type T2 is a subtype of type T1. In other words, all references to the base class must be able to transparently use objects of its subclasses.
  3. When using inheritance, follow the Richter substitution principle and try not to override the methods of the parent class in the subclass.
  4. The Richter Replacement Principle tells us that inheritance actually makes the two classes more coupled, and where appropriate, problems can be solved through aggregation, composition, and dependency.

5.3 Questions and reflections arising from a program

Look at a program and think about the problem and how to solve it:

public class Liskov { public static void main(String[] args) { A a = new A(); System.out.println("11-3=" + a.func1(11, 3)); System.out.println("1-8=" + a.func1(1, 8)); System.out.println("-----------------------"); B b = new B(); Println ("11-3=" + b.unc1 (11, 3)); println("11-3=" + b.unc1 (11, 3)); // 1-8 System.out.println("11+3+9=" + b.func2(11, 3)); System.out.println("1-8=" + b.func1(1, 8)); Public int func1(int num1, int num2) {return num1 - num2; public int func1(int num1, int num2) {return num1 - num2; Public int func1(int A, int B) {return A + B; public int func1(int A, int B) {return A + B; Public int func2(int a, int b) {return func1(a, b) + 9; }}

5.4 Solutions

  1. We found an error in the subtraction function that was working properly. The reason is that class B inadvertently overrides the parent class’s methods, causing errors in the original functionality. In actual programming, we often complete new functions by overwriting the parent class, which is simple to write, but the reuse of the whole inheritance system will be relatively poor. Especially if you’re running a lot of polymorphism.
  2. The common approach is: the original parent class and subclass inherit a more common base class, the original inheritance relation is removed, and the dependency, aggregation, combination and other relations are used instead.
  3. Improvement scheme: code implementation:
public class Liskov { public static void main(String[] args) { A a = new A(); System.out.println("11-3=" + a.func1(11, 3)); System.out.println("1-8=" + a.func1(1, 8)); System.out.println("-----------------------"); B b = new B(); // Because class B no longer inherits from class A, the caller no longer subtracts func1. Println ("11+3=" + b.unc1 (11, 3)); println("11+3=" + b.unc1 (11, 3)); // 1+8 System.out.println("11+3+9=" + b.func2(11, 3)); System.out.println("1+8=" + b.func1(1, 8)); Println ("11-3=" + b.unc3 (11, 3)); println("11-3=" + b.unc3 (11, 3)); Class A extends Base {// This extends Base class extends Base {// This extends Base class extends Base {// This extends Base class extends Base {// This extends Base class extends Base {// This extends Base class extends Base int func1(int num1, int num2) { return num1 - num2; }} private A = new A(); private A = new A(); private A = new A(); private A = new A(); private A = new A(); Public int func1(int A, int b) {return A + b; } public int func2(int a, int b) { return func1(a, b) + 9; } public int func3(int A, int b) {return this.a.func1(A, b); }}

6 The Open Closed Principle

6.1 Basic introduction

  1. The Open Closed Principle is the most fundamental and important design Principle in programming.
  2. A software entity such as classes, modules, and functions should be open for extension (for providers) and closed for modification (for consumers). Build frameworks with abstractions and extend details with implementations. If the provider extends or modifies the code, the user does not need to modify it, which can be considered compatible.
  3. When the software needs to change, try to do so by extending the behavior of the software entity, rather than by modifying existing code.
  4. The purpose of following other principles in programming, and of using design patterns, is to follow the Open Closed Principle.

6.2 Look at the following code

  1. See a drawing graph function. Class diagram design, as follows:
  2. Code demo
Public class Ocp {public static void main(String[] args) {public static void main(String[] args) {public static void main(String[] args) { Rectangle = new Rectangle = new Rectangle = new Rectangle = new Rectangle = new Rectangle; graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); }} // This is a drawing class [user] class GraphicEditor {// take Shape object, and then according to type, Public void DrawShape (Shape S) {if (s.m_type == 1) DrawRectangle (s); else if (s.m_type == 2) drawCircle(s); else if (s.m_type == 3) drawTriangle(s); } public void drawRectangle(Shape R) {System.out.println();} public void drawRectangle(Shape R) {System.out.println(); } public void drawCircle(Shape r) {System.out.println(" drawCircle "); } public void drawTriangle(Shape) {System.out.println();} public void drawTriangle(Shape) {System.out.println(); }} class Shape {int m_type; } class Rectangle extends Shape { Rectangle() { super.m_type = 1; } } class Circle extends Shape { Circle() { super.m_type = 2; }} class Triangle extends Shape {Triangle() {super.m_type = 3; }}

6.3 Advantages and disadvantages of Option 1

  1. Advantages are relatively easy to understand, simple and easy to operate.
  2. The disadvantage is that it violates the OCP principle of the design pattern, which is open for extensions (for providers) and closed for modifications (for consumers). That is, when we add new functionality to a class, we try to change the code as little as possible, or as little as possible.
  3. For example, if we want to add a new graph type triangle at this time, we need to make the following modifications, and there are many modifications

6.4 Analysis of improvement ideas

Create Shape class as abstract class, and provide an abstract draw method, let subclasses to implement it, so that when we have a new type of graphics, we only need to let the new graphics class inherit Shape, and draw method can be implemented, the user code does not need to modify -> meets the open and close principle.

Improved code:

Public class Ocp {public static void main(String[] args) {public static void main(String[] args) {public static void main(String[] args) { Rectangle = new Rectangle = new Rectangle = new Rectangle = new Rectangle = new Rectangle; graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); } public void drawShape(Shape s) {s.raw ();} public void drawShape(Shape s) {s.raw (); } class Shape {int m_type; public abstract void draw(); } class Rectangle extends Shape {Rectangle() {super.m_type = 1; Public void draw() {System.out.println();} public void draw() {System.out.println(); } } class Circle extends Shape { Circle() { super.m_type = 2; } public void draw() {System.out.println();} public void draw(); }} class Triangle extends Shape {Triangle() {super.m_type = 3; Public void draw() {System.out.println();} public void draw() {System.out.println(); } class otherGraphic extends Shape {otherGraphic () {super.m_type = 4; Public void draw() {System.out.println();} public void draw() {System.out.println(); }}

7 Demeter’s Rule

7.1 Basic Introduction

  1. One object should have minimal knowledge of other objects.
  2. The closer the relationship between classes, the greater the coupling degree.
  3. The Demeter Principle, also known as the least known Principle, states that the less a class knows about the classes it depends on, the better. That is, try to encapsulate logic inside the dependent classes, no matter how complex they are. Do not disclose any information other than the public methods provided.
  4. A simpler definition of Demeter’s Law is to communicate only with immediate friends.
  5. Direct friends: Every object is coupled to every other object. If two objects are coupled to each other, they are said to be friends. Coupling can be done in many ways: dependency, association, composition, aggregation, etc. Classes that appear in member variables, method parameters, and method return values are direct friends. Classes that appear in local variables are not direct friends. That is, it is best not to have an unfamiliar class inside the class as a local variable.

7.2 Application Examples

  1. There is a school which has various schools and headquarters under it. Now it is required to print out the ID of staff of the school headquarters and the ID of staff of the school.
  2. Programming to achieve the above functions, see the code demonstration
  3. Code demo:
Public class Demeter {public static void main(String[] args) {String () {String () {String () new SchoolManager(); / / output college staff headquarters staff id and school information schoolManager. PrintAllEmployee (new CollegeManager ()); }} // Class Employee {private String ID; public void setId(String id) { this.id = id; } public String getId() { return id; }} // Class CollegeEmployee {private String ID; public void setId(String id) { this.id = id; } public String getId() { return id; }} public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee>  list = new ArrayList<>(); List for (int I = 0; int I = 0; i < 10; i++) { CollegeEmployee emp = new CollegeEmployee(); Emp.setid (" Faculty employee id= "+ I); list.add(emp); } return list; }} /** * SchoolManager * Employee * CollegeManager * CollegeEmployee * CollegeManager * CollegeEmployee Public List<Employee> getAllEmployee() {List<Employee> List = new ArrayList<>(); List for (int I = 0; int I = 0; i < 5; i++) { Employee emp = new Employee(); Emp.setid (" Staff ID = "+ I); list.add(emp); } return list; } void printAllEmployee(ColleageManager sub) {/** * Analyse the problem * 1. 'CollegeEmployee' is not a direct friend of SchoolManager * 2. 'CollegeEmployee' appears as a local variable in SchoolManager * 3. */ * List<CollegeEmployee> list1 = sub.getAllEmployee(); System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- college staff -- -- -- -- -- -- -- -- -- -- -- -- "); for (CollegeEmployee e : list1) { System.out.println(e.getId()); } list2 = this.getAllEmployee();} list2 = this.getAllEmployee(); System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- the school headquarters staff -- -- -- -- -- -- -- -- -- -- -- -- "); for (Employee e : list2) { System.out.println(e.getId()); }}}

7.3 Improvement of application examples

  1. The problem with the previous design is that in SchoolManager the CollegeEmployee class is not a direct friend of the SchoolManager class (analysis).
  2. According to Demeter’s Law, you should avoid such coupling in classes that are not directly friends.
  3. Modify the code according to Demeter’s Law.
  4. Code demo:
Public class Demeter {public static void main(String[] args) {String () {String () {String () new SchoolManager(); / / output college staff headquarters staff id and school information schoolManager. PrintAllEmployee (new CollegeManager ()); }} // Class Employee {private String ID; public void setId(String id) { this.id = id; } public String getId() { return id; }} // Class CollegeEmployee {private String ID; public void setId(String id) { this.id = id; } public String getId() { return id; }} public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee>  list = new ArrayList<>(); List for (int I = 0; int I = 0; i < 10; i++) { CollegeEmployee emp = new CollegeEmployee(); Emp.setid (" Faculty employee id= "+ I); list.add(emp); } return list; } public void printEmployee() {List<CollegeEmployee> list1 = getAllEmployee(); System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- college staff -- -- -- -- -- -- -- -- -- -- -- -- "); for (CollegeEmployee e : list1) { System.out.println(e.getId()); }}} / class * * * * school management analysis SchoolManager classes of direct friends what the Employee, CollegeManager * CollegeEmployee not direct friends But an unfamiliar class, Public List<Employee> getAllEmployee() {List<Employee> List = new ArrayList<>(); List for (int I = 0; int I = 0; i < 5; i++) { Employee emp = new Employee(); Emp.setid (" Staff ID = "+ I); list.add(emp); } return list; } void printAllEmployee(ColleageManager sub) {/** * Analyse the problem * 1. Writing the output logic to the collegeManager class reduces coupling so that the SchoolManager doesn't have to be aware of the logic of the dependent classes. */ sub.printEmployee(); List2 = this.getAllEmployee(); list2 = this.getAllEmployee(); System. The out. Println (" -- -- -- -- -- -- -- -- -- -- -- -- the school headquarters staff -- -- -- -- -- -- -- -- -- -- -- -- "); for (Employee e : list2) { System.out.println(e.getId()); }}}

7.4 Precautions and details of Demeter’s Law

  1. The core of Demeter’s rule is to reduce coupling between classes
  2. Note, however: since each class reduces unnecessary dependencies, Demeter’s law requires only a reduction in coupling between classes (objects), not the complete absence of dependencies

Composite Reuse Principle

8.1 Basic Introduction

The principle is to use composition/aggregation as much as possible rather than inheritance.

Using inheritance increases the coupling between B and A if you just let B use A’s methods.

You can create A new method in class B, pass in the instance of A, and then call the method of A. 2) Aggregation mode: you can add global variables in class B, and assign values through set method; 3) combination: in the global variable B, direct new an A object, put A combination directly to B.

9 Core ideas of design principles

  1. Identify changes that might be needed in your application and keep them separate from code that doesn’t need to be changed.
  2. Program to interfaces, not implementations.
  3. Strive for loose coupling design between interacting objects.