코드의 결합도를 줄여라

부끄럼타는 코드를 작성하라. 즉 불필요한 어떤 것도 달느 모듈에 보여주지 않으며, 다른 모듈의 구현에 의존하지 않는 코드르 작성하라. 그리고 디미터 법칙을 따르려 노력 해 보자. 객체의 상태를 바꿀 필요가 있다면, 객체 스스로가 여러분을 위해 그러한 일을 수행하게 만들라. 이렇게 한다면 코드는 다른 코드 구현으로부터 분리 된 채로 남아있을 것이며, 계속하여 직교성을 유지 할 기회가 많아 질 것이다.

전역 데이터를 피하라

코드가 전역 데이터를 참조 할 때마다, 코드는 해당 데이터를 공유하는 다른 컴포넌트와 묶이게 된다. 읽기 전용 목적으로 전역 데이터를 사용한다 하더라도 문제가 발생 할 수 있다. 예를 들어 코드를 갑자기 멀티쓰레드로 바꿔야 한다면 어떻게 될까? 일반적으로 모듈이 필요로 하는 컨텍스트를 명시적으로 넘겨주면 코드를 이해하고 유지보수하기 쉽게 된다. 객체지향 애플리케이션에서는 컨텍스트를 객체 생성자의 매개 변수로 넘기기도 한다. 또한 컨텍스트를 포함하는 구조체를 만들어 이를 필요로 하는 모듈에 레퍼런스로 넘겨 줄 수도 있다. 싱글톤 패턴은 특정 클래스의 객체가 단 하나의 인스턴스만을 갖도록 보장 해 준다.
하지만 많은 개발자들이 싱글톤 객체를 전역 데이터의 일종으로 남용한다 (특히 자바와 같이 전역 개념을 지원하지 않는 언어의 경우에는 더욱 심하다). 싱글톤을 사용 할 때는 주의를 기울여라. 싱글톤은 불필요한 링크를 유도한다. 

유사한 함수를 피하라

종종 유사해 보이는 함수의 집합을 구현해야 할 때가 있다. 아마도 시작과 끝에서는 공통 코드를 공유하지만 중간의 알고리즘이 다를 것이다. 중복 코드는 구조적 문제의 징후다. 스트래티지 패턴을 사용하여 더 나은 구현을 할 수는 없는지 고려 해보기 바란다.


실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
 

'Archive' 카테고리의 다른 글

슈뢰딩거의 고양이 이야기  (0) 2011.10.02
직교성과 테스트  (0) 2011.10.01
직교적인 설계가 되어있는지 테스트하는 방법  (0) 2011.10.01
코드의 직교성의 장점  (0) 2011.10.01
코드내의 문서화  (0) 2011.10.01
  
특정 기능에 대한 요구사항을 변경했을 경우, 몇 개의 모듈이 영향을 받는가?
직교적인 시스템에서는 답이 '하나' 여야 한다.
(이상적인 경우. 실제로는 직교적일지라도 하나보단 많은 경우가 많다)

 
실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
  

코드의 직교성의 장점

Posted by epicdev Archive : 2011. 10. 1. 12:59
생산성 향상

변화가 국소화되서 개발 시간과 테스트 시간이 줄어든다. 상대적으로 작고, 자족적인 컴포넌트를 작성하는 것이 하나의 커다란 코드 덩어리를 만드는 것보다 더 쉽다. 간단한 컴포넌트들은 설계하고, 코딩하고, 단위 테스트하고, 그러고는 잊어버릴 수 있다. 새로운 코드를 추가할 때마다 기존의 코드를 계속 바꾸어야 할 필요가 없다.

직교적인 접근법은 또한 재사용을 촉진한다. 컴포넌트들에 명확하고 잘 정의된 책임이 할당되어 있다면 애초의 구현자들이 미처 생각하지 못했던 방식으로 새로운 컴포넌트와 결합할 수 있다. 시스템이 더 느슨하게 결합되어 있을수록 재설정하고 리엔지니어링하기 쉽다.

