Object Oriented Design
- From a client’s point of view, understand what problem are you trying to solve. Find out all use-cases and properties of the system.
- Compose a narrative of the individual use cases. Your first set of use cases shouldn’t be a laundry list of everything the program will eventually do. Start with the smallest set of use cases you can come up with that still captures the essence of what your program is for. For stack overflow, for example, the core use cases might be log in, ask a question, answer a question, and view questions and answers. Nothing about reputation, voting, or the community wiki, just the raw essence of what you’re shooting for.
- A number of teams that have embraced use cases find themselves, without realizing it, practicing top-down functional design (“the system must do a, then b, …”). . Their role in object-oriented software construction has been misunderstood. Rather than an analysis tool they are a validation tool.
- Use cases emphasize ordering (“When a customer places an order over the phone, his credit card number is validated. Then the database is updated and a confirmation number is issued”, etc.). This is incompatible with object technology: the method shuns early reliance on sequentiality properties, because they are so fragile and subject to change. The competent O-O analyst and designer refuses to focus on properties of the form “The system does a, then b”; instead, he asks the question “What are the operations available on instances of abstraction A, and the constraints on these operations?”. The truly fundamental sequentiality properties will emerge in the form of high-level constraints on the operations; for example, instead of saying that a stack supports alternating sequences of push and pop operations with never more pop than push, we define the preconditions attached with each of these operations, which imply the ordering property but are more abstract. Less fundamental ordering requirements simply have no place in the analysis model as they destroy the system’s adaptability and hence its future survival. Early emphasis on ordering is among the worst mistakes an O-O project can make. If you rely on use cases for analysis, this mistake is hard to avoid.
- Relying on a scenario means that you focus on how users see the system’s operation. But the system does not exist yet. So the system picture that use cases will give you is based on existing processes, computerized or not. Your task as a system builder is to come up with new, better scenarios, not to perpetuate antiquated modes of operation. There are enough examples around of computer systems that slavishly mimic obsolete procedures.
- Use cases favor a functional approach, based on processes (actions). This approach is the reverse of O-O decomposition, which focuses on data abstractions; it carries a serious risk of reverting, under the heading of object-oriented development, to the most traditional forms of functional design. True, you may rely on several scenarios rather than just one main program. But this is still an approach that considers what the system does as the starting point, whereas object technology considers what it does it to. The clash is irreconcilable.
- Thinking primarily in sequences of actions and collecting whatever information required from wherever that information is lying around. In this approach, functions are highly cohesive and isolated in one place, but data is grabbed from many other objects. Functions depend on the internal details of many objects. Changes in a function may require changes in several objects. It is difficult to judge the impact of changes to a single object, because many other functions depend on it as well. Functions become sensitive to changes across the system.
- A class describes an abstract data type – a set of software objects characterized by well-defined operations and formal properties of these operations. When you are assessing whether a certain notion should yield a class or not, only the ADT view can provide the right criterion: do the objects of the system under discussion exhibit enough specific operations and properties of their own, relevant to the system and not covered by existing classes?
- When we define an abstract data type, we’re extending the universe of built-in types provided by the language to include a new type, with new operations, appropriate to our problem domain. This new type is like a new language: a new set of nouns (values) and verbs (operations) we can manipulate. Of course, those nouns and verbs are abstractions built on top of the existing nouns and verbs which were themselves already abstractions. A language has greater flexibility than a mere program, because we can use a language to solve a large class of related problems, instead of just a single problem.
- Imagine an object oriented system as a cage in which objects live. When a request is fired to the system, the objects work together to fulfill the request. Every object does what it can do best and delegates the rest to its collaborators – the other objects it works together with. Start with the top level user stories and sketch the high-level interactions they imply. This gets you the first idea of what the big modules are; and an iteration or two of high level CRC-card like play you should have stabilised a list of major components, what they do and how they interact.
- Go through the use case narrative and highlight nouns (person, place, thing), as candidate classes and verbs (actions), as methods / behaviors. You will be aided by the requirements document, but do not expect grammatical criteria to be of more than superficial help.
- What to do: Identify the important concepts in your problem domain (noun and verb phrases mention repeatedly). The candidates we find are typically passive concepts from the real world without any behavior. For example, a task in a workflow system is a passive thing being executed by a real world person and an invoice is something passive being paid for by a real world client.
- What to do: As you come up with potential classes, don’t think of them only in terms of what noun they represent, but what responsibilities they have. We find responsibilities by making passive things active. We assign responsibilities to the passive concepts that are associated with them. For example, a workflow task gets the responsibility to execute itself. An invoice gets the responsibility to pay itself. All messages that the object responds to should match its responsibilities. We look for what an object needs to know to fulfill its responsibilities. This drives us towards putting information where we directly need it.
- Not every noun is a class. All your classes which describe roles should probably simply be variables, not classes. Pet is such an example. Both, a dog and a cat can be a pet. In fact, one can have any animal as pet. Pet is much more a description of the relationship between the pet holder and the animal.
- Use Delegation/Composition whenever it makes sense to create classes for roles. You could have a class Pet. But in that case, Pet should not be a subclass of these other classes like class Dog. Instead you should use delegation / composition, like this: class Pet { Animal animal;}
Then the class Pet describes a role, and that role can be fulfilled by any type of animal.
- A class does not just cover physical “objects” in the naïve sense. A type of real-world objects may or may not have a counterpart in the software in the form of a type of software objects — a class.
- This is the biggest aid in figuring out how classes relate to each other during program execution. It’s easy to come up with relationships like “a dog is an animal” or “a puppy has one mother.” It’s usually harder to figure out relationships describing run-time interactions between objects. You’re program’s algorithms are at least as important as your objects, and they’re much easier to design if you’ve spelled out what each class’s job is.
- If any of the responsibilities are large or complex, refine those modules down until you have things that are small and simple enough to be objects, by playing out the interactions inside the module for each of the major operations identified by the higher level interactions. Knowing when to stop is a matter of judgement (which only comes with experience).
- We can simulate a usage scenario of the system, by letting objects respond to messages and delegate tasks that other objects can do better to those objects. Next, we look for what an object needs to know to fulfill its responsibilities. This drives us towards putting information where we directly need it. As a result of localizing this knowledge to objects, changes to the resulting system tend to be localized as well. Changes in one place do not ripple through the design and will not affect other parts. This reduces the risk of introducing defects.
- We suggest driving a design toward completion with the aid of execution scenarios. We start with only one or two obvious cards and start playing “what-if”. If the situation calls for a responsibility not already covered by one of the objects we either add the responsibility to one of the objects, or create a new object to address that responsibility. If one of the object becomes too cluttered during this process we copy the information on its card to a new card, searching for more concise and powerful ways of saying what the object does. If it is not possible to shrink the information further, but the object is still too complex, we create a new object to assume some of the responsibilities. We stress the importance of creating objects not to meet mythical future needs, but only under the demands of the moment. This ensures that a design contains only as much information as the designer has directly experienced, and avoids premature complexity.
- Theory of Abstract Data Types
- Requirement: The elevator will close its door before it moves to another floor. Is “door” a separate data type with its own clearly identified operations, or are all the operations on doors already covered by operations on other data types such as ELEVATOR?
- Example of a noun which may or may not give a class in the elevator example is floor. Here (as opposed to the door and operation cases) the question is not whether the concept is a relevant ADT: floors are definitely an important data abstraction for an elevator system. But this does not necessarily mean we should have a FLOOR class. The reason is simply that the properties of floors may be entirely covered, for the purposes of the elevator system, by those of integers. Each floor has a floor number; then if a floor (as seen by the elevator system) has no other features than those associated with its floor number, you may not need a separate FLOOR class. A typical floor feature that comes from a feature of integers is the distance between two floors, which is simply the difference of their floor numbers. If, however, floors have properties other than those of their numbers – that is to say, according to the principles of abstract data types and object-oriented software construction, significant operations not covered by those of integers – then a FLOOR class will be appropriate. For example, some floors may have special access rights defining who can visit them; then the FLOOR class could include a feature such as “rights: SET [AUTHORIZATION]” and the associated procedures. But even that is not certain: we might get away by including in some other class an array: “floor_rights: ARRAY [SET [AUTHORIZATION]] which simply associates a set of AUTHORIZATION values with each floor, identified by its number. Another argument for having a specific class FLOOR would be to limit the available operations: it makes sense to subtract two floors and to compare them (through the infix “<” function), but not to add or multiply them. Such a class may be written as an heir to INTEGER. The designer must ask himself, however, whether this goal really justifies adding a new class.
- What I would recommend is to use the following techniques, which if applied liberally, will force you to use OO techniques, even though you may not yet be aware of them.
- Create classes that represent the things you talk about when talking about the functionality - for example, an order entry system will have Orders, Customers, Accounts, OrderItems, InventoryItems, etc.
- When creating these classes, do NOT code any public set and get methods to access the class data members.
- Add methods to these domain model classes that perform the work on the object in question. Order.invoice(), account.close(), InventoryItem.decrement(). The lack of public get methods will show you the correct location for the code (where the data is - in the appropriate domain object). Remember, an object is data and the code that operates on it - anything short of both is not an object.
- You will eventually find that you have to add some public get methods for some class members, and that is ok, just hold off until you are forced to do so. Reluctantly add public get methods.
- At the level of the application, almost every line of code should be moving mountains. In other words, most of the lines of code at the application level should be calling on domain model methods.
- Put all of the functionality in the domain model objects, then expose that functionality in an application by hooking it up to a user interface. I repeat, put the functionality in the domain model, not the application.
- Consider the undo-redo mechanism design. The discussion distinguished between commands, such as the line insertion command in a text editor, and the more general notion of operation, which includes commands but also special requests such as Undo. Both of these words figured prominently in the statement of the problem; yet only COMMAND yielded a data abstraction (one of the principal classes of the design), whereas no class in the solution directly reflects the notion of operation. No analysis of a requirements document can suggest this striking difference of treatment. if the author of the requirements for a text editor with undo-redo has written “the system must support line insertion and deletion”, we are in luck since we can spot the nouns insertion and deletion; but the need for these facilities may just as well follow from a sentence of the form “The editor must allow its users to insert or delete a line at the current cursor position leading the naïve designer to devote his attention to the trivial notions of “cursor” and “position” while missing the command abstractions (line insertion and line deletion).
- A class is not supposed to do one thing but to offer a number of services (features) on objects of a certain type. If it really does just one thing, it is probably a case of the Grand Mistake: devising a class for what should just be a routine of some other class. This usually points to a design flaw: This class prints the results” or “this class parses the input”, or some other variant of “This class does…”. Even if (as is most likely the case here) the classes discussed represent valuable data abstractions, it would be preferable to describe them less operationally by emphasizing these abstractions.
- A class representing a car is no more tangible than one that models the job satisfaction of employees. What counts is how important the concepts are to the enterprise, and what you can do with them. Keep this comment in mind when looking for external classes: they can be quite abstract. SENIORITY_RULE for a parliament voting system and MARKET_TENDENCY for a trading system may be just as real as SENATOR and STOCK_EXCHANGE. A good external class will be based on abstract concepts of the problem domain, characterized (in the ADT way) through external features chosen because of their lasting value.
- Discard duplicate nouns and factor out common functionality. Create a class diagram. Apply OOD principles to organize your classes (factor out common functionality, build hierarchies, etc.)
- when a certain set of classes has been proposed to solve a certain problem, you should study them from the criteria and principles of modularity given : do they constitute autonomous, coherent modules, with strictly controlled communication channels? Often, the discovery that two modules are too tightly coupled, that a module communicates with too many others, that an argument list is too long, will pinpoint design errors and lead to a better solution.
- An important criterion was explored in the panel-driven system example: data flow. We saw then how important it is to study, in a candidate class structure, the flow of objects passed as arguments in successive calls. If, as with the notion of State in that example, you detect that a certain item of information is transmitted over many modules, it is almost certainly a sign that you have missed an important data abstraction. Such an analysis, which we applied to obtain the class STATE, is an important source of abstractions.
- Once you’ve got that minimal set of use cases and objects, start coding. Get something that actually runs as soon as possible, even though it doesn’t do much and probably looks like crap. It’s a starting point, and will force you to answer questions you might gloss over on paper.
- Now go back and pick more use cases, write up how they’ll work, modify your class model, and write more code. Just like your first cut, take on as little at a time as you can while still adding something meaningful. Rinse and repeat.
Practice
TETRIS
“Implement Tetris” is a problem the can’t be given away. There’s no secret that you could tell someone that would fundamentally improve how they implement Tetris. It’s a process of programming and design choices.
Contrast this with “Imagine a person walking up a flight of stairs. Imagine that at any point the person can either take a small stride (up a single step) or a large stride (up two steps). How many unique paths are there to reach the Nth stair?” The solution to this problem is the fibonacci series. Now, I don’t mean to say that being able to solve this problem means nothing. Being able to answer this is some indication that a programmer is mathy and smart. But the problem can be given away, requires a single leap of insight, and is probably a bad interview question for those reasons.
If you’ve done Tetris on your own beforehand the interview will go much better/faster. There are a couple of bits of tricky logic you won’t get hung up on (rotations and deleting full lines) and you’ll be able to speak much more fluently about the design problems and the trade-offs of each since you’re not thinking it through completely on the spot. That is, knowing the problem well I could walk through a complete solution in ~20 minutes. When I solved it as an interviewer it took more like 60 minutes. That seems like it fits your description?
-
Create a class design to represent a filesystem.
-
Design an OO representation to model HTML.
-
How would you implement the rendering engine of a web browser?
-
What would be a good data structure for a photo editor?
-
Text Editor
- https://stackoverflow.com/questions/32211409/data-structure-to-implement-text-editor
- https://stackoverflow.com/questions/649279/what-is-best-data-structure-suitable-to-implement-editor-like-notepad
- https://stackoverflow.com/questions/2143817/4-program-design-interview-questions
References
22 March 2019