How to write a beautiful DSL using Objective-C

How to write a beautiful DSL using Objective-C

Foreword: This article is contributed by Zang Chengwei, an iOS technical expert from Meituan. After teaching two series of RactiveCocoa courses at StuQ, Mr. Zang recently opened a new course called "iOS Practical Black Magic". The course content involves a lot of low-level knowledge and application skills such as Objective-C Runtime and Swift. If you are interested, you can read the introduction at the end of the article.

Thanks to Zang Chengwei for the authorization, the following is the text of the article.

background

In program development, we always hope to express our logic more concisely and semantically. Chain call is a common way of processing. The third-party libraries we often use, such as Masonry and Expecta, adopt this processing method.

  1. //Masonry
  2. [view1 mas_makeConstraints:^(MASConstraintMaker *make) {
  3. make. top .equalTo(superview.mas_top). with .offset(padding. top );
  4. make. left .equalTo(superview.mas_left). with .offset(padding. left );
  5. make.bottom.equalTo(superview.mas_bottom). with .offset(-padding.bottom);
  6. make. right .equalTo(superview.mas_right). with .offset(-padding. right );
  7. }];

  1. //Expecta
  2. expect(@ "foo" ). to .equal(@ "foo" ); // ` to ` is a syntactic sugar and can be safely omitted.
  3. expect(foo).notTo.equal(1);
  4. expect([bar isBar]). to .equal(YES);
  5. expect(baz). to .equal(3.14159);

This kind of expression used in a specific field is called DSL (Domain Specific Language). This article will introduce how to implement a chain call DSL.

Implementation of chain call

Let’s take a specific example. For example, we use chain expressions to create a UIView, set its frame and backgroundColor, and add it to a parent View.

For the most basic Objective-C (before the iOS4 block appeared), if you want to implement chain calls, it can only be like this:

  1. UIView *aView = [[[[UIView alloc] initWithFrame:aFrame] bgColor:aColor] intoView:aSuperView];

With blocks, we can change the bracket syntax to dot syntax.

  1. UIView *aView = AllocA(UIView). with .position(x, y). size (width, height).bgColor(aColor).intoView(aSuperView);
  2.  
  3. // When x and y are the default values ​​0 and 0 or width and height are the default values ​​0, they can also be omitted
  4. UIView *bView = AllocA(UIView). with . size (width, height).bgColor(aColor).intoView(aSuperView);

It can be seen that the semantics of the chained syntax is very clear, and the syntax of the latter is more compact. Let's look at the implementation of the latter from two perspectives.

1. From a grammatical perspective

Chaining can be done in two ways:

1) Use attributes in the return value to save information in the method

For example, the .left .right .top .bottom and other methods in Masonry will return an instance of the MASConstraintMaker class when called, which has properties such as left/right/top/bottom to save information for each call;

  1. make. left .equalTo(superview.mas_left). with .offset(15);

For another example, the .notTo method in Expecta returns an instance of the EXPExpect class, which has a BOOL attribute self.negative to record whether .notTo is called;

  1. expect(foo).notTo.equal(1);

For another example, in the .with method in the above example, we can directly return self;

2). Use block type properties to accept parameters

For example, the .offset(15) method in Masonry receives a CGFloat as a parameter. You can add a block type property to the MASConstraintMaker class:

  1. @property (nonatomic, copy) MASConstraintMaker* (^offset)(CGFloat);

For example, in the example of .position(x, y), you can add an attribute to a class:

  1. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);

When the .position(x, y) method is called, this block is executed and the ViewMaker instance is returned to ensure that the chain call can be performed.

2. From a semantic perspective

From a semantic perspective, it is necessary to define which are auxiliary words and which need to accept parameters. In order to ensure that the chain call can be completed, it is necessary to consider what is passed in and what is returned.

Let’s take the example above as an example:

  1. UIView *aView = AllocA(UIView). with .position(x, y). size (width, height).bgColor(aColor).intoView(aSuperView);

Let's look at it step by step. This DSL expression needs to describe an imperative sentence, starting with Alloc and ending with intoView. Before the terminator of intoView, we modify UIView in some ways, using position, size, bgColor, etc.

Let's look at four sections below to see how to implement such an expression:

(1) Object

In the semantics of AllocA(UIView), we have determined that the object is a UIVIew. Since the UIView is determined at the end of intoView, we need to create an intermediate class to save all the intermediate conditions. Here we use the ViewMaker class.

  1. @interface ViewMaker : NSObject
  2. @property (nonatomic, strong) Class viewClass;
  3. @property (nonatomic, assign) CGPoint position;
  4. @property (nonatomic, assign) CGPoint size ;
  5. @property (nonatomic, strong) UIColor *color;
  6. @ end  