직교적인 컴포넌트들을 결합하는 경우 꽤 미묘한 생산성 향상이 있다. 컴포넌트 하나가 M가지 서로 다른 일을 한다고 치고, 또 다른 컴포넌트 하나가 N가지 다른 일을 한다고 가정하자. 만약 그것들이 직교적이라면 결합했을 때 결과물은 M X N개 만큼  일을 한다. 그렇지만, 두 개의 컴포넌트가 직교적이지 못하면 겹치는 부분이 있을 테고, 결과물이 할 수 있는 일은 그 이하일 것이다. 직교적인 컴포넌트들을 결합함으로써 단위 노력당 더 많은 기능을 얻을 수 있다.

리스크 감소

감염된 코드는 격리된다. 어떤 모듈이 병에 걸렸다 해도 시스템의 나머지 부분으로 증상이 전파될 확률이 낮다. 게다가 그 부분만 도려내고 새롭고 건강한 놈으로 이식해 넣기도 쉽다.

시스템이 잘 깨어지지 않는다. 어떤 부분을 골라서 약간 바꾸고 수리해도 거기서 생기는 문제점들은 그 부분에만 한정될 것이다.

직교적인 시스템은 해당 컴포넌트들에 대해 테스트를 설계하고 실행하기 훨씬 쉽기 때문에, 아무래도 더 많은 테스트를 하게 된다.

써드파티 컴포넌트로 연결되는 인터페이스들이 전체 개발의 작은 부분에 한정되기 때문에 특정 벤더나 제품, 플랫폼에 덜 종속될 것이다. 


실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
 
  

코드내의 문서화

Posted by epicdev Archive : 2011. 10. 1. 12:21
프로그래머는 자신의 코드에 주석을 달도록 교육받는다. 훌륭한 코드에는 주석이 많다고 배운다. 불행히도 그들은 코드에 왜 주석이 필요한지 배우지 않는다. 나쁜 코드야 말로 많은 주석을 필요로 한다.

Don't repeat yourself 원칙은 낮은 차원의 지식은 그것이 속하는 코드에 놔두고, 주석은 다른 높은 차원의 설명을 위해 아껴두라고 말한다. 그러지 않으면 지식을 중복하게 되며, 변경 할 때마다 매번 코드와 주석 모두를 바꾸어야 한다. 주석은 필연적으로 낡게 될 것이고, 믿을 수 없는 주석은 주석이 전혀 없는 것보다 더 심각한 문제를 만들어 낸다.


실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
 
  

어떻게 코드에 중복이 생기는가?

Posted by epicdev Archive : 2011. 10. 1. 12:17
강요된 중복: 개발자들은 다른 선택이 없다고 느낀다. 환경이 중복을 요구하는 것처럼 보인다.

부주의한 중복: 개발자들은 자신들이 정보를 중복하고 있다는 것을 깨닫지 못한다.

참을성 없는 중복: 중복이 쉬워 보이기 때문에 개발자들이 게을러져서 중복을 하게 된다.

개발자간의 중복: 한 팀에 있는 (혹은 다른 팀에 있는) 여러 사람들이 동일한 정보를 중복한다. 


실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
 

'Archive' 카테고리의 다른 글

코드의 직교성의 장점  (0) 2011.10.01
코드내의 문서화  (0) 2011.10.01
언제 멈춰야 할지 알라  (0) 2011.10.01
리소스가 없다고 자꾸 뜰 경우  (0) 2011.09.29
소프트웨어에 버전을 매기는 방법  (0) 2011.09.25
  

언제 멈춰야 할지 알라

Posted by epicdev Archive : 2011. 10. 1. 10:47
어떤 면에서 프로그래밍은 그림 그리기와 유사하다. 깨끗한 캔버스와 몇 가지 기본 재료를 갖고 시작한다. 과학, 예술 그리고 기술을 조합해서 그것들로 뭘 할지 결정한다. 전체 그림을 스케치하고 주변 환경을 칠한 다음, 세부 내용을 채워 넣는다. 자신이 한 것을 비판적인 눈으로 보기 위해 늘 뒤로 물러서서 보기도 한다. 때로는 캔버스를 버리고 완전히 새로 시작하기도 한다.

