Path: csiph.com!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail From: "Pascal J. Bourguignon" Newsgroups: comp.lang.objective-c Subject: Re: A question on designated initializers Date: Sun, 29 Nov 2015 03:45:31 +0100 Organization: Informatimago Lines: 281 Message-ID: <87fuzpseus.fsf@kuiper.lan.informatimago.com> References: <87r3jhwosu.fsf@kuiper.lan.informatimago.com> <87egfhwmsj.fsf@kuiper.lan.informatimago.com> <87poyus47x.fsf@kuiper.lan.informatimago.com> Mime-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Trace: individual.net Kr26g8eoa5n0r4bDr3z8/ASYCrlv6jzQPJumuYJe24qe5OdKOj Cancel-Lock: sha1:YTRiMmEzNDZlZGZlMzM1NWM0ZDYwODA4NzM2ZmY5ZjIxMWQ0MDFlNw== sha1:wnAkVmOr7ojLgnQcAOKPWq092bY= Face: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwAQMAAABtzGvEAAAABlBMVEUAAAD///+l2Z/dAAAA oElEQVR4nK3OsRHCMAwF0O8YQufUNIQRGIAja9CxSA55AxZgFO4coMgYrEDDQZWPIlNAjwq9 033pbOBPtbXuB6PKNBn5gZkhGa86Z4x2wE67O+06WxGD/HCOGR0deY3f9Ijwwt7rNGNf6Oac l/GuZTF1wFGKiYYHKSFAkjIo1b6sCYS1sVmFhhhahKQssRjRT90ITWUk6vvK3RsPGs+M1RuR mV+hO/VvFAAAAABJRU5ErkJggg== X-Accept-Language: fr, es, en User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/24.3 (gnu/linux) Xref: csiph.com comp.lang.objective-c:244 Jon Rossen writes: > There was one simple example given that discussed a Rectangle class > that had a Square sub-class. Obviously, in Geometry a square is a > rectangle but a rectangle is not a square. The programming issue that > was brought up was basically an outgrowth of this subset/superset > relationship involving the setters needed to set the dimension of the > objects. For rectangles you'd need height and width but for squares > you really only need one, and furthermore having two for the square > would not really be correct contextually. These seem like expected > inherent differences in these two classes, but I was getting mixed up > following the article and whether it was suggesting that this > condition between these two classes were following LSP or 'breaking' > it. Basically I got lost trying to fully understand how this example > was being used. Perhaps I'll reread that specific section and you as > Professor Bourguignon can fill me in with answers if I can figure out > how to craft some specific questions. :-) It all depends on the set of invariants and pre-/post- conditions you choose. Basically, when you don't specify them, then you have: invariant = true all pre-condition = true all post-condition = true and since true => true, anything goes in subclasses. But also, all bugs are features, of course. If you have: -(int)width; -(void)setWidth:(int)aWidth; then you have nothing. Ie. you cannot say anything about: ([r setWidth:42],[r width]) If you have: @interface Rectangle -(int)width; -(void)setWidth:(int)aWidth; // pre: h==[self height] // post: aWidth==[self width] && h==[self height] @end and similar for heigth, then you won't be able to have Square with the invariant [square width]==[square height] as a subclass of Rectangle. But if you have: @interface Rectangle -(bool)invariant { return YES; } -(int)width; -(void)setWidth:(int)aWidth; // pre: true // post: aWidth==[self width] @end then you can have Square as a subclass of Rectangle, with: @interface Square -(bool)invariant { return [self width]==[self height]; } -(void)setWidth:(int)aWidth; // pre: true // post: aWidth==[self width] && aWidth==[self height] @end because: true ==> true and: (aWidth==[self width] && aWidth==[self height]) ==> aWidth==[self width] therefore the pre-condition of -[Rectangle setWidth:] ==> the pre-condition of -[Square setWidth:] and the post-condition of -[Square setWidth:] ==> the post-condition of -[Rectangle setWidth:] and -[Square invariant] ==> -[Rectangle invariant] so Liskov's substitution principle will let you substitue any Rectangle instance by a Square instance anywhere. (Because basically, you are not expecting that [self width]!=[self height], you are just expecting TRUE (ie. anything)). Notice that there are two sources of difficulty here: 1- mutation. 2- lack of multiple-inheritance. Consider the graph: https://en.wikipedia.org/wiki/Parallelogram#/media/File:Symmetries_of_square.svg it actually gives an inheriting graph (with the superclasses down), where some subclasses inherit from multiple superclasses (thus representing an intersection of the set of values of the superclasses). Trapezoid isSubclassOf: IrregularQuadrilateral IsoceleTrapezoid isSubclassOf: Trapezoid Parallelogram isSubclassOf: IrregularQuadrilateral Kite isSubclassOf: IrregularQuadrilateral Rhombus isSubclassOf: Parallelogram and: Kite Rectangle isSubclassOf: Parallelogram and: IsoceleTrapezoid Square isSubclassOf: Parallelogram and: Rectangle and: Rhombus If we consider only Square and Rectangle, we could have: @interface Rectangle -(id)initWithWidth:(int)w andHeight:(int)h; //designated initialiser; -(bool)invariant{return [self width]>0 && [self height]>0;} -(int)width; -(int)height; // no setters, no mutation @end @interface Square -(id)initWithSide:(int)side; //designated initialiser; -(bool)invariant{return [super invariant] && [self width]==[self height];} -(int)width; -(int)height; // no setters, no mutation @end @implementation Rectangle -(id)clusterInitWithWidth:(int)w andHeight:(int)h{ if((self=[super init])){ width=w; height=h; } return self; } -(id)initWithWidth:(int)w andHeight:(int)h{ w=max(1,w); h=max(1,h); if(w==h){ [self dealloc]; self=[[Square alloc]initWithSide:w]; }else{ self=[self clusterInitWithWidth:w andHeight:h]; } return self; } @end @implementation Square -(id)initWithSide:(int)s return [self clusterInitWithWidth:s andHeight:s]; } @end So now, when you do: [Rectangle initWithWidth:3 andHeight:3] you get actually an instance of Square, which is a subclass of Rectangle, so all the method of Rectangle still apply (thanks to Liskov's Substitution Principle). (Notice that in Foundation and AppKit, there are several such instances of class clusters, where allocating a superclass actually gives you an instance of a specific subclass) But since in Objective-C we lack multiple-inheritance, this using immutable classes goes only so far, since we couldn't make Square also a subclass of Rhombus and Parallelogram. Types are often considered as mathematical sets of values. On the other hand, classes, while having a potential set of instances, define more a representation than a set of values. For example, we could have two unrelated classes: @interface Rectangle -(id)initWithWidth:(int)w andHeight:(int)h; -(int)width; -(int)height; @end @interface Square -(id)initWithSide:(int)s; -(int)side; @end id r=[[Rectangle alloc]initWithWidth:3 andHeight:3]; id s=[[Square alloc]initWithSide:3]; What is r? You'll say it's an instance of Rectangle, but it's also the representation of a square of side 3. Just like s, is an instance of Square, and the representation of the same square of side 3. So we have two representations of a square of side 3. Mathematically, there would be a single type, a single set of a square of side 3, with this unique element, but in OO, we have defined two different classes and two different instances representating this same value. So one should not confuse types and classes, and the set operations that are mathematically valid on types, are not directly applicable to classes and their instances. Having two unrelated classes for Square and Rectangle can be as problematic has having one class being a subclass of the other or vice-versa. You have to choose what representations are the best for the problem at hand, by specifying the invariants, pre-/post- conditions that makes you program simple and clear, and therefore the class hierarchy that works best for the current problem. For example, if you have to deal with polygons and their mathematical properties, it might be better to have a general Polygon class, allowing for any number of points at any coordinates, and instead of representing the mathematical sets Square Rectangle, Rhombus, Parallelograms, Triangle, Hexagon, etc, you could define: @interface Polygon -(id)initWithPoints:(NSArray*)points; -(bool)invariant{return [point count]>=3;} -(bool)isRectangle; -(bool)isRhombus; -(bool)isParallelogram; -(bool)isSquare{return [self isParallelogram] && [self isRectangle] && [self isRhombus];} … -(float)baseAngle; … @end So the definition of Square as the intersection of Rectangle Parallelogram and Rhombus translates clearly as a conjuction in the predicate -isSquare. And this model would allow mutation, since you could easily "move" a single point of the parallelogram (or add or remove points), and the predicates would "reclassify" it. -- __Pascal Bourguignon__ http://www.informatimago.com/ “The factory of the future will have only two employees, a man and a dog. The man will be there to feed the dog. The dog will be there to keep the man from touching the equipment.” -- Carl Bass CEO Autodesk