In addition, we can notice that AllocA is a function, and UIView cannot be directly passed to this function, so the syntax becomes AllocA([UIView class]) and loses its simplicity. So we need to define a macro to "swallow" the brackets and class method:

  1. #define AllocA(aClass) alloc_a([aClass class])
  2.  
  3. ViewMaker* alloc_a(Class aClass){
  4. ViewMaker *maker = ViewMaker.new;
  5. maker.viewClass = aClass;
  6. return maker;
  7. }

(2) Particles

Often, in order to make the DSL syntax look more coherent, we need some auxiliary words to help, such as the "with" in the sentence "make.top.equalTo(superview.mas_top).with.offset(padding.top)" in Masonry.

This auxiliary word is the same as the grammar we have learned. It usually has no practical effect and simply returns self.

  1. @interface ViewMaker : NSObject
  2. @property (nonatomic, strong) Class viewClass;
  3. @property (nonatomic, assign) CGPoint position;
  4. @property (nonatomic, assign) CGPoint size ;
  5. @property (nonatomic, strong) UIColor *color;
  6. @property (nonatomic, readonly) ViewMaker * with ;
  7. @ end  
  8.  
  9. @implementation ViewMaker
  10.  
  11. - (ViewMaker *) with  
  12. {
  13. return self;
  14. }
  15. @ end  

It should be noted that if you return to yourself, there is no way to prevent users from constantly calling yourself.with.with.with. To avoid this situation, you can generate a new class. Each class has methods at its own level to avoid cross-level calls.

  1. @interface ViewMaker : NSObject
  2. @property (nonatomic, strong) Class viewClass;
  3. @property (nonatomic, assign) CGPoint position;
  4. @property (nonatomic, assign) CGPoint size ;
  5. @property (nonatomic, strong) UIColor *color;
  6. @ end  
  7.  
  8. @interface ViewClassHelper : NSObject
  9. @property (nonatomic, strong) Class viewClass;
  10. @property (nonatomic, readonly) ViewMaker * with ;
  11. @ end  
  12.  
  13. #define AllocA(aClass) alloc_a([aClass class])
  14.  
  15. ViewClassHelper* alloc_a(Class aClass){
  16. ViewClassHelper *helper = ViewClassHelper.new;
  17. helper.viewClass = aClass;
  18. return helper;
  19. }
  20. @implementation ViewClassHelper
  21.  
  22. - (ViewMaker *) with  
  23. {
  24. ViewMaker *maker = ViewMaker.new;
  25. maker.viewClass = self.viewClass;
  26. return maker;
  27. }
  28. @ end  

This effectively prevents syntax like .with.with.with. But in reality, we need to develop according to real needs. Users who use DSL for better expressiveness will not write code like .with.with.with. Such protective measures seem a bit unnecessary.

However, using classes to distinguish particles has several other small advantages. It ensures that when giving syntax prompts, the ViewClassHelper class only has a syntax prompt like .with, while ViewMaker does not have a .with syntax prompt; and at the same time ensures that .with must appear.

However, to simplify the article, we will use the former, that is, .with returns self to continue the following text:

  1. @interface ViewMaker : NSObject
  2. @property (nonatomic, strong) Class viewClass;
  3. @property (nonatomic, assign) CGPoint position;
  4. @property (nonatomic, assign) CGPoint size ;
  5. @property (nonatomic, strong) UIColor *color;
  6. @property (nonatomic, readonly) ViewMaker * with ;
  7. @ end  
  8.  
  9. @implementation ViewMaker
  10.  
  11. - (ViewMaker *) with  
  12. {
  13. return self;
  14. }
  15. @ end  

(3) Modifying part: attributive

Like the position size bgColor in the example, these are the attributive parts used to modify UIView. They exist in the ViewMaker instance in the form of attributes. In order to support chain expressions, they will continue to return self when implemented.

Let's try to implement this:

  1. @interface ViewMaker : NSObject
  2. // ...
  3. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);
  4. @property (nonatomic, copy) ViewMaker* (^ size )(CGFloat x, CGFloat y);
  5. @property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color);
  6. @ end  
  7.  
  8. @implementation ViewMaker
  9.  
  10. - (instancetype)init
  11. {
  12. if (self = [super init]) {
  13. @weakify(self)
  14. _position = ^ViewMaker *(CGFloat x, CGFloat y) {
  15. @strongify(self)
  16. self.position = CGPointMake(x, y);
  17. };
  18. _size = ^ViewMaker *(CGFloat x, CGFloat y) {
  19. @strongify(self)
  20. self. size = CGPointMake(x, y);
  21. };
  22. _bgColor = ^ViewMaker *(UIColor *color) {
  23. @strongify(self)
  24. self.color = color;
  25. };
  26. }
  27. return self;
  28. }
  29. @ end  

(4) Terminal word

The term "terminal word" is really not found in modern grammar, but it is particularly important in DSL. The ViewMaker instance collects a lot of modifications from beginning to end, and a *** expression word is needed to produce the *** result, which is called a "terminal word". For example, in the open source library Expecta, equal is to show the real behavior, and neither to nor notTo will actually trigger the behavior.

In our example, the term .intoView(aSuperView) can be implemented like this:

  1. @interface ViewMaker : NSObject
  2. // ...
  3. @property (nonatomic, copy) UIView* (^intoView)(UIView *superView);
  4. @ end  
  5.  
  6. @implementation ViewMaker
  7.  
  8. - (instancetype)init
  9. {
  10. if (self = [super init]) {
  11. @weakify(self)
  12. // ...
  13. _intoView = ^UIView *(UIView *superView) {
  14. @strongify(self)
  15. CGRect rect = CGRectMake(self.position.x, self.position.y,
  16. self. size .width, self. size .height);
  17. UIView * view = [[UIView alloc] initWithFrame:rect];
  18. view .backgroundColor = self.color;
  19. [superView addSubView: view ];
  20. return   view ;
  21. };
  22. }
  23. return self;
  24. }
  25. @ end  

In this way, a terminal word is written.

Summary of the final code:

  1. @interface ViewMaker : NSObject
  2. @property (nonatomic, strong) Class viewClass;
  3. @property (nonatomic, assign) CGPoint position;
  4. @property (nonatomic, assign) CGPoint size ;
  5. @property (nonatomic, strong) UIColor *color;
  6. @property (nonatomic, readonly) ViewMaker * with ;
  7. @property (nonatomic, copy) ViewMaker* (^position)(CGFloat x, CGFloat y);
  8. @property (nonatomic, copy) ViewMaker* (^ size )(CGFloat x, CGFloat y);
  9. @property (nonatomic, copy) ViewMaker* (^bgColor)(UIColor *color);
  10. @property (nonatomic, copy) UIView* (^intoView)(UIView *superView);
  11. @ end  
  12.  
  13. @implementation ViewMaker
  14.  
  15. - (instancetype)init
  16. {
  17. if (self = [super init]) {
  18. @weakify(self)
  19. _position = ^ViewMaker *(CGFloat x, CGFloat y) {
  20. @strongify(self)
  21. self.position = CGPointMake(x, y);
  22. };
  23. _size = ^ViewMaker *(CGFloat x, CGFloat y) {
  24. @strongify(self)
  25. self. size = CGPointMake(x, y);
  26. };
  27. _bgColor = ^ViewMaker *(UIColor *color) {
  28. @strongify(self)
  29. self.color = color;
  30. };
  31. _intoView = ^UIView *(UIView *superView) {
  32. @strongify(self)
  33. CGRect rect = CGRectMake(self.position.x, self.position.y,
  34. self. size .width, self. size .height);
  35. UIView * view = [[UIView alloc] initWithFrame:rect];
  36. view .backgroundColor = self.color;
  37. [superView addSubView: view ];
  38. return   view ;
  39. };
  40. }
  41. return self;
  42. }
  43.  
  44. - (ViewMaker *) with  
  45. {
  46. return self;
  47. }
  48. @ end  

Summarize

This chain call can make the program clearer and more readable in certain scenarios. The same is true in Swift. You can make good use of it to make your code more beautiful.

In fact, if iOS developers want to continue to improve and grow into true masters, they must put their vision above business needs, streamline and strengthen their core skills, and improve their mastery of languages ​​​​and tools in order to improve development efficiency and enhance their skill levels.

Here we have prepared more fun and advanced iOS black magic attack and defense techniques for you to get twice the result with half the effort. StuQ Academy has specially invited Mr. Zang Chengwei, a senior iOS technical expert who is well-liked by students, to open the course "iOS Practical Black Magic". You can efficiently get the advanced iOS black magic attack and defense techniques that must be mastered in 6 weeks and 12 hours, so that you can gradually step out of ordinary developers, see a different language, and experience a different development!

<<:  Reward Collection | The second issue of Aiti Tribe Stories is officially launched

>>:  Android development: from modularization to componentization (Part 1)

Recommend

iOS 15.4 beta allows you to unlock your iPhone while wearing a mask

It is understood that this update appears under &...

Maugham's Literature Class: How to Read and Write

Introduction Maugham, author of The Moon and Sixp...

11 common mistakes that new entrepreneurs make

[[155037]] The process of starting a business is ...

Across a century, we meet the "Chinese Marie Curie"

[Popular Science Long Picture] Across a Century, ...

Apple releases first iOS 9.1 beta to public beta users

[[148706]] After the press conference yesterday, ...