하지만 예술가들은 여러분에게 언제 멈춰야 할지를 알지 못하면 이 모든 고된 작업을 망치게 될 거라고 말해 준다. 칠한 위에 덧칠하고, 세부묘사 위에 다시 세부묘사를 하다보면, 그림은 물감 속에서 사라진다.

완벽하게 훌륭한 프로그램을 과도하게 장식하거나 지나칠 정도로 다듬느라 망치지 말라. 그냥 넘어가고 코드가 현재 상태에서 한동안은 그대로 있도록 놓아두라. 완벽하지 않을 수도 있다. 걱정하지 마라. 완벽해지기란 불가능하다.

 
실용주의프로그래머
카테고리 컴퓨터/IT > 프로그래밍/언어
지은이 앤드류 헌트 (인사이트, 2007년)
상세보기
  

Object를 byte array로 쓰고 읽기

Posted by epicdev Archive : 2011. 9. 24. 00:01
출처: http://scr4tchp4d.blogspot.com/2008/07/object-to-byte-array-and-byte-array-to.html
 
privimive 타입이나 java.io.Serializable 인터페이스가 구현 된 객체만 쓰고 읽을 수 있음
public byte[] toByteArray (Object obj)
{
  byte[] bytes = null;
  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  try {
    ObjectOutputStream oos = new ObjectOutputStream(bos); 
    oos.writeObject(obj);
    oos.flush(); 
    oos.close(); 
    bos.close();
    bytes = bos.toByteArray ();
  }
  catch (IOException ex) {
    //TODO: Handle the exception
  }
  return bytes;
}
    
public Object toObject (byte[] bytes)
{
  Object obj = null;
  try {
    ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
    ObjectInputStream ois = new ObjectInputStream (bis);
    obj = ois.readObject();
  }
  catch (IOException ex) {
    //TODO: Handle the exception
  }
  catch (ClassNotFoundException ex) {
    //TODO: Handle the exception
  }
  return obj;
}
  

비동기 I/O (Asynchronous I/O)

Posted by epicdev Archive : 2011. 9. 21. 16:03
출처: http://en.wikipedia.org/wiki/Asynchronous_I/O

Asynchronous I/O

From Wikipedia, the free encyclopedia

Asynchronous I/O, or non-blocking I/O, is a form of input/output processing that permits other processing to continue before the transmission has finished.

Input and output (I/O) operations on a computer can be extremely slow compared to the processing of data. An I/O device can incorporate mechanical devices that must physically move, such as a hard drive seeking a track to read or write; this is often orders of magnitude slower than the switching of electric current. For example, during a disk operation that takes ten milliseconds to perform, a processor that is clocked at one gigahertz could have performed ten million instruction-processing cycles.

A simple approach to I/O would be to start the access and then wait for it to complete. But such an approach (called synchronous I/O or blocking I/O) would block the progress of a program while the communication is in progress, leaving system resources idle. When a program makes many I/O operations, this means that the processor can spend almost all of its time idle waiting for I/O operations to complete.

Alternatively, it is possible, but more complicated to predict, to start the communication and then perform processing that does not require that the I/O has completed. This approach is called asynchronous input/output. Any task that actually depends on the I/O having completed (this includes both using the input values and critical operations that claim to assure that a write operation has been completed) still needs to wait for the I/O operation to complete, and thus is still blocked, but other processing that does not have a dependency on the I/O operation can continue.

Many operating system functions exist to implement asynchronous I/O at many levels. In fact, one of the main functions of all but the most rudimentary of operating systems is to perform at least some form of basic asynchronous I/O, though this may not be particularly apparent to the operator or programmer. In the simplest software solution, the hardware device status is polled at intervals to detect whether the device is ready for its next operation. (For example the CP/M operating system was built this way. Its system call semantics did not require any more elaborate I/O structure than this, though most implementations were more complex, and thereby more efficient.) Direct memory access (DMA) can greatly increase the efficiency of a polling-based system, and hardware interrupts can eliminate the need for polling entirely. Multitasking operating systems can exploit the functionality provided by hardware interrupts, whilst hiding the complexity of interrupt handling from the user. Spooling was one of the first forms of multitasking designed to exploit asynchronous I/O. Finally, multithreading and explicit asynchronous I/O APIs within user processes can exploit asynchronous I/O further, at the cost of extra software complexity.

Asynchronous I/O is used to improve throughput, latency, and/or responsiveness.

Contents

 [hide]

[edit]Forms

All forms of asynchronous I/O open applications up to potential resource conflicts and associated failure. Careful programming (often using mutual exclusionsemaphores, etc.) is required to prevent this.

When exposing asynchronous I/O to applications there are a few broad classes of implementation. The form of the API provided to the application does not necessarily correspond with the mechanism actually provided by the operating system; emulations are possible. Furthermore, more than one method may be used by a single application, depending on its needs and the desires of its programmer(s). Many operating systems provide more than one of these mechanisms, it is possible that some may provide all of them.

[edit]Process

Available in early Unix. In a multitasking operating system, processing can be distributed across different processes, which run independently, have their own memory, and process their own I/O flows; these flows are typically connected in pipelines. Processes are fairly expensive to create and maintain, so this solution only works well if the set of processes is small and relatively stable. It also assumes that the individual processes can operate independently, apart from processing each other's I/O; if they need to communicate in other ways, coordinating them can become difficult.

An extension of this approach is dataflow programming, which allows more complicated networks than just the chains that pipes support.

[edit]Polling

Variations:

  • Error if it cannot be done yet (reissue later)
  • Report when it can be done without blocking (then issue it)

Available in traditional Unix. Its major problem is that it can waste CPU time polling repeatedly when there is nothing else for the issuing process to do, reducing the time available for other processes. Also, because a polling application is essentially single-threaded it may be unable to fully exploit I/O parallelism that the hardware is capable of.

[edit]Select(/poll) loops

Available in BSD Unix, and almost anything else with a TCP/IP protocol stack that either utilizes or is modeled after the BSD implementation. A variation on the theme of polling, a select loop uses the select system call to sleep until a condition occurs on a file descriptor (e.g., when data is available for reading), a timeout occurs, or a signal is received (e.g., when a child process dies). By examining the return parameters of the select call, the loop finds out which file descriptor has changed and executes the appropriate code. Often, for ease of use, the select loop is implemented as an event loop, perhaps using callback functions; the situation lends itself particularly well to event-driven programming.

While this method is reliable and relatively efficient, it depends heavily on the Unix paradigm that "everything is a file"; any blocking I/O that does not involve a file descriptor will block the process. The select loop also relies on being able to involve all I/O in the central select call; libraries that conduct their own I/O are particularly problematic in this respect. An additional potential problem is that the select and the I/O operations are still sufficiently decoupled that select's result may effectively be a lie: if two processes are reading from a single file descriptor (arguably bad design) the select may indicate the availability of read data that has disappeared by the time that the read is issued, thus resulting in blocking; if two processes are writing to a single file descriptor (not that uncommon) the select may indicate immediate writability yet the write may still block, because a buffer has been filled by the other process in the interim, or due to the write being too large for the available buffer or in other ways unsuitable to the recipient.

The select loop doesn't reach the ultimate system efficiencies possible with, say, the completion queues method, because the semantics of the select call, allowing as it does for per-call tuning of the acceptable event set, consumes some amount of time per invocation traversing the selection array. This creates little overhead for user applications that might have open one file descriptor for the windowing system and a few for open files, but becomes more of a problem as the number of potential event sources grows, and can hinder development of many-client server applications; other asynchronous methods may be noticeably more efficient in such cases. Some Unixes provide system-specific calls with better scaling; for example, epoll in Linux (that fills the return selection array with only those event sources on which an event has occurred), kqueue in FreeBSD, and/dev/poll in Solaris.

SVR3 Unix provided the poll system call. Arguably better-named than select, for the purposes of this discussion it is essentially the same thing. SVR4 Unixes (and thus POSIX) offer both calls.

[edit]Signals (interrupts)

Available in BSD and POSIX Unix. I/O is issued asynchronously, and when it is complete a signal (interrupt) is generated. As in low-level kernel programming, the facilities available for safe use within the signal handler are limited, and the main flow of the process could have been interrupted at nearly any point, resulting in inconsistent data structures as seen by the signal handler. The signal handler is usually not able to issue further asynchronous I/O by itself.

The signal approach, though relatively simple to implement within the OS, brings to the application program the unwelcome baggage associated with writing an operating system's kernel interrupt system. Its worst characteristic is that every blocking (synchronous) system call is potentially interruptible; the programmer must usually incorporate retry code at each call.

[edit]Callback functions

Available in Mac OS (pre-Mac OS X), VMS and Windows. Bears many of the characteristics of the signal method as it is fundamentally the same thing, though rarely recognized as such. The difference is that each I/O request usually can have its own completion function, whereas the signalsystem has a single callback.

A potential problem is that stack depth can grow unmanageably, as an extremely common thing to do when one I/O is finished is to schedule another. If this should be satisfied immediately, the first callback is not 'unwound' off the stack before the next one is invoked. Systems to prevent this (like 'mid-ground' scheduling of new work) add complexity and reduce performance.

[edit]Light-weight processes or threads

Light-weight processes (LWPs) or threads are available in more modern Unixes, originating in Plan 9. Like the process method, but without the data isolation that hampers coordination of the flows. This lack of isolation introduces its own problems, usually requiring kernel-provided synchronization mechanisms and thread-safe libraries. Each LWP or thread itself uses traditional blocking synchronous I/O. The requisite separate per-thread stack may preclude large-scale implementations using very large numbers of threads. The separation of textual (code) and time (event) flows provides fertile ground for errors.

This approach is also used in the Erlang programming language runtime system. The Erlang virtual machine uses asynchronous IO using a small pool of only a few threads or sometimes just one process, to handle IO from up to millions of Erlang processes. IO handling in each process is written mostly using blocking synchronous I/O. This way high performance of asynchronous I/O is merged with simplicity of normal IO. Many IO problems in Erlang are mapped to message passing, which can be easily processed using built-in selective receive.

[edit]Completion queues/ports

Available in Microsoft WindowsSolaris and DNIX. I/O requests are issued asynchronously, but notifications of completion are provided via a synchronizing queue mechanism in the order they are completed. Usually associated with a state-machine structuring of the main process (event-driven programming), which can bear little resemblance to a process that does not use asynchronous I/O or that uses one of the other forms, hampering code reuse. Does not require additional special synchronization mechanisms or thread-safe libraries, nor are the textual (code) and time (event) flows separated.

[edit]Event flags

Available in VMS. Bears many of the characteristics of the completion queue method, as it is essentially a completion queue of depth one. To simulate the effect of queue 'depth', an additional event flag is required for each potential unprocessed (but completed) event, or event information can be lost. Waiting for the next available event in such a clump requires synchronizing mechanisms that may not scale well to larger numbers of potentially parallel events.

[edit]Implementation

The vast majority of general-purpose computing hardware relies entirely upon two methods of implementing asynchronous I/O: polling and interrupts. Usually both methods are used together, the balance depends heavily upon the design of the hardware and its required performance characteristics. (DMA is not itself another independent method, it is merely a means by which more work can be done per poll or interrupt.)

Pure polling systems are entirely possible, small microcontrollers (such as systems using the PIC) are often built this way. CP/M systems could also be built this way (though rarely were), with or without DMA. Also, when the utmost performance is necessary for only a few tasks, at the expense of any other potential tasks, polling may also be appropriate as the overhead of taking interrupts may be unwelcome. (Servicing an interrupt requires time [and space] to save at least part of the processor state, along with the time required to resume the interrupted task.)

Most general-purpose computing systems rely heavily upon interrupts. A pure interrupt system may be possible, though usually some component of polling is also required, as it is very common for multiple potential sources of interrupts to share a common interrupt signal line, in which case polling is used within the device driver to resolve the actual source. (This resolution time also contributes to an interrupt system's performance penalty. Over the years a great deal of work has been done to try to minimize the overhead associated with servicing an interrupt. Current interrupt systems are rather lackadaisical when compared to some highly-tuned earlier ones, but the general increase in hardware performance has greatly mitigated this.)

Hybrid approaches are also possible, wherein an interrupt can trigger the beginning of some burst of asynchronous I/O, and polling is used within the burst itself. This technique is common in high-speed device drivers, such as network or disk, where the time lost in returning to the pre-interrupt task is greater than the time until the next required servicing. (Common I/O hardware in use these days relies heavily upon DMA and large data buffers to make up for a relatively poorly-performing interrupt system. These characteristically use polling inside the driver loops, and can exhibit tremendous throughput. Ideally the per-datum polls are always successful, or at most repeated a small number of times.)

At one time this sort of hybrid approach was common in disk and network drivers where there was not DMA or significant buffering available. Because the desired transfer speeds were faster even than could tolerate the minimum four-operation per-datum loop (bit-test, conditional-branch-to-self, fetch, and store), the hardware would often be built with automatic wait state generation on the I/O device, pushing the data ready poll out of software and onto the processor's fetch or store hardware and reducing the programmed loop to two operations. (In effect using the processor itself as a DMA engine.) The 6502 processor offered an unusual means to provide a three-element per-datum loop, as it had a hardware pin that, when asserted, would cause the processor's Overflow bit to be set directly. (Obviously one would have to take great care in the hardware design to avoid overriding the Overflow bit outside of the device driver!)

[edit]Synthesis

Using only these two tools (polling, and interrupts), all the other forms of asynchronous I/O discussed above may be (and in fact, are) synthesized.

In an environment such as a Java Virtual Machine (JVM), asynchronous I/O can be synthesized even though the environment the JVM is running in may not offer it at all. This is due to the interpreted nature of the JVM. The JVM may poll (or take an interrupt) periodically to institute an internal flow of control change, effecting the appearance of multiple simultaneous processes, at least some of which presumably exist in order to perform asynchronous I/O. (Of course, at the microscopic level the parallelism may be rather coarse and exhibit some non-ideal characteristics, but on the surface it will appear to be as desired.)

That, in fact, is the problem with using polling in any form to synthesize a different form of asynchronous I/O. Every CPU cycle that is a poll is wasted, and lost to overhead rather than accomplishing a desired task. Every CPU cycle that is not a poll represents an increase in latency of reaction to pending I/O. Striking an acceptable balance between these two opposing forces is difficult. (This is why hardware interrupt systems were invented in the first place.)

The trick to maximize efficiency is to minimize the amount of work that has to be done upon reception of an interrupt in order to awaken the appropriate application. Secondarily (but perhaps no less important) is the method the application itself uses to determine what it needs to do.

Particularly problematic (for application efficiency) are the exposed polling methods, including the select/poll mechanisms. Though the underlying I/O events they are interested in are in all likelihood interrupt-driven, the interaction to this mechanism is polled and can consume a large amount of time in the poll. This is particularly true of the potentially large-scale polling possible through select (and poll). Interrupts map very well to Signals, Callback functions, Completion Queues, and Event flags, such systems can be very efficient.

[edit]References

[edit]See also

[edit]External links

 
  

Tail recursion

Posted by epicdev Archive : 2011. 9. 8. 01:02
  
 «이전 1 ··· 3 4 5 6  다